diff --git a/core/pom.xml b/core/pom.xml index ade8c8f..90aeebd 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,12 +7,12 @@ HEAD-SNAPSHOT autorest-core - jar + gwt-lib AutoREST :: core - 1.7 - 1.7 + 1.8 + 1.8 @@ -40,6 +40,18 @@ test + + + + net.ltgt.gwt.maven + gwt-maven-plugin + + com.intendia.gwt.autorest + autorest + + + + diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/CollectorResourceVisitor.java b/core/src/main/java/com/intendia/gwt/autorest/client/CollectorResourceVisitor.java index 69fd04b..98db749 100644 --- a/core/src/main/java/com/intendia/gwt/autorest/client/CollectorResourceVisitor.java +++ b/core/src/main/java/com/intendia/gwt/autorest/client/CollectorResourceVisitor.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; + import javax.annotation.Nullable; import javax.ws.rs.HttpMethod; @@ -16,29 +17,31 @@ public abstract class CollectorResourceVisitor implements ResourceVisitor { private static final String ABSOLUTE_PATH = "[a-z][a-z0-9+.-]*:.*|//.*"; private static final List DEFAULT_EXPECTED_STATUS = asList(200, 201, 204, 1223/*MSIE*/); - public static class Param { + public static class Param { public final String k; - public final Object v; - public Param(String k, Object v) { this.k = k; this.v = v; } - public static List expand(List in) { - List out = new ArrayList<>(); - for (Param p : in) { + public final T v; + public final TypeToken t; + public Param(String k, T v, TypeToken t) { this.k = k; this.v = v; this.t = t; } + + public static List> expand(List> in) { + List> out = new ArrayList<>(); + for (Param p : in) { if (!(p.v instanceof Iterable)) out.add(p); - else for (Object v : ((Iterable) p.v)) out.add(new Param(p.k, v)); + else for (Object v : ((Iterable) p.v)) out.add(new Param(p.k, v, null)); } return out; } - @Override public String toString() { return "Param{k='" + k + "', v=" + v + '}'; } + @Override public String toString() { return "Param{k='" + k + "', v=" + v + ", t='" + t + '}'; } } - protected List paths = new ArrayList<>(); - protected List queryParams = new ArrayList<>(); - protected List headerParams = new ArrayList<>(); - protected List formParams = new ArrayList<>(); + protected List paths = new ArrayList<>(); + protected List> queryParams = new ArrayList<>(); + protected List> headerParams = new ArrayList<>(); + protected List> formParams = new ArrayList<>(); protected String method = HttpMethod.GET; protected String produces[] = { "application/json" }; protected String consumes[] = { "application/json" }; - protected Object data = null; + protected Param data = null; private List expectedStatuses = DEFAULT_EXPECTED_STATUS; @Override public ResourceVisitor method(String method) { @@ -59,6 +62,11 @@ public ResourceVisitor path(String path) { return this; } + @Override public ResourceVisitor path(@Nullable T value, TypeToken typeToken) { + if (value != null) paths.add(new Param<>(null, value, typeToken)); + return this; + } + @Override public ResourceVisitor produces(String... produces) { if (produces.length > 0 /*0 means undefined, so do not override default*/) this.produces = produces; return this; @@ -69,26 +77,26 @@ public ResourceVisitor path(String path) { return this; } - @Override public ResourceVisitor param(String key, @Nullable Object value) { + @Override public ResourceVisitor param(String key, @Nullable T value, TypeToken typeToken) { Objects.requireNonNull(key, "query param key required"); - if (value != null) queryParams.add(new Param(key, value)); + if (value != null) queryParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor header(String key, @Nullable Object value) { + @Override public ResourceVisitor header(String key, @Nullable T value, TypeToken typeToken) { Objects.requireNonNull(key, "header param key required"); - if (value != null) headerParams.add(new Param(key, value)); + if (value != null) headerParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor form(String key, @Nullable Object value) { + @Override public ResourceVisitor form(String key, @Nullable T value, TypeToken typeToken) { Objects.requireNonNull(key, "form param key required"); - if (value != null) formParams.add(new Param(key, value)); + if (value != null) formParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor data(Object data) { - this.data = data; + @Override public ResourceVisitor data(T data, TypeToken typeToken) { + this.data = new Param<>(null, data, typeToken); return this; } @@ -96,7 +104,14 @@ public ResourceVisitor path(String path) { public String uri() { String path = ""; - for (String p : paths) path += p; + for (Object p : paths) { + if (p instanceof Param) { + Param param = (Param)p; + path += encodeComponent(Objects.toString(param.v)); + } else + path += p; + } + return path + query(); } @@ -105,9 +120,9 @@ public String query() { return q.isEmpty() ? "" : "?" + q; } - protected String encodeParams(List params) { + protected String encodeParams(List> params) { String q = ""; - for (Param p : expand(params)) { + for (Param p : expand(params)) { q += (q.isEmpty() ? "" : "&") + encodeComponent(p.k) + "=" + encodeComponent(Objects.toString(p.v)); } return q; diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/GwtIncompatible.java b/core/src/main/java/com/intendia/gwt/autorest/client/GwtIncompatible.java new file mode 100644 index 0000000..196d1ec --- /dev/null +++ b/core/src/main/java/com/intendia/gwt/autorest/client/GwtIncompatible.java @@ -0,0 +1,16 @@ +package com.intendia.gwt.autorest.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * GWT & J2CL will skip any classes or methods marked with this annotation (or any other as long as it is named "@GwtIncompatible") + */ +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GwtIncompatible { +} diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/ReflectionTypeUtils.java b/core/src/main/java/com/intendia/gwt/autorest/client/ReflectionTypeUtils.java new file mode 100644 index 0000000..d7347c2 --- /dev/null +++ b/core/src/main/java/com/intendia/gwt/autorest/client/ReflectionTypeUtils.java @@ -0,0 +1,62 @@ +package com.intendia.gwt.autorest.client; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * A set of private utility methods used by {@link TypeToken} in JavaSE/Android environments. + */ +@GwtIncompatible +class ReflectionTypeUtils { + private ReflectionTypeUtils() {} + + public static Type[] getActualTypeArguments(Type type) { + if (type instanceof ParameterizedType) + return ((ParameterizedType) type).getActualTypeArguments(); + else + return new Type[0]; + } + + public static Class getRawType(Type type) { + // For wildcard or type variable, the first bound determines the runtime type. + return getRawTypes(type).iterator().next(); + } + + private static Collection> getRawTypes(Type type) { + if (type instanceof Class) { + return Collections.>singleton((Class) type); + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + // JDK implementation declares getRawType() to return Class: http://goo.gl/YzaEd + return Collections.>singleton((Class) parameterizedType.getRawType()); + } else if (type instanceof GenericArrayType) { + GenericArrayType genericArrayType = (GenericArrayType) type; + return Collections.>singleton(getArrayClass(getRawType(genericArrayType.getGenericComponentType()))); + } else if (type instanceof TypeVariable) { + return getRawTypes(((TypeVariable) type).getBounds()); + } else if (type instanceof WildcardType) { + return getRawTypes(((WildcardType) type).getUpperBounds()); + } else { + throw new AssertionError(type + " unsupported"); + } + } + + /** Returns the {@code Class} object of arrays with {@code componentType}. */ + private static Class getArrayClass(Class componentType) { + return Array.newInstance(componentType, 0).getClass(); + } + + private static Collection> getRawTypes(Type[] types) { + return Arrays.stream(types) + .flatMap(type -> getRawTypes(type).stream()) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/ResourceVisitor.java b/core/src/main/java/com/intendia/gwt/autorest/client/ResourceVisitor.java index feecd0f..384d131 100644 --- a/core/src/main/java/com/intendia/gwt/autorest/client/ResourceVisitor.java +++ b/core/src/main/java/com/intendia/gwt/autorest/client/ResourceVisitor.java @@ -11,26 +11,29 @@ public interface ResourceVisitor { /** Append paths, or set if the path is absolute. */ ResourceVisitor path(Object... paths); + /** Sets a path param with its type */ + ResourceVisitor path(@Nullable T value, TypeToken typeToken); + /** Sets the produced media-type. */ ResourceVisitor produces(String... produces); /** Sets the consumed media-type. */ ResourceVisitor consumes(String... consumes); - /** Sets a query param. */ - ResourceVisitor param(String key, @Nullable Object value); + /** Sets a query param with its type */ + ResourceVisitor param(String key, @Nullable T value, TypeToken typeToken); - /** Sets a header param. */ - ResourceVisitor header(String key, @Nullable Object value); + /** Sets a header param with its type. */ + ResourceVisitor header(String key, @Nullable T value, TypeToken typeToken); - /** Sets a from param. */ - ResourceVisitor form(String key, @Nullable Object value); + /** Sets a form param with its type. */ + ResourceVisitor form(String key, @Nullable T value, TypeToken typeToken); + + /** Sets the content data with its type. */ + ResourceVisitor data(T data, TypeToken typeToken); - /** Sets the content data. */ - ResourceVisitor data(Object data); - - /** Wrap the current resource state into a {@code container}. */ - T as(Class container, Class type); + /** Wrap the current resource state into a {@code type}. */ + T as(TypeToken typeToken); interface Supplier { ResourceVisitor get(); diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/TypeToken.java b/core/src/main/java/com/intendia/gwt/autorest/client/TypeToken.java new file mode 100644 index 0000000..d6685f5 --- /dev/null +++ b/core/src/main/java/com/intendia/gwt/autorest/client/TypeToken.java @@ -0,0 +1,252 @@ +package com.intendia.gwt.autorest.client; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + +/** + * A "supertype" token capable of representing any Java type. + * + * While the purpose of this class is primarily to be instantiated by AutoRest itself and then just used by the user code, it is a public API.

+ * + * Based on ideas from http://gafter.blogspot.com/2006/12/super-type-tokens.html, + * as well as on the implementation of the same notion in various Java libraries (Guava's TypeToken, Jackson's TypeReference, Guice's TypeLiteral, + * Gson's TypeToken, Apache Commons TypeLiteral - too many to enumerate all of those here). + * + * Unlike all those other implementations however, this {@link TypeToken} is capable of operating in a GWT/J2CL environment as well, where Java reflection + * - and therefore, the {@link Type} class on which all other implementations are based of - is not available. + * + * How to use: + *
  • For simple, non-parameterized types like {@link String}, {@link Integer} or non-parameterized Java Beans:
    + *
    • TypeToken.of(String.class)
    + *
  • For parameterized types, like List<String>, in JavaSE/Android environments:
    + *
    • new TypeToken<List<String>>() {}
    + * Please note that the above statement creates an anonymous inner class - a necessary precondition for the type to be captured correctly. + *
  • For parameterized types, like List<String>, in GWT/J2CL environments:
    + *
    • new TypeToken<List<String>>(List.class, TypeToken.of(String)) {}
    + * + * A more advanced example with a multiple-nesting of a parameterized type like List<Map<Integer, String>>:
    + *
  • In JavaSE/Android environments:
    + *
    • new TypeToken<List<Map<Integer, String>>() {}
    + *
  • In GWT/J2CL environments:
    + *
    • new TypeToken<List<Map<Integer, String>>(List.class, new TypeToken<Map<Integer, String>>(Map.class, TypeToken.of(Integer.class), TypeToken.of(String.class)) {}) {}
    + *
    + * + * The syntax for GWT/J2CL is much more verbose and requires the captured type not only to be written in the type parameter of the type token (TypeToken<...>) + * but to be also explicitly enumerated as a pair of a raw class type reference (i.e. "List.class") plus a chain of nested type token instantiations describing all the + * instantiations of the type parameters of the raw type for the concrete type we are trying to capture with the type token. This verbosity is unavoidable, because GWT/J2CL is missing Java reflection, + * which in turn prohibits the {@link TypeToken} instance from "introspecting" itself and figuring out the type automatically. + *

    + * Nevertheless, the concept of a type token is very useful in these environments too, as it allows generified interfaces like the following (from AutoRest RequestVisitor):
    + *
  • ResourceVisitor param(String key, T data, TypeToken typeToken)
    + * Without the presence of the {@link TypeToken} parameter, the above method signature deteriorates to:
    + *
  • ResourceVisitor param(String key, Object data)

    + * + * Last but not least, the presence of {@link TypeToken} in the above method signatures allows AutoRest to easily interoperate with 3rd party libraries which e.g. need to know the proper type + * so as to deserialize the (JSON) payload returned from the server. + */ +public class TypeToken implements Comparable> { + private Class rawType; + private TypeToken[] typeArguments; + + @GwtIncompatible + private Type runtimeType; + + public static TypeToken of(Class type) { + return new TypeToken(type, new TypeToken[0]); + } + + /** + * Allows the construction of a {@link TypeToken} instance based on a provided Java Reflection {@link Type} instance. Not type safe. + */ + @GwtIncompatible + public static TypeToken of(Type type) { + return new TypeToken(type); + } + + /** + * Use this constructor only in code that needs to be compatible with GWtT/J2CL. For JavaSE/Android-only code, {@link TypeToken#TypeToken()} quite a bit less verbose. + */ + protected TypeToken(Class rawType, TypeToken... typeArguments) { + if (rawType != null && rawType.isArray()) { + // User provided the array directly as a raw class + // Normalize to the standard type token representation of arrays, where the raw type is null, and the array component type is in the type arguments + if (typeArguments.length > 0) + throw new IllegalArgumentException("To create a type token for an array, either pass the non-generic array class instance as the raw type and keep the type argumetns empty, or pass null as raw type and provide a single type argument for the component type of the (possibly generic) array"); + + typeArguments = new TypeToken[] {TypeToken.of(rawType.getComponentType())}; + rawType = null; + } + + this.rawType = rawType; + this.typeArguments = typeArguments; + } + + /** + * Less verbose alternative to {@link TypeToken#TypeToken(Class, TypeToken...)}. Only available in JavaSE/Android. + */ + @GwtIncompatible + protected TypeToken() { + initialize(); + } + + @GwtIncompatible + private TypeToken(Type type) { + this.runtimeType = type; + initialize(); + } + + /** + * Return the raw type represented by this {@link TypeToken} instance. E.g.:
    + *
  • When called on TypeToken<String> it will return String.class + *
  • When called on TypeToken<List<String>> it will return List.class
    + * + * For arrays, this method will return null. + */ + public final Class getRawType() { + return rawType; + } + + /** + * Return the type tokens corresponding to the type arguments of the parameterized type represented by this type token. If the type is not parameterized, + * an empty array is returned. For example:
    + *
  • When called on TypeToken<String> an empty array will be returned + *
  • When called on TypeToken<List<String>> a single-element array with a type token TypeToken<String> + *
  • When called on TypeToken<String[]> a single-element array with a type token TypeToken<String> will be returned as well + */ + public final TypeToken[] getTypeArguments() { + return typeArguments; + } + + /** + * A JavaSE/Android-only method that returns the underlying Java Reflection {@link Type} instance. + */ + @GwtIncompatible + public final synchronized Type getType() { + initialize(); // Call initialize() because runtimeType might not be populated yet in case the (only) GWT-compatible constructor was used + return runtimeType; + } + + /** + * The only reason we define this method (and require implementation + * of Comparable) is to prevent constructing a + * reference without type information. + */ + @Override + public final int compareTo(TypeToken o) { + return 0; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((rawType == null) ? 0 : rawType.hashCode()); + result = prime * result + Arrays.hashCode(typeArguments); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof TypeToken)) + return false; + TypeToken other = (TypeToken) obj; + if (rawType == null) { + if (other.rawType != null) + return false; + } else if (!rawType.equals(other.rawType)) + return false; + if (!Arrays.equals(typeArguments, other.typeArguments)) + return false; + return true; + } + + @Override + public String toString() { + return "TypeToken<" + stringify() + ">"; + } + + public final String stringify() { + StringBuilder buf = new StringBuilder(); + + stringify(buf); + + return buf.toString(); + } + + private void stringify(StringBuilder buf) { + if (getRawType() != null) { + buf.append(getRawType().getName()); + + if (typeArguments.length > 0) { + buf.append('<'); + + for (int i = 0; i < typeArguments.length; i++) { + if(i > 0) + buf.append(", "); + + typeArguments[i].stringify(buf); + } + + buf.append('>'); + } + } else { + typeArguments[0].stringify(buf); + + buf.append("[]"); + } + } + + @SuppressWarnings("unchecked") + @GwtIncompatible + private void initialize() { + if (runtimeType == null) { + if (getClass() == TypeToken.class) { + // The type token was created with a raw type only + // Assign the raw type to the runtime type + if (rawType != null) + runtimeType = rawType; + else + // Array + runtimeType = Array.newInstance(typeArguments[0].getRawType(), 0).getClass(); + } else { + // The type token was created via inheritance and is likely not representing the raw type + // Extract the actual type using the "supertype" token trick + + Type superClass = getClass().getGenericSuperclass(); + + if (superClass instanceof Class) { // Should never happen + throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information"); + } + + runtimeType = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + } + + if (typeArguments == null) { + if (runtimeType instanceof GenericArrayType) { + rawType = null; + typeArguments = new TypeToken[] {new TypeToken(((GenericArrayType)runtimeType).getGenericComponentType())}; + } else { + rawType = (Class)ReflectionTypeUtils.getRawType(runtimeType); + + typeArguments = Arrays.stream(ReflectionTypeUtils.getActualTypeArguments(runtimeType)) + .map(TypeToken::new) + .toArray(TypeToken[]::new); + + if (rawType.isArray()) { + // Normalize to the canonical representtion of an array + if (typeArguments.length == 0) + typeArguments = new TypeToken[] {new TypeToken(rawType.getComponentType())}; + + rawType = null; + } + } + } + } +} diff --git a/core/src/main/module.gwt.xml b/core/src/main/module.gwt.xml new file mode 100644 index 0000000..300c8c7 --- /dev/null +++ b/core/src/main/module.gwt.xml @@ -0,0 +1,3 @@ + + + diff --git a/core/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java b/core/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java index 69c0e2d..1afebfd 100644 --- a/core/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java +++ b/core/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java @@ -4,6 +4,8 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; +import java.util.List; + import org.junit.Test; public class ResourceVisitorTest { @@ -16,13 +18,13 @@ public class ResourceVisitorTest { assertThat(rb0.uri(), equalTo("http://base")); final ResourceVisitor.Supplier ch1 = new ResourceVisitor.Supplier() { - public ResourceVisitor get() { return ch0.get().path("path1").param("p1", "v1"); } + public ResourceVisitor get() { return ch0.get().path("path1").param("p1", "v1", new TypeToken(String.class) {}); } }; MyCollectorResourceVisitor rb1 = (MyCollectorResourceVisitor) ch1.get(); assertThat(rb1.uri(), equalTo("http://base/path1?p1=v1")); ResourceVisitor.Supplier ch2 = new ResourceVisitor.Supplier() { - public ResourceVisitor get() { return ch1.get().path("path2").param("p2", asList("v2a", "v2b")); } + public ResourceVisitor get() { return ch1.get().path("path2").param("p2", asList("v2a", "v2b"), new TypeToken>(List.class, new TypeToken(String.class) {}) {}); } }; MyCollectorResourceVisitor rb2 = (MyCollectorResourceVisitor) ch2.get(); assertThat(rb2.uri(), equalTo("http://base/path1/path2?p1=v1&p2=v2a&p2=v2b")); @@ -30,6 +32,6 @@ public class ResourceVisitorTest { private static class MyCollectorResourceVisitor extends CollectorResourceVisitor { @Override protected String encodeComponent(String str) { return str; } - @Override public T as(Class container, Class type) { return null; } + @Override public T as(TypeToken typeToken) { return null; } } } diff --git a/core/src/test/java/com/intendia/gwt/autorest/client/TypeTokenTest.java b/core/src/test/java/com/intendia/gwt/autorest/client/TypeTokenTest.java new file mode 100644 index 0000000..b1f7a97 --- /dev/null +++ b/core/src/test/java/com/intendia/gwt/autorest/client/TypeTokenTest.java @@ -0,0 +1,68 @@ +package com.intendia.gwt.autorest.client; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +public class TypeTokenTest { + public static final java.util.List>[] TEST_FIELD = null; + + @Test + public void simpleTest() { + assertThat(TypeToken.of(String.class).stringify(), equalTo("java.lang.String")); + assertThat(new TypeToken(Integer.class) {}.stringify(), equalTo("java.lang.Integer")); + } + + @Test + public void arrayTest() { + assertThat(TypeToken.of(String[].class).stringify(), equalTo("java.lang.String[]")); + } + + @Test + public void genericTest() { + assertThat( + new TypeToken>>( + List.class, + new TypeToken>(Map.class, TypeToken.of(String.class), TypeToken.of(Integer[].class))) + .stringify(), + equalTo("java.util.List>")); + } + + @Test + public void genericTestArray() { + assertThat( + new TypeToken>[]>( + null, + new TypeToken>>( + List.class, + new TypeToken>(Map.class, TypeToken.of(String.class), TypeToken.of(Integer[].class)))) + .stringify(), + equalTo("java.util.List>[]")); + } + + @Test + public void genericJRETest() { + assertThat( + new TypeToken>>() {} + .stringify(), + equalTo("java.util.List>")); + + assertThat( + new TypeToken>>( + List.class, + new TypeToken>(Map.class, TypeToken.of(String.class), TypeToken.of(Integer[].class))), + equalTo(new TypeToken>>() {})); + + try { + assertThat( + TypeToken.of(TypeTokenTest.class.getDeclaredField("TEST_FIELD").getGenericType()).stringify(), + equalTo("java.util.List>[]")); + } catch (NoSuchFieldException | SecurityException e) { + throw new RuntimeException(e); + } + } +} diff --git a/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleEntryPoint.java b/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleEntryPoint.java index 347b2a2..9ee2a93 100644 --- a/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleEntryPoint.java +++ b/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleEntryPoint.java @@ -14,11 +14,13 @@ import com.google.web.bindery.event.shared.HandlerRegistration; import com.intendia.gwt.autorest.client.RequestResourceBuilder; import com.intendia.gwt.autorest.client.ResourceVisitor; +import com.intendia.gwt.autorest.client.TypeToken; import com.intendia.gwt.autorest.example.client.ExampleService.Greeting; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; import io.reactivex.functions.Consumer; + public class ExampleEntryPoint implements EntryPoint { private Consumer err = e -> GWT.log("exception: " + e, e); @@ -27,7 +29,7 @@ public void onModuleLoad() { HTML out = append(new HTML()); ResourceVisitor.Supplier getApi = () -> new RequestResourceBuilder().path(GWT.getModuleBaseURL(), "api"); - ExampleService srv = new ExampleService_RestServiceModel(() -> getApi.get().header("auth", "ok")); + ExampleService srv = new ExampleService_RestServiceModel(() -> getApi.get().header("auth", "ok", new TypeToken(String.class){})); Observable.merge(valueChange(name), keyUp(name)).map(e -> name.getValue()) .switchMap(q -> { diff --git a/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleService.java b/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleService.java index f0f9726..df0b682 100644 --- a/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleService.java +++ b/example/src/main/java/com/intendia/gwt/autorest/example/client/ExampleService.java @@ -2,6 +2,9 @@ import static jsinterop.annotations.JsPackage.GLOBAL; +import java.util.List; +import java.util.Map; + import com.intendia.gwt.autorest.client.AutoRestGwt; import io.reactivex.Completable; import io.reactivex.Maybe; @@ -33,7 +36,23 @@ public interface ExampleService { @PathParam("foo") String foo, @QueryParam("bar") String bar, @QueryParam("unk") String oth); - + + @GET @Path("observable/foo/{foo}") Greeting getFoo( + @PathParam("foo") String foo, + @QueryParam("bar") String bar, + @QueryParam("unk") Integer anInt, + @QueryParam("iii") int i, + @QueryParam("greeting") Greeting g); + + @GET @Path("observable/foo/{foo}") Greeting getFoo( + @PathParam("foo") String foo, + @QueryParam("unk") List oth, + @QueryParam("greeting") List> g); + + @POST @Path("observable/foo/{foo}") Greeting getFoo4( + @PathParam("foo") String foo, + List oth[]); + @JsType(namespace = GLOBAL, name = "Object", isNative = true) class Greeting { public String greeting; } diff --git a/gwt/src/main/java/com/intendia/gwt/autorest/client/RequestResourceBuilder.java b/gwt/src/main/java/com/intendia/gwt/autorest/client/RequestResourceBuilder.java index 1692da6..b43cdd1 100755 --- a/gwt/src/main/java/com/intendia/gwt/autorest/client/RequestResourceBuilder.java +++ b/gwt/src/main/java/com/intendia/gwt/autorest/client/RequestResourceBuilder.java @@ -7,8 +7,19 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +import javax.annotation.Nullable; + import com.intendia.gwt.autorest.client.RequestResponseException.FailedStatusCodeException; import com.intendia.gwt.autorest.client.RequestResponseException.ResponseFormatException; + import elemental2.core.Global; import elemental2.dom.FormData; import elemental2.dom.XMLHttpRequest; @@ -16,17 +27,8 @@ import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; -import javax.annotation.Nullable; import jsinterop.base.Js; -@SuppressWarnings("GwtInconsistentSerializableClass") public class RequestResourceBuilder extends CollectorResourceVisitor { public static final Function DEFAULT_REQUEST_FACTORY = data -> { XMLHttpRequest xhr = new XMLHttpRequest(); xhr.open(data.method(), data.uri()); return xhr; @@ -56,21 +58,22 @@ public RequestResourceBuilder requestTransformer( } @SuppressWarnings("unchecked") - @Override public T as(Class container, Class type) { - if (Completable.class.equals(container)) return (T) request().toCompletable(); - if (Maybe.class.equals(container)) return (T) request().flatMapMaybe(ctx -> { + @Override public T as(TypeToken typeToken) { + if (Completable.class.equals(typeToken.getRawType())) return (T) request().toCompletable(); + if (Maybe.class.equals(typeToken.getRawType())) return (T) request().flatMapMaybe(ctx -> { @Nullable Object decode = decode(ctx); return decode == null ? Maybe.empty() : Maybe.just(decode); }); - if (Single.class.equals(container)) return (T) request().map(ctx -> { + if (Single.class.equals(typeToken.getRawType())) return (T) request().map(ctx -> { @Nullable Object decode = decode(ctx); return requireNonNull(decode, "null response forbidden, use Maybe instead"); }); - if (Observable.class.equals(container)) return (T) request().toObservable().flatMapIterable(ctx -> { + if (Observable.class.equals(typeToken.getRawType())) return (T) request().toObservable().flatMapIterable(ctx -> { @Nullable Object[] decode = decode(ctx); return decode == null ? Collections.emptyList() : Arrays.asList(decode); }); - throw new UnsupportedOperationException("unsupported type " + container); + + throw new UnsupportedOperationException("unsupported type " + typeToken); } private @Nullable T decode(XMLHttpRequest ctx) { @@ -86,7 +89,7 @@ public Single request() { return Single.create(em -> { XMLHttpRequest xhr = requestFactory.apply(this); Map headers = new HashMap<>(); - for (Param h : headerParams) headers.put(h.k, Objects.toString(h.v)); + for (Param h : headerParams) headers.put(h.k, Objects.toString(h.v)); for (Map.Entry h : headers.entrySet()) xhr.setRequestHeader(h.getKey(), h.getValue()); try { diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/JreResourceBuilder.java b/jre/src/main/java/com/intendia/gwt/autorest/client/JreResourceBuilder.java index 2c58737..448a21b 100644 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/JreResourceBuilder.java +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/JreResourceBuilder.java @@ -1,10 +1,5 @@ package com.intendia.gwt.autorest.client; -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Single; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -17,6 +12,13 @@ import java.util.Objects; import java.util.stream.Stream; +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; + +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Single; + public class JreResourceBuilder extends CollectorResourceVisitor { private final ConnectionFactory factory; private final JsonCodec json; @@ -40,8 +42,8 @@ public JreResourceBuilder(String root, ConnectionFactory factory, JsonCodec code } catch (Exception e) { throw new RuntimeException(e); } } - @Override public T as(Class container, Class type) { - return json.fromJson(request(), container, type); + @Override public T as(TypeToken typeToken) { + return json.fromJson(request(), typeToken); } private Single request() { @@ -51,7 +53,7 @@ private Single request() { req = factory.apply(uri()); req.setRequestMethod(method); if (produces.length > 0) req.setRequestProperty("Accept", produces[0]); - for (Param e : headerParams) req.setRequestProperty(e.k, Objects.toString(e.v)); + for (Param e : headerParams) req.setRequestProperty(e.k, Objects.toString(e.v)); } catch (Exception e) { throw err("open connection error", e); } @@ -100,7 +102,7 @@ public interface ConnectionFactory { public interface JsonCodec { void toJson(Object src, Appendable writer); - C fromJson(Single json, Class container, Class type); + C fromJson(Single json, TypeToken typeToken); } public static class GsonCodec implements JsonCodec { @@ -111,16 +113,23 @@ public static class GsonCodec implements JsonCodec { } @SuppressWarnings("unchecked") - @Override public T fromJson(Single req, Class container, Class type) { - if (Completable.class.equals(container)) return (T) req.doOnSuccess(this::consume).toCompletable(); - if (Single.class.equals(container)) return (T) req.map(reader -> { - if (Reader.class.equals(type)) return reader; - if (String.class.equals(type)) return readAsString(reader); - return gson.fromJson(reader, type); + @Override public T fromJson(Single req, TypeToken typeToken) { + if (Completable.class.equals(typeToken.getRawType())) return (T) req.doOnSuccess(this::consume).toCompletable(); + if (Single.class.equals(typeToken.getRawType())) return (T) req.map(reader -> { + TypeToken itemTypeToken = typeToken.getTypeArguments()[0]; + + if (Reader.class.equals(itemTypeToken.getRawType())) return reader; + if (String.class.equals(itemTypeToken.getRawType())) return readAsString(reader); + return gson.fromJson(reader, itemTypeToken.getType()); }); - if (Observable.class.equals(container)) return (T) req.toObservable() - .flatMapIterable(n -> () -> new ParseArrayIterator<>(n, type)); - throw new IllegalArgumentException("unsupported type " + container); + if (Observable.class.equals(typeToken.getRawType())) { + TypeToken itemTypeToken = typeToken.getTypeArguments()[0]; + + return (T) req.toObservable() + .flatMapIterable(n -> () -> new ParseArrayIterator<>(n, itemTypeToken)); + } + + throw new IllegalArgumentException("unsupported type " + typeToken); } private static String readAsString(Reader in) { @@ -135,10 +144,10 @@ private static String readAsString(Reader in) { } private class ParseArrayIterator implements Iterator { - private final Class type; + private final TypeToken typeToken; private JsonReader reader; - public ParseArrayIterator(Reader reader, Class type) { - this.type = type; + public ParseArrayIterator(Reader reader, TypeToken typeToken) { + this.typeToken = typeToken; this.reader = new JsonReader(reader); try { this.reader.beginArray(); } catch (Exception e) { throw err("parsing error", e); } } @@ -150,7 +159,7 @@ public ParseArrayIterator(Reader reader, Class type) { @Override public T next() { if (!hasNext()) throw new NoSuchElementException(); try { - T next = gson.fromJson(reader, type); + T next = gson.fromJson(reader, typeToken.getType()); if (!reader.hasNext()) { reader.endArray(); reader.close(); reader = null; } return next; } catch (Exception e) { throw err("parsing error", e); } diff --git a/jre/src/test/java/com/intendia/gwt/autorest/client/JreResourceBuilderTest.java b/jre/src/test/java/com/intendia/gwt/autorest/client/JreResourceBuilderTest.java index 0b3292f..5951688 100644 --- a/jre/src/test/java/com/intendia/gwt/autorest/client/JreResourceBuilderTest.java +++ b/jre/src/test/java/com/intendia/gwt/autorest/client/JreResourceBuilderTest.java @@ -40,6 +40,13 @@ public class JreResourceBuilderTest { responseBody.write("[{\"bar\":\"expected1\"},{\"bar\":\"expected2\"}]".getBytes()); } }); + httpServer.createContext("/api/base", httpExchange -> { + httpExchange.sendResponseHeaders(200, 0); + try (OutputStream responseBody = httpExchange.getResponseBody()) { + responseBody.write("{\"bar\":\"expected base\"}".getBytes()); + } + }); + httpServer.start(); } @AfterClass public static void closeServer() { @@ -58,6 +65,10 @@ public class JreResourceBuilderTest { assertNull(rest.zero().blockingGet()); } + @Test public void baseOne() { + assertEquals("expected base", rest.baseOne().blockingGet().bar); + } + @Test public void one() { assertEquals("expected", rest.one().blockingGet().bar); } @@ -66,8 +77,12 @@ public class JreResourceBuilderTest { assertEquals(2, rest.many().toList().blockingGet().size()); } + public interface baseInterface { + @GET @Path("base") Single baseOne(); + } + @AutoRestGwt @Path("api") - public interface TestRestService { + public interface TestRestService extends baseInterface { @GET @Path("zero") Completable zero(); @GET @Path("one") Single one(); @GET @Path("many") Observable many(); diff --git a/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java new file mode 100644 index 0000000..93c4350 --- /dev/null +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java @@ -0,0 +1,52 @@ +package com.intendia.gwt.autorest.processor; + +import static com.google.auto.common.MoreTypes.asElement; + +import java.lang.annotation.Annotation; +import java.util.Objects; + +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; + + +/** + * A minimal abstraction of an annotated element (a class, method or method parameter). + * + * {@link AnnotationProcessor} works with this abstraction only, which allows it to target the + * javax.lang.model-based elements + */ +public class AnnotatedElement { + private String simpleName; + private Element jlmElement; + private TypeMirror jlmType; + + public AnnotatedElement(String simpleName, Element jlmElement, TypeMirror jlmType) { + this.simpleName = simpleName; + this.jlmElement = jlmElement; + this.jlmType = jlmType; + } + + public Element getJlmElement() { + return jlmElement; + } + + public TypeMirror getJlmType() { + return jlmType; + } + + public String getSimpleName() { + return simpleName; + } + + public T getAnnotation(Class annotationClass) { + return jlmElement.getAnnotation(annotationClass); + } + + public T getAnnotationOverAnnotations(Class annotationClass) { + return jlmElement.getAnnotationMirrors().stream() + .map(a -> asElement(a.getAnnotationType()).getAnnotation(annotationClass)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } +} diff --git a/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java new file mode 100644 index 0000000..0543e0b --- /dev/null +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java @@ -0,0 +1,173 @@ +package com.intendia.gwt.autorest.processor; + +import static java.util.Optional.ofNullable; +import static javax.ws.rs.HttpMethod.GET; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.ws.rs.Consumes; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +/** + * A utility class capable of processing a type annotated with JAX-RS annotations and extracting the REST paths, + * HTTP method type, as well as data, query, header and form parameters' information. + */ +public class AnnotationProcessor { + public static class ParamInfo { + private String name; + private AnnotatedElement annotatedElement; + + private int javaArgumentIndex; + private String javaArgumentName; + + public ParamInfo(String name, AnnotatedElement annotatedElement, int javaArgumentIndex, String javaArgumentName) { + this.name = name; + this.annotatedElement = annotatedElement; + this.javaArgumentIndex = javaArgumentIndex; + this.javaArgumentName = javaArgumentName; + } + + public String getName() { + return name; + } + + public AnnotatedElement getAnnotatedElement() { + return annotatedElement; + } + + public int getJavaArgumentIndex() { + return javaArgumentIndex; + } + + public String getJavaArgumentName() { + return javaArgumentName; + } + } + + private AnnotatedElement annotatedElement; + + public AnnotationProcessor(AnnotatedElement annotatedElement) { + this.annotatedElement = annotatedElement; + } + + public String getHttpMethod() { + return ofNullable(annotatedElement.getAnnotationOverAnnotations(HttpMethod.class)) + .map(a -> ((HttpMethod)a).value()) + .orElse(GET); + } + + public Stream getPaths(Stream> parameters) { + List> paramList = parameters.collect(Collectors.toList()); + + return Arrays.stream( + ofNullable(annotatedElement.getAnnotation(Path.class)) + .map(Path::value) + .orElse("") + .split("/")) + .filter(s -> !s.isEmpty()) + .map(path -> !path.startsWith("{")? + (Object)path: + paramList.stream() + .filter(entry -> ofNullable(entry.getValue().getAnnotation(PathParam.class)) + .map(PathParam::value) + .map(v -> path.equals("{" + v + "}")) + .orElse(false)) + .findFirst() + .map(entry -> { + int javaArgumentIndex = entry.getKey(); + AnnotatedElement annotatedElement = entry.getValue(); + + return new ParamInfo( + null, + annotatedElement, + javaArgumentIndex, + annotatedElement.getSimpleName()); + }) + .orElseThrow(() -> new IllegalArgumentException("Unknown path parameter: " + path))); + } + + public Stream getProduces(String... produces) { + return Arrays.stream( + ofNullable(annotatedElement.getAnnotation(Produces.class)) + .map(Produces::value) + .orElse(produces)); + } + + public Stream getConsumes(String... consumes) { + return Arrays.stream( + ofNullable(annotatedElement.getAnnotation(Consumes.class)) + .map(Consumes::value) + .orElse(consumes)); + } + + public Stream getQueryParams(Stream> parameters) { + return getParams(QueryParam.class, QueryParam::value, parameters); + } + + public Stream getHeaderParams(Stream> parameters) { + return getParams(HeaderParam.class, HeaderParam::value, parameters); + } + + public Stream getFormParams(Stream> parameters) { + return getParams(FormParam.class, FormParam::value, parameters); + } + + public Optional getData(Stream> parameters) { + return parameters + .filter(entry -> !isParam(entry.getValue())) + .map(entry -> { + int javaArgumentIndex = entry.getKey(); + AnnotatedElement annotatedElement = entry.getValue(); + + return new ParamInfo( + null, + annotatedElement, + javaArgumentIndex, + annotatedElement.getSimpleName()); + }) + .findFirst(); + } + + private Stream getParams( + Class paramAnnotationClass, + Function paramNameAnnotationExtractor, + Stream> parameters) { + return parameters + .filter(entry -> entry.getValue().getAnnotation(paramAnnotationClass) != null) + .map(entry -> { + int javaArgumentIndex = entry.getKey(); + AnnotatedElement annotatedElement = entry.getValue(); + String nameFromAnnotation = paramNameAnnotationExtractor.apply(entry.getValue().getAnnotation(paramAnnotationClass)); + + return new ParamInfo( + nameFromAnnotation != null? nameFromAnnotation: annotatedElement.getSimpleName(), + annotatedElement, + javaArgumentIndex, + annotatedElement.getSimpleName()); + }); + } + + private boolean isParam(AnnotatedElement annotatedElement) { + return annotatedElement.getAnnotation(CookieParam.class) != null + || annotatedElement.getAnnotation(FormParam.class) != null + || annotatedElement.getAnnotation(HeaderParam.class) != null + || annotatedElement.getAnnotation(MatrixParam.class) != null + || annotatedElement.getAnnotation(PathParam.class) != null + || annotatedElement.getAnnotation(QueryParam.class) != null; + } +} diff --git a/processor/src/main/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessor.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessor.java index c74ac8d..8f43db0 100644 --- a/processor/src/main/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessor.java +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessor.java @@ -1,12 +1,11 @@ package com.intendia.gwt.autorest.processor; -import static com.google.auto.common.MoreTypes.asElement; import static java.util.Collections.singleton; -import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toSet; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; +import static javax.lang.model.element.Modifier.NATIVE; import static javax.ws.rs.HttpMethod.DELETE; import static javax.ws.rs.HttpMethod.GET; import static javax.ws.rs.HttpMethod.HEAD; @@ -14,28 +13,24 @@ import static javax.ws.rs.HttpMethod.POST; import static javax.ws.rs.HttpMethod.PUT; -import com.google.auto.common.MoreTypes; -import com.google.common.base.Throwables; -import com.intendia.gwt.autorest.client.AutoRestGwt; -import com.intendia.gwt.autorest.client.ResourceVisitor; -import com.intendia.gwt.autorest.client.RestServiceModel; -import com.squareup.javapoet.AnnotationSpec; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.CodeBlock; -import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.TypeSpec; -import java.util.Arrays; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; + +import javax.annotation.Generated; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.inject.Inject; import javax.lang.model.SourceVersion; @@ -45,29 +40,59 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; import javax.tools.Diagnostic.Kind; -import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; +import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; +import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.HttpMethod; import javax.ws.rs.MatrixParam; -import javax.ws.rs.Path; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import com.google.common.base.Throwables; +import com.intendia.gwt.autorest.client.AutoRestGwt; +import com.intendia.gwt.autorest.client.ResourceVisitor; +import com.intendia.gwt.autorest.client.RestServiceModel; +import com.intendia.gwt.autorest.client.TypeToken; +import com.intendia.gwt.autorest.processor.AnnotationProcessor.ParamInfo; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; + public class AutoRestGwtProcessor extends AbstractProcessor { private static final Set HTTP_METHODS = Stream.of(GET, POST, PUT, DELETE, HEAD, OPTIONS).collect(toSet()); - private static final String[] EMPTY = {}; private static final String AutoRestGwt = AutoRestGwt.class.getCanonicalName(); - + private Types typeUtils; + private Elements elementUtils; + @Override public Set getSupportedOptions() { return singleton("debug"); } @Override public Set getSupportedAnnotationTypes() { return singleton(AutoRestGwt); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } + @Override public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + typeUtils = processingEnv.getTypeUtils(); + elementUtils = processingEnv.getElementUtils(); + + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { if (roundEnv.processingOver()) return false; roundEnv.getElementsAnnotatedWith(AutoRestGwt.class).stream() @@ -85,9 +110,13 @@ public class AutoRestGwtProcessor extends AbstractProcessor { } private void processRestService(TypeElement restService) throws Exception { - String rsPath = restService.getAnnotation(Path.class).value(); - String[] produces = ofNullable(restService.getAnnotation(Produces.class)).map(Produces::value).orElse(EMPTY); - String[] consumes = ofNullable(restService.getAnnotation(Consumes.class)).map(Consumes::value).orElse(EMPTY); + AnnotatedElement restServiceAnnotatedElement = new AnnotatedElement(restService.getSimpleName().toString(), restService, restService.asType()); + + AnnotationProcessor restServiceProcessor = new AnnotationProcessor(restServiceAnnotatedElement); + + String[] rsPath = restServiceProcessor.getPaths(Stream.empty()).toArray(String[]::new); + String[] produces = restServiceProcessor.getProduces().toArray(String[]::new); + String[] consumes = restServiceProcessor.getConsumes().toArray(String[]::new); ClassName rsName = ClassName.get(restService); log("rest service interface: " + rsName); @@ -96,82 +125,84 @@ private void processRestService(TypeElement restService) throws Exception { log("rest service model: " + modelName); TypeSpec.Builder modelTypeBuilder = TypeSpec.classBuilder(modelName.simpleName()) + .addJavadoc(CodeBlock.of("This is generated class, please don't modify\n")) + .addAnnotation(AnnotationSpec.builder(Generated.class) + .addMember("value", "\"" + getClass().getCanonicalName() + "\"") + .build()) .addOriginatingElement(restService) .addModifiers(Modifier.PUBLIC) .superclass(RestServiceModel.class) .addSuperinterface(TypeName.get(restService.asType())); - + modelTypeBuilder.addMethod(MethodSpec.constructorBuilder() .addAnnotation(Inject.class) .addModifiers(PUBLIC) .addParameter(TypeName.get(ResourceVisitor.Supplier.class), "parent", FINAL) .addStatement("super(new $T() { public $T get() { return $L.get().path($S); } })", - ResourceVisitor.Supplier.class, ResourceVisitor.class, "parent", rsPath) + ResourceVisitor.Supplier.class, ResourceVisitor.class, "parent", rsPath.length > 0? rsPath[0]: "") .build()); - List methods = restService.getEnclosedElements().stream() - .filter(e -> e.getKind() == ElementKind.METHOD && e instanceof ExecutableElement) - .map(e -> (ExecutableElement) e) - .filter(method -> !(method.getModifiers().contains(STATIC) || method.isDefault())) - .collect(Collectors.toList()); - + Map methods = getAllMethods(restService); + Set methodImports = new HashSet<>(); - for (ExecutableElement method : methods) { - String methodName = method.getSimpleName().toString(); - - Optional incompatible = isIncompatible(method); + for (Map.Entry method: methods.entrySet()) { + + AnnotatedElement annotatedElement = new AnnotatedElement(method.getKey().getSimpleName().toString(), method.getKey(), method.getValue()); + AnnotationProcessor processor = new AnnotationProcessor(annotatedElement); + + Optional incompatible = isIncompatible(method.getKey()); if (incompatible.isPresent()) { - modelTypeBuilder.addMethod(MethodSpec.overriding(method) + modelTypeBuilder.addMethod(MethodSpec.overriding(method.getKey()) .addAnnotation(AnnotationSpec.get(incompatible.get())) - .addStatement("throw new $T(\"$L\")", UnsupportedOperationException.class, methodName) + .addStatement("throw new $T(\"$L\")", UnsupportedOperationException.class, annotatedElement.getSimpleName()) .build()); continue; } CodeBlock.Builder builder = CodeBlock.builder().add("$[return "); { + List parameters = method.getKey().getParameters(); + List parameterTypes = method.getValue().getParameterTypes(); + + Supplier>> parametersFactory = () -> + IntStream + .range(0, parameters.size()) + .mapToObj(index -> new SimpleEntry<>( + index, + new AnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.get(index), parameterTypes.get(index)))); + // method type - builder.add("method($L)", methodImport(methodImports, method.getAnnotationMirrors().stream() - .map(a -> asElement(a.getAnnotationType()).getAnnotation(HttpMethod.class)) - .filter(Objects::nonNull).map(HttpMethod::value).findFirst().orElse(GET))); + builder.add("method($L)", methodImport(methodImports, processor.getHttpMethod())); + // resolve paths - builder.add(".path($L)", Arrays - .stream(ofNullable(method.getAnnotation(Path.class)).map(Path::value).orElse("").split("/")) - .filter(s -> !s.isEmpty()).map(path -> !path.startsWith("{") ? "\"" + path + "\"" : method - .getParameters().stream() - .filter(a -> ofNullable(a.getAnnotation(PathParam.class)).map(PathParam::value) - .map(v -> path.equals("{" + v + "}")).orElse(false)) - .findFirst().map(VariableElement::getSimpleName).map(Object::toString) - // next comment will produce a compilation error so the user get notified - .orElse("/* path param " + path + " does not match any argument! */")) - .collect(Collectors.joining(", "))); + addObjectOrParamLiterals(builder, "path", processor.getPaths(parametersFactory.get())); + // produces - builder.add(".produces($L)", Arrays - .stream(ofNullable(method.getAnnotation(Produces.class)).map(Produces::value).orElse(produces)) - .map(str -> "\"" + str + "\"").collect(Collectors.joining(", "))); + addStringLiterals(builder, "produces", processor.getProduces(produces)); + // consumes - builder.add(".consumes($L)", Arrays - .stream(ofNullable(method.getAnnotation(Consumes.class)).map(Consumes::value).orElse(consumes)) - .map(str -> "\"" + str + "\"").collect(Collectors.joining(", "))); + addStringLiterals(builder, "consumes", processor.getConsumes(consumes)); + // query params - method.getParameters().stream().filter(p -> p.getAnnotation(QueryParam.class) != null).forEach(p -> - builder.add(".param($S, $L)", p.getAnnotation(QueryParam.class).value(), p.getSimpleName())); + addParamLiterals(builder, "param", processor.getQueryParams(parametersFactory.get())); + // header params - method.getParameters().stream().filter(p -> p.getAnnotation(HeaderParam.class) != null).forEach(p -> - builder.add(".header($S, $L)", p.getAnnotation(HeaderParam.class).value(), p.getSimpleName())); + addParamLiterals(builder, "header", processor.getHeaderParams(parametersFactory.get())); + // form params - method.getParameters().stream().filter(p -> p.getAnnotation(FormParam.class) != null).forEach(p -> - builder.add(".form($S, $L)", p.getAnnotation(FormParam.class).value(), p.getSimpleName())); + addParamLiterals(builder, "form", processor.getFormParams(parametersFactory.get())); + // data - method.getParameters().stream().filter(p -> !isParam(p)).findFirst() - .ifPresent(data -> builder.add(".data($L)", data.getSimpleName())); + processor.getData(parametersFactory.get()) + .ifPresent(dataInfo -> builder.add( + ".data($L, $L)", + dataInfo.getJavaArgumentName(), + asTypeTokenLiteral(dataInfo.getAnnotatedElement()))); } - builder.add(".as($T.class, $T.class);\n$]", - processingEnv.getTypeUtils().erasure(method.getReturnType()), - MoreTypes.asDeclared(method.getReturnType()).getTypeArguments().stream().findFirst() - .map(TypeName::get).orElse(TypeName.get(Void.class))); + + builder.add(".as($L);\n$]", asTypeTokenLiteral(annotatedElement)); - modelTypeBuilder.addMethod(MethodSpec.overriding(method).addCode(builder.build()).build()); + modelTypeBuilder.addMethod(MethodSpec.overriding(method.getKey(), (DeclaredType)restService.asType(), typeUtils).addCode(builder.build()).build()); } Filer filer = processingEnv.getFiler(); @@ -180,7 +211,119 @@ private void processRestService(TypeElement restService) throws Exception { boolean skipJavaLangImports = processingEnv.getOptions().containsKey("skipJavaLangImports"); file.skipJavaLangImports(skipJavaLangImports).build().writeTo(filer); } + + private Map getAllMethods(TypeElement restService) { + return + elementUtils.getAllMembers(restService).stream() + .filter(e -> e.getKind() == ElementKind.METHOD && e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(method -> !( + method.getModifiers().contains(STATIC) + || method.getModifiers().contains(FINAL) + || method.getModifiers().contains(NATIVE) + || method.isDefault())) + .filter(method -> ( + isIncompatible(method).isPresent() + || method.getAnnotation(GET.class) != null + || method.getAnnotation(PUT.class) != null + || method.getAnnotation(POST.class) != null + || method.getAnnotation(DELETE.class) != null)) + .collect(Collectors.toMap(m -> m, m -> ((ExecutableType)typeUtils.asMemberOf((DeclaredType)restService.asType() , m)))); + } + + private CodeBlock asTypeTokenLiteral(AnnotatedElement annotatedElement) { + CodeBlock.Builder builder = CodeBlock.builder(); + + AnnotatedElement jlmAnnotatedElement = (AnnotatedElement)annotatedElement; + + if (jlmAnnotatedElement.getJlmType() instanceof ExecutableType) + addTypeTokenLiteral(builder, TypeName.get(((ExecutableType)jlmAnnotatedElement.getJlmType()).getReturnType())); + else + addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmType())); + + return builder.build(); + } + + private void addTypeTokenLiteral(CodeBlock.Builder builder, TypeName name) { + builder.add("new $T<$L>(", TypeToken.class, name.isPrimitive()? name.box(): name); + TypeName rawType; + List typeArguments; + + if (name instanceof ParameterizedTypeName) { + ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName)name; + rawType = parameterizedTypeName.rawType; + typeArguments = parameterizedTypeName.typeArguments; + } else if (name instanceof ArrayTypeName) { + ArrayTypeName arrayTypeName = (ArrayTypeName)name; + + rawType = null; + typeArguments = Collections.singletonList(arrayTypeName.componentType); + } else if (name instanceof ClassName || name instanceof TypeName) { + rawType = name.isPrimitive()? name.box(): name; + typeArguments = Collections.emptyList(); + } else + throw new IllegalArgumentException("Unsupported type " + name); + + if(rawType == null) + builder.add("null"); + else + builder.add("$T.class", rawType); + + for (TypeName typeArgumentName: typeArguments) { + builder.add(", "); + addTypeTokenLiteral(builder, typeArgumentName); + } + + builder.add(") {}"); + } + + private void addParamLiterals(CodeBlock.Builder builder, String name, Stream params) { + params + .forEach(paramInfo -> builder.add( + ".$L($S, $L, $L)", + name, + paramInfo.getName(), + paramInfo.getJavaArgumentName(), + asTypeTokenLiteral(paramInfo.getAnnotatedElement()))); + } + + private void addObjectOrParamLiterals(CodeBlock.Builder builder, String name, Stream items) { + items + .forEach(item -> { + if (item instanceof ParamInfo) { + ParamInfo paramInfo = (ParamInfo)item; + builder.add( + ".$L($L, $L)", + name, + paramInfo.getJavaArgumentName(), + asTypeTokenLiteral(paramInfo.getAnnotatedElement())); + } else { + builder.add(".$L($S)", name, item.toString()); + } + }); + + } + + private static void addStringLiterals(CodeBlock.Builder builder, String name, Stream strings) { + builder.add(".$L(", name); + addCommaSeparated(builder, strings, (cb, path) -> cb.add("$S", path)); + builder.add(")"); + } + + private static void addCommaSeparated(CodeBlock.Builder builder, Stream items, BiConsumer consumer) { + boolean[] first = {true}; + + items.forEach(item -> { + if (!first[0]) + builder.add("$L", ", "); + + consumer.accept(builder, item); + + first[0] = false; + }); + } + private String methodImport(Set methodImports, String method) { if (HTTP_METHODS.contains(method)) { methodImports.add(method); return method; diff --git a/processor/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java b/processor/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java index 87fe36e..e86036c 100644 --- a/processor/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java +++ b/processor/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java @@ -11,6 +11,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -21,26 +23,57 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.InOrder; +import org.mockito.Matchers; +import org.mockito.internal.matchers.Any; public class ResourceVisitorTest { + @Test public void interface_inheritance() throws Exception { + ResourceVisitor visitor = mock(ResourceVisitor.class, RETURNS_SELF); + when(visitor.as(TypeToken.of(Integer.class))).thenReturn(0); + TestService service = new TestService_RestServiceModel(() -> visitor); + service.baseMethod(); + InOrder inOrder = inOrder(visitor); + inOrder.verify(visitor).path("base"); + inOrder.verify(visitor).produces("application/json"); + inOrder.verify(visitor).consumes("application/json"); + inOrder.verify(visitor).as(TypeToken.of(Integer.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test public void interface_inheritance_more() throws Exception { + ResourceVisitor visitor = mock(ResourceVisitor.class, RETURNS_SELF); + when(visitor.as(TypeToken.of(Integer.class))).thenReturn(0); + TestService service = new TestService_RestServiceModel(() -> visitor); + service.childMethod("Test string"); + InOrder inOrder = inOrder(visitor); + inOrder.verify(visitor).path("child"); + inOrder.verify(visitor).produces("application/json"); + inOrder.verify(visitor).consumes("application/json"); + inOrder.verify(visitor).param("str_param", "Test string", TypeToken.of(String.class)); + inOrder.verify(visitor).as(TypeToken.of(Integer.class)); + inOrder.verifyNoMoreInteractions(); + } + @Test public void visitor_works() throws Exception { ResourceVisitor visitor = mock(ResourceVisitor.class, RETURNS_SELF); - when(visitor.as(List.class, String.class)).thenReturn(singletonList("done")); + when(visitor.as(new TypeToken>(List.class, TypeToken.of(String.class)) {})).thenReturn(singletonList("done")); TestService service = new TestService_RestServiceModel(() -> visitor); - service.method("s", 1, "s", 1, asList(1, 2, 3), "s", 1); + service.method("s", 1, "s", 1, asList(1, 2 ,3), new Integer[]{ 1, 2, 3}, "s", 1); InOrder inOrder = inOrder(visitor); inOrder.verify(visitor).path("a"); - inOrder.verify(visitor).path("b", "s", 1, "c"); + inOrder.verify(visitor).path("b"); inOrder.verify(visitor).produces("application/json"); inOrder.verify(visitor).consumes("application/json"); - inOrder.verify(visitor).param("qS", "s"); - inOrder.verify(visitor).param("qI", 1); - inOrder.verify(visitor).param("qIs", asList(1, 2, 3)); - inOrder.verify(visitor).header("hS", "s"); - inOrder.verify(visitor).header("hI", 1); - inOrder.verify(visitor).as(List.class, String.class); + inOrder.verify(visitor).param("qS", "s", TypeToken.of(String.class)); + inOrder.verify(visitor).param("qI", 1, TypeToken.of(Integer.class)); + inOrder.verify(visitor).param("qIs", asList(1, 2, 3), new TypeToken>(List.class, TypeToken.of(Integer.class)) {}); + inOrder.verify(visitor).param("qIa", new Integer[] {1, 2, 3}, TypeToken.of(Integer[].class)); + inOrder.verify(visitor).header("hS", "s", TypeToken.of(String.class)); + inOrder.verify(visitor).header("hI", 1, TypeToken.of(Integer.class)); + inOrder.verify(visitor).as(new TypeToken>(List.class, TypeToken.of(String.class)) {}); inOrder.verifyNoMoreInteractions(); } @@ -49,12 +82,23 @@ public class ResourceVisitorTest { service.gwtIncompatible(); } + + public interface BaseInterface { + @Produces("application/json") @Consumes("application/json") + @GET @Path("base") T baseMethod(); + } + + public interface ChildInterface extends BaseInterface{ + @Produces("application/json") @Consumes("application/json") + @GET @Path("child") T childMethod(@QueryParam("str_param") P param); + } + @AutoRestGwt @Path("a") @Produces("*/*") @Consumes("*/*") - public interface TestService { + public interface TestService extends ChildInterface{ @Produces("application/json") @Consumes("application/json") @GET @Path("b/{pS}/{pI}/c") List method( @PathParam("pS") String pS, @PathParam("pI") int pI, - @QueryParam("qS") String qS, @QueryParam("qI") int qI, @QueryParam("qIs") List qIs, + @QueryParam("qS") String qS, @QueryParam("qI") int qI, @QueryParam("qIs") List qIs, @QueryParam("qIa") Integer[] qIa, @HeaderParam("hS") String hS, @HeaderParam("hI") int hI); @GwtIncompatible Response gwtIncompatible(); diff --git a/processor/src/test/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessorTest.java b/processor/src/test/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessorTest.java index 6ebbde4..2ce40de 100644 --- a/processor/src/test/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessorTest.java +++ b/processor/src/test/java/com/intendia/gwt/autorest/processor/AutoRestGwtProcessorTest.java @@ -25,10 +25,17 @@ public class AutoRestGwtProcessorTest { + "\n" + "import com.intendia.gwt.autorest.client.ResourceVisitor;\n" + "import com.intendia.gwt.autorest.client.RestServiceModel;\n" + + "import com.intendia.gwt.autorest.client.TypeToken;\n" + "import java.util.Optional;\n" + + "import javax.annotation.Generated;\n" + "import javax.inject.Inject;\n" + "\n" + + "/**\n" + + " * This is generated class, please don't modify\n" + + " */\n" + + "@Generated(\"com.intendia.gwt.autorest.processor.AutoRestGwtProcessor\")\n" + "public class Rest_RestServiceModel extends RestServiceModel implements Rest {\n" + + "\n" + " @Inject\n" + " public Rest_RestServiceModel(final ResourceVisitor.Supplier parent) {\n" + " super(new ResourceVisitor.Supplier() {\n" @@ -38,7 +45,7 @@ public class AutoRestGwtProcessorTest { + "\n" + " @Override" + " public Optional getStr() {\n" - + " return method(GET).path().produces().consumes().as(Optional.class, String.class);\n" + + " return method(GET).produces().consumes().as(new TypeToken>(Optional.class, new TypeToken(String.class){}){});\n" + " }\n" + "}")); }