Skip to content

Commit d2d1fbf

Browse files
committed
Ensure that typed adapters correctly handle Object methods.
1 parent 626f17d commit d2d1fbf

File tree

8 files changed

+206
-99
lines changed

8 files changed

+206
-99
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
subprojects {
1616
group = 'org.httprpc'
17-
version = '7.4.2'
17+
version = '7.4.3'
1818

1919
apply plugin: 'java-library'
2020
apply plugin: 'maven-publish'

httprpc-client/src/main/java/org/httprpc/WebServiceException.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
* Thrown to indicate that a service operation returned an error.
2121
*/
2222
public class WebServiceException extends IOException {
23-
private static final long serialVersionUID = 0;
24-
2523
private int statusCode;
2624

2725
/**

httprpc-client/src/main/java/org/httprpc/WebServiceProxy.java

Lines changed: 117 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.io.OutputStream;
2424
import java.io.OutputStreamWriter;
2525
import java.io.UnsupportedEncodingException;
26+
import java.lang.reflect.InvocationHandler;
27+
import java.lang.reflect.Method;
2628
import java.lang.reflect.Parameter;
2729
import java.lang.reflect.Proxy;
2830
import java.net.HttpURLConnection;
@@ -103,6 +105,118 @@ public interface ResponseHandler<T> {
103105
T decodeResponse(InputStream inputStream, String contentType, Map<String, String> headers) throws IOException;
104106
}
105107

108+
// Typed invocation handler
109+
private static class TypedInvocationHandler implements InvocationHandler {
110+
URL baseURL;
111+
Class<?> type;
112+
BiFunction<String, URL, WebServiceProxy> factory;
113+
114+
HashMap<Object, Object> keys;
115+
116+
TypedInvocationHandler(URL baseURL, Class<?> type, BiFunction<String, URL, WebServiceProxy> factory) {
117+
this.baseURL = baseURL;
118+
this.type = type;
119+
this.factory = factory;
120+
121+
if (Map.class.isAssignableFrom(type)) {
122+
keys = new HashMap<>();
123+
} else {
124+
keys = null;
125+
}
126+
}
127+
128+
@Override
129+
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
130+
Class<?> declaringClass = method.getDeclaringClass();
131+
132+
if (declaringClass == Object.class) {
133+
return method.invoke(this, arguments);
134+
} else if (declaringClass == Map.class) {
135+
return method.invoke(keys, arguments);
136+
} else {
137+
RequestMethod requestMethod = method.getAnnotation(RequestMethod.class);
138+
139+
if (requestMethod == null) {
140+
throw new UnsupportedOperationException();
141+
}
142+
143+
ResourcePath resourcePath = method.getAnnotation(ResourcePath.class);
144+
145+
URL url;
146+
if (resourcePath != null) {
147+
String[] components = resourcePath.value().split("/");
148+
149+
for (int i = 0; i < components.length; i++) {
150+
String component = components[i];
151+
152+
if (component.length() == 0) {
153+
continue;
154+
}
155+
156+
if (component.startsWith(ResourcePath.PATH_VARIABLE_PREFIX)) {
157+
int k = ResourcePath.PATH_VARIABLE_PREFIX.length();
158+
159+
if (component.length() == k || component.charAt(k++) != ':') {
160+
throw new IllegalStateException("Invalid path variable.");
161+
}
162+
163+
Object value = getParameterValue(keys.get(component.substring(k)));
164+
165+
if (value != null) {
166+
components[i] = URLEncoder.encode(value.toString(), UTF_8);
167+
} else {
168+
components[i] = "";
169+
}
170+
}
171+
}
172+
173+
url = new URL(baseURL, String.join("/", components));
174+
} else {
175+
url = baseURL;
176+
}
177+
178+
WebServiceProxy webServiceProxy = factory.apply(requestMethod.value(), url);
179+
180+
Parameter[] parameters = method.getParameters();
181+
182+
HashMap<String, Object> argumentMap = new HashMap<>();
183+
184+
for (int i = 0; i < parameters.length; i++) {
185+
Parameter parameter = parameters[i];
186+
187+
argumentMap.put(parameter.getName(), arguments[i]);
188+
}
189+
190+
webServiceProxy.setArguments(argumentMap);
191+
192+
return BeanAdapter.adapt(webServiceProxy.invoke(), method.getGenericReturnType());
193+
}
194+
}
195+
196+
@Override
197+
public int hashCode() {
198+
return type.hashCode();
199+
}
200+
201+
@Override
202+
public boolean equals(Object object) {
203+
if (object instanceof Proxy) {
204+
object = Proxy.getInvocationHandler(object);
205+
}
206+
207+
if (!(object instanceof TypedInvocationHandler)) {
208+
return false;
209+
}
210+
211+
return (type == ((TypedInvocationHandler)object).type);
212+
}
213+
214+
@Override
215+
public String toString() {
216+
return type.toString();
217+
}
218+
}
219+
106220
private String method;
107221
private URL url;
108222

@@ -672,76 +786,8 @@ public static <T> T adapt(URL baseURL, Class<T> type, BiFunction<String, URL, We
672786
throw new IllegalArgumentException();
673787
}
674788

675-
HashMap<Object, Object> keys;
676-
if (Map.class.isAssignableFrom(type)) {
677-
keys = new HashMap<>();
678-
} else {
679-
keys = null;
680-
}
681-
682-
return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, (proxy, method, arguments) -> {
683-
if (method.getDeclaringClass() == Map.class) {
684-
return method.invoke(keys, arguments);
685-
} else {
686-
RequestMethod requestMethod = method.getAnnotation(RequestMethod.class);
687-
688-
if (requestMethod == null) {
689-
throw new UnsupportedOperationException();
690-
}
691-
692-
ResourcePath resourcePath = method.getAnnotation(ResourcePath.class);
693-
694-
URL url;
695-
if (resourcePath != null) {
696-
String[] components = resourcePath.value().split("/");
697-
698-
for (int i = 0; i < components.length; i++) {
699-
String component = components[i];
700-
701-
if (component.length() == 0) {
702-
continue;
703-
}
704-
705-
if (component.startsWith(ResourcePath.PATH_VARIABLE_PREFIX)) {
706-
int k = ResourcePath.PATH_VARIABLE_PREFIX.length();
707-
708-
if (component.length() == k || component.charAt(k++) != ':') {
709-
throw new IllegalStateException("Invalid path variable.");
710-
}
711-
712-
Object value = getParameterValue(keys.get(component.substring(k)));
713-
714-
if (value != null) {
715-
components[i] = URLEncoder.encode(value.toString(), UTF_8);
716-
}
717-
}
718-
}
719-
720-
url = new URL(baseURL, String.join("/", components));
721-
} else {
722-
url = baseURL;
723-
}
724-
725-
if (keys != null) {
726-
keys.clear();
727-
}
728-
729-
WebServiceProxy webServiceProxy = factory.apply(requestMethod.value(), url);
730-
731-
Parameter[] parameters = method.getParameters();
732-
733-
HashMap<String, Object> argumentMap = new HashMap<>();
734-
735-
for (int i = 0; i < parameters.length; i++) {
736-
Parameter parameter = parameters[i];
737-
738-
argumentMap.put(parameter.getName(), arguments[i]);
739-
}
740-
741-
webServiceProxy.setArguments(argumentMap);
742-
743-
return BeanAdapter.adapt(webServiceProxy.invoke(), method.getGenericReturnType());
744-
}
745-
}));
789+
return type.cast(Proxy.newProxyInstance(type.getClassLoader(),
790+
new Class<?>[] {type},
791+
new TypedInvocationHandler(baseURL, type, factory)));
746792
}
747793
}

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

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package org.httprpc.beans;
1616

17+
import java.lang.reflect.InvocationHandler;
1718
import java.lang.reflect.InvocationTargetException;
1819
import java.lang.reflect.Method;
1920
import java.lang.reflect.ParameterizedType;
@@ -145,6 +146,53 @@ public Object setValue(Object value) {
145146
}
146147
}
147148

149+
// Typed invocation handler
150+
private static class TypedInvocationHandler implements InvocationHandler {
151+
Map<?, ?> map;
152+
153+
TypedInvocationHandler(Map<?, ?> map) {
154+
this.map = map;
155+
}
156+
157+
@Override
158+
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
159+
if (method.getDeclaringClass() == Object.class) {
160+
return method.invoke(this, arguments);
161+
} else {
162+
String key = getKey(method);
163+
164+
if (key == null) {
165+
throw new UnsupportedOperationException();
166+
}
167+
168+
return adapt(map.get(key), method.getGenericReturnType());
169+
}
170+
}
171+
172+
@Override
173+
public int hashCode() {
174+
return map.hashCode();
175+
}
176+
177+
@Override
178+
public boolean equals(Object object) {
179+
if (object instanceof Proxy) {
180+
object = Proxy.getInvocationHandler(object);
181+
}
182+
183+
if (!(object instanceof TypedInvocationHandler)) {
184+
return false;
185+
}
186+
187+
return map.equals(((TypedInvocationHandler)object).map);
188+
}
189+
190+
@Override
191+
public String toString() {
192+
return map.toString();
193+
}
194+
}
195+
148196
private Object bean;
149197
private HashMap<Class<?>, TreeMap<String, Method>> accessorCache;
150198

@@ -511,7 +559,9 @@ private static Object adapt(Object value, Class<?> type) {
511559
} else if (type == LocalDateTime.class) {
512560
return LocalDateTime.parse(value.toString());
513561
} else if (value instanceof Map<?, ?>) {
514-
return adaptBean((Map<?, ?>)value, type);
562+
return type.cast(Proxy.newProxyInstance(type.getClassLoader(),
563+
new Class<?>[] {type},
564+
new TypedInvocationHandler((Map<?, ?>)value)));
515565
} else {
516566
throw new IllegalArgumentException();
517567
}
@@ -520,22 +570,6 @@ private static Object adapt(Object value, Class<?> type) {
520570
}
521571
}
522572

523-
private static Object adaptBean(Map<?, ?> map, Class<?> type) {
524-
return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, (proxy, method, arguments) -> {
525-
if (method.getDeclaringClass() == Object.class) {
526-
return method.invoke(map, arguments);
527-
} else {
528-
String key = getKey(method);
529-
530-
if (key == null) {
531-
throw new UnsupportedOperationException();
532-
}
533-
534-
return adapt(map.get(key), method.getGenericReturnType());
535-
}
536-
}));
537-
}
538-
539573
/**
540574
* Adapts a list instance for typed access.
541575
*

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.time.LocalDateTime;
3131
import java.time.LocalTime;
3232
import java.util.Date;
33+
import java.util.HashMap;
3334
import java.util.Map;
3435

3536
import static org.httprpc.util.Collections.entry;
@@ -66,6 +67,27 @@ public void testPrimitiveAdapt() {
6667
assertEquals(BeanAdapter.adapt("true", Boolean.TYPE), Boolean.TRUE);
6768
}
6869

70+
@Test
71+
public void testMapAdapt() {
72+
Map<String, Object> map1 = new HashMap<String, Object>() {
73+
@Override
74+
public String toString() {
75+
return "abc";
76+
}
77+
};
78+
79+
map1.put("flag", true);
80+
81+
Map<String, Object> map2 = mapOf(entry("flag", true));
82+
83+
TestInterface.NestedInterface nestedBean1 = BeanAdapter.adapt(map1, TestInterface.NestedInterface.class);
84+
TestInterface.NestedInterface nestedBean2 = BeanAdapter.adapt(map2, TestInterface.NestedInterface.class);
85+
86+
assertEquals(nestedBean1, nestedBean2);
87+
assertEquals(map1.hashCode(), nestedBean1.hashCode());
88+
assertEquals(map1.toString(), nestedBean1.toString());
89+
}
90+
6991
@Test
7092
public void testBeanAdapter1() throws MalformedURLException {
7193
Map<String, ?> expected = mapOf(

httprpc-server/src/main/java/org/httprpc/WebService.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@
6363
* Abstract base class for web services.
6464
*/
6565
public abstract class WebService extends HttpServlet {
66-
private static final long serialVersionUID = 0;
67-
6866
private static class Resource {
6967
static List<String> order = listOf("get", "post", "put", "delete");
7068

0 commit comments

Comments
 (0)