Skip to content

Commit d2d5ab8

Browse files
committed
Add describe() method to BeanAdapter.
1 parent 1ce95bc commit d2d5ab8

File tree

5 files changed

+237
-12
lines changed

5 files changed

+237
-12
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ HTTP-RPC provides the following classes for creating and consuming REST services
4141
* `RequestMethod` - annotation that associates an HTTP verb with a service method
4242
* `RequestParameter` - annotation that associates a custom request parameter name with a method argument
4343
* `ResourcePath` - annotation that associates a resource path with a service method
44+
* `Response` - annotation that associates a response description with a service method
4445
* `JSONEncoder` - class that serializes an object hierarchy to JSON
4546
* `JSONDecoder` - class that deserializes an object hierarchy from JSON
4647
* `CSVEncoder` - class that serializes an iterable sequence of values to CSV
@@ -415,7 +416,7 @@ The `BeanAdapter` class implements the `Map` interface and exposes any propertie
415416

416417
If a property value is `null` or an instance of one of the following types, it is returned as is:
417418

418-
* `String`
419+
* `CharSequence`
419420
* `Number`
420421
* `Boolean`
421422
* `Enum`

httprpc/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ plugins {
2020
}
2121

2222
group = 'org.httprpc'
23-
version = '5.7.2'
23+
version = '5.8'
2424

2525
repositories {
2626
mavenCentral()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package org.httprpc;
16+
17+
import java.lang.annotation.ElementType;
18+
import java.lang.annotation.Retention;
19+
import java.lang.annotation.RetentionPolicy;
20+
import java.lang.annotation.Target;
21+
22+
import org.httprpc.beans.BeanAdapter;
23+
24+
/**
25+
* Annotation that associates a response description with a service method.
26+
* Values should be encoded as described for
27+
* {@link BeanAdapter#describe(java.lang.reflect.Type, java.util.Map)}.
28+
*/
29+
@Retention(RetentionPolicy.RUNTIME)
30+
@Target(ElementType.METHOD)
31+
public @interface Response {
32+
/**
33+
* @return
34+
* The response description associated with the method.
35+
*/
36+
public String value();
37+
}

httprpc/src/main/java/org/httprpc/beans/BeanAdapter.java

Lines changed: 163 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.List;
3535
import java.util.Map;
3636
import java.util.Set;
37+
import java.util.TreeMap;
3738

3839
/**
3940
* Class that presents the properties of a Java bean object as a map. Property
@@ -194,6 +195,10 @@ private BeanAdapter(Object bean, HashMap<Class<?>, HashMap<String, Method>> acce
194195
}
195196

196197
private static String getKey(Method method, boolean accessor) {
198+
if (method.isBridge()) {
199+
return null;
200+
}
201+
197202
String methodName = method.getName();
198203

199204
String prefix;
@@ -295,11 +300,11 @@ public Entry<String, Object> next() {
295300
}
296301

297302
/**
298-
* Adapts a value. If the value is <tt>null</tt> or an
299-
* instance of one of the following types, it is returned as is:
303+
* Adapts a value. If the value is <tt>null</tt> or an instance of one of
304+
* the following types, it is returned as is:
300305
*
301306
* <ul>
302-
* <li>{@link String}</li>
307+
* <li>{@link CharSequence}</li>
303308
* <li>{@link Number}</li>
304309
* <li>{@link Boolean}</li>
305310
* <li>{@link Enum}</li>
@@ -330,7 +335,7 @@ public static <T> T adapt(Object value) {
330335

331336
private static Object adapt(Object value, HashMap<Class<?>, HashMap<String, Method>> accessorCache) {
332337
if (value == null
333-
|| value instanceof String
338+
|| value instanceof CharSequence
334339
|| value instanceof Number
335340
|| value instanceof Boolean
336341
|| value instanceof Enum<?>
@@ -358,7 +363,7 @@ private static Object adapt(Object value, HashMap<Class<?>, HashMap<String, Meth
358363
* values are automatically converted to <tt>0</tt> or <tt>false</tt> for
359364
* primitive argument types.</li>
360365
* <li>If the target type is {@link String}, the value is adapted via
361-
* {@link String#toString()}.</li>
366+
* {@link Object#toString()}.</li>
362367
* <li>If the target type is {@link Date}, the value is coerced to a long
363368
* value and passed to {@link Date#Date(long)}.</li>
364369
* <li>If the target type is {@link LocalDate}, the value is parsed using
@@ -367,7 +372,7 @@ private static Object adapt(Object value, HashMap<Class<?>, HashMap<String, Meth
367372
* {@link LocalTime#parse(CharSequence)}.</li>
368373
* <li>If the target type is {@link LocalDateTime}, the value is parsed using
369374
* {@link LocalDateTime#parse(CharSequence)}.</li>
370-
*</ul>
375+
* </ul>
371376
*
372377
* If the target type is a {@link List}, the value is wrapped in an adapter
373378
* that will adapt the list's elements. If the target type is a {@link Map},
@@ -411,11 +416,12 @@ private static <T> T adapt(Object value, Type type, HashMap<Class<?>, HashMap<St
411416
ParameterizedType parameterizedType = (ParameterizedType)type;
412417

413418
Type rawType = parameterizedType.getRawType();
419+
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
414420

415421
if (rawType == List.class) {
416-
return (T)adaptList((List<?>)value, parameterizedType.getActualTypeArguments()[0], mutatorCache);
422+
return (T)adaptList((List<?>)value, actualTypeArguments[0], mutatorCache);
417423
} else if (rawType == Map.class) {
418-
return (T)adaptMap((Map<?, ?>)value, parameterizedType.getActualTypeArguments()[1], mutatorCache);
424+
return (T)adaptMap((Map<?, ?>)value, actualTypeArguments[1], mutatorCache);
419425
} else {
420426
throw new IllegalArgumentException();
421427
}
@@ -502,7 +508,7 @@ private static Object adapt(Object value, Class<?> type, HashMap<Class<?>, HashM
502508
} else if (type == LocalDateTime.class) {
503509
return LocalDateTime.parse(value.toString());
504510
} else if (value instanceof Map<?, ?>) {
505-
return adapt((Map<?, ?>)value, type, mutatorCache);
511+
return adaptBean((Map<?, ?>)value, type, mutatorCache);
506512
} else {
507513
throw new IllegalArgumentException();
508514
}
@@ -511,7 +517,7 @@ private static Object adapt(Object value, Class<?> type, HashMap<Class<?>, HashM
511517
}
512518
}
513519

514-
private static Object adapt(Map<?, ?> map, Class<?> type, HashMap<Class<?>, HashMap<String, LinkedList<Method>>> mutatorCache) {
520+
private static Object adaptBean(Map<?, ?> map, Class<?> type, HashMap<Class<?>, HashMap<String, LinkedList<Method>>> mutatorCache) {
515521
if (!type.isInterface()) {
516522
Object object;
517523
try {
@@ -744,4 +750,151 @@ public V setValue(V value) {
744750
}
745751
};
746752
}
753+
754+
/**
755+
* Describes a type. Types are encoded as follows:
756+
*
757+
* <ul>
758+
* <li>{@link Object}: "any"</li>
759+
* <li>{@link Void} or <tt>void</tt>: "void"</li>
760+
* <li>{@link Byte} or <tt>byte</tt>: "byte"</li>
761+
* <li>{@link Short} or <tt>short</tt>: "short"</li>
762+
* <li>{@link Integer} or <tt>int</tt>: "integer"</li>
763+
* <li>{@link Long} or <tt>long</tt>: "long"</li>
764+
* <li>{@link Float} or <tt>float</tt>: "float"</li>
765+
* <li>{@link Double} or <tt>double</tt>: "double"</li>
766+
* <li>Any other type that extends {@link Number}: "number"</li>
767+
* <li>Any type that implements {@link CharSequence}: "string"</li>
768+
* <li>Any {@link Enum} type: "enum"</li>
769+
* <li>Any type that extends {@link Date}: "date"</li>
770+
* <li>{@link LocalDate}: "local-date"</li>
771+
* <li>{@link LocalTime}: "local-time"</li>
772+
* <li>{@link LocalDateTime}: "local-datetime"</li>
773+
* <li>{@link LocalDateTime}: "local-datetime"</li>
774+
* <li>{@link List}: "[<i>element description</i>]"</li>
775+
* <li>{@link Map}: "[<i>key description</i>: <i>value description</i>]"</li>
776+
* </ul>
777+
*
778+
* Otherwise, the type is assumed to be a bean and is described as follows:
779+
*
780+
* <blockquote>
781+
* {
782+
* property1: <i>property 1 description</i>,
783+
* property2: <i>property 2 description</i>,
784+
* ...
785+
* }
786+
* </blockquote>
787+
*
788+
* @param type
789+
* The type to describe.
790+
*
791+
* @param structures
792+
* A map that will be populated with descriptions of all bean types
793+
* referenced by this type.
794+
*
795+
* @return
796+
* The type's description.
797+
*/
798+
public static String describe(Type type, Map<Class<?>, String> structures) {
799+
if (type instanceof Class<?>) {
800+
return describe((Class<?>)type, structures);
801+
} else if (type instanceof WildcardType) {
802+
WildcardType wildcardType = (WildcardType)type;
803+
804+
return describe(wildcardType.getUpperBounds()[0], structures);
805+
} else if (type instanceof ParameterizedType) {
806+
ParameterizedType parameterizedType = (ParameterizedType)type;
807+
808+
Type rawType = parameterizedType.getRawType();
809+
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
810+
811+
if (rawType == List.class) {
812+
return "[" + describe(actualTypeArguments[0], structures) + "]";
813+
} else if (rawType == Map.class) {
814+
return "[" + describe(actualTypeArguments[0], structures) + ": " + describe(actualTypeArguments[1], structures) + "]";
815+
} else {
816+
throw new IllegalArgumentException();
817+
}
818+
} else {
819+
throw new IllegalArgumentException();
820+
}
821+
}
822+
823+
private static String describe(Class<?> type, Map<Class<?>, String> structures) {
824+
if (type == Object.class) {
825+
return "any";
826+
} else if (type == Void.TYPE || type == Void.class) {
827+
return "void";
828+
} else if (type == Byte.TYPE || type == Byte.class) {
829+
return "byte";
830+
} else if (type == Short.TYPE || type == Short.class) {
831+
return "short";
832+
} else if (type == Integer.TYPE || type == Integer.class) {
833+
return "integer";
834+
} else if (type == Long.TYPE || type == Long.class) {
835+
return "long";
836+
} else if (type == Float.TYPE || type == Float.class) {
837+
return "float";
838+
} else if (type == Double.TYPE || type == Double.class) {
839+
return "double";
840+
} else if (Number.class.isAssignableFrom(type)) {
841+
return "number";
842+
} else if (type == Boolean.TYPE || type == Boolean.class) {
843+
return "boolean";
844+
} else if (CharSequence.class.isAssignableFrom(type)) {
845+
return "string";
846+
} else if (Enum.class.isAssignableFrom(type)) {
847+
return "enum";
848+
} else if (Date.class.isAssignableFrom(type)) {
849+
return "date";
850+
} else if (type == LocalDate.class) {
851+
return "local-date";
852+
} else if (type == LocalTime.class) {
853+
return "local-time";
854+
} else if (type == LocalDateTime.class) {
855+
return "local-datetime";
856+
} else {
857+
if (!structures.containsKey(type)) {
858+
Method[] methods = type.getMethods();
859+
860+
TreeMap<String, String> properties = new TreeMap<>();
861+
862+
for (int i = 0; i < methods.length; i++) {
863+
Method method = methods[i];
864+
865+
if (method.getDeclaringClass() == Object.class) {
866+
continue;
867+
}
868+
869+
String key = getKey(method, true);
870+
871+
if (key != null) {
872+
properties.put(key, describe(method.getGenericReturnType(), structures));
873+
}
874+
}
875+
876+
int j = 0;
877+
878+
StringBuilder descriptionBuilder = new StringBuilder();
879+
880+
descriptionBuilder.append("{\n");
881+
882+
for (Map.Entry<String, String> entry : properties.entrySet()) {
883+
if (j > 0) {
884+
descriptionBuilder.append(",\n");
885+
}
886+
887+
descriptionBuilder.append(" " + entry.getKey() + ": " + entry.getValue());
888+
889+
j++;
890+
}
891+
892+
descriptionBuilder.append("\n}");
893+
894+
structures.put(type, descriptionBuilder.toString());
895+
}
896+
897+
return type.getSimpleName();
898+
}
899+
}
747900
}

httprpc/src/test/java/org/httprpc/beans/BeanAdapterTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.time.LocalDateTime;
2929
import java.time.LocalTime;
3030
import java.util.Date;
31+
import java.util.HashMap;
3132
import java.util.Map;
3233

3334
public class BeanAdapterTest extends AbstractTest {
@@ -135,4 +136,37 @@ public void testBeanAdapter2() throws IOException {
135136

136137
Assert.assertEquals(true, result.getNestedBean().getFlag());
137138
}
139+
140+
@Test
141+
public void testDescribe() {
142+
HashMap<Class<?>, String> structures = new HashMap<>();
143+
144+
BeanAdapter.describe(TestBean.class, structures);
145+
146+
Assert.assertEquals(structures.get(TestBean.class),
147+
"{\n" +
148+
" bigInteger: number,\n" +
149+
" date: date,\n" +
150+
" dayOfWeek: enum,\n" +
151+
" double: double,\n" +
152+
" i: integer,\n" +
153+
" list: [any],\n" +
154+
" localDate: local-date,\n" +
155+
" localDateTime: local-datetime,\n" +
156+
" localTime: local-time,\n" +
157+
" long: long,\n" +
158+
" map: [string: any],\n" +
159+
" nestedBean: NestedBean,\n" +
160+
" nestedBeanList: [NestedBean],\n" +
161+
" nestedBeanMap: [string: NestedBean],\n" +
162+
" string: string\n" +
163+
"}"
164+
);
165+
166+
Assert.assertEquals(structures.get(TestBean.NestedBean.class),
167+
"{\n" +
168+
" flag: boolean\n" +
169+
"}"
170+
);
171+
}
138172
}

0 commit comments

Comments
 (0)