From b9182dbcd23f1aa2e76c117a35e296a14e6fc84c Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Tue, 13 Nov 2018 14:22:56 +0200 Subject: [PATCH 1/8] Extend ResourceVisitor to pass type information associated to each parameter and return result --- .../client/CollectorResourceVisitor.java | 23 ++-- .../gwt/autorest/client/ResourceVisitor.java | 22 ++-- .../intendia/gwt/autorest/client/Type.java | 118 ++++++++++++++++++ .../autorest/client/ResourceVisitorTest.java | 8 +- .../example/client/ExampleEntryPoint.java | 3 +- .../example/client/ExampleService.java | 21 +++- .../client/RequestResourceBuilder.java | 12 +- .../autorest/client/JreResourceBuilder.java | 11 +- .../processor/AutoRestGwtProcessor.java | 104 +++++++++++++-- .../autorest/client/ResourceVisitorTest.java | 24 ++-- .../processor/AutoRestGwtProcessorTest.java | 7 +- 11 files changed, 298 insertions(+), 55 deletions(-) create mode 100644 core/src/main/java/com/intendia/gwt/autorest/client/Type.java 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..b43211e 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 @@ -19,7 +19,10 @@ public abstract class CollectorResourceVisitor implements ResourceVisitor { 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 final Type t; + public Param(String k, Object v) { this(k, v, Type.undefined()); } + public Param(String k, Object v, Type t) { this.k = k; this.v = v; this.t = t;} + public static List expand(List in) { List out = new ArrayList<>(); for (Param p : in) { @@ -28,7 +31,7 @@ public static List expand(List in) { } 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<>(); @@ -39,6 +42,7 @@ public static List expand(List in) { protected String produces[] = { "application/json" }; protected String consumes[] = { "application/json" }; protected Object data = null; + protected Type dataType; private List expectedStatuses = DEFAULT_EXPECTED_STATUS; @Override public ResourceVisitor method(String method) { @@ -69,26 +73,27 @@ public ResourceVisitor path(String path) { return this; } - @Override public ResourceVisitor param(String key, @Nullable Object value) { + @Override public ResourceVisitor param(String key, @Nullable Object value, Type type) { 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, type)); return this; } - @Override public ResourceVisitor header(String key, @Nullable Object value) { + @Override public ResourceVisitor header(String key, @Nullable Object value, Type type) { 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, type)); return this; } - @Override public ResourceVisitor form(String key, @Nullable Object value) { + @Override public ResourceVisitor form(String key, @Nullable Object value, Type type) { 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, type)); return this; } - @Override public ResourceVisitor data(Object data) { + @Override public ResourceVisitor data(Object data, Type type) { this.data = data; + this.dataType = type; return this; } 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..d81377e 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 @@ -17,20 +17,20 @@ public interface ResourceVisitor { /** 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 Object value, Type type); - /** Sets a header param. */ - ResourceVisitor header(String key, @Nullable Object value); + /** Sets a header param with its type. */ + ResourceVisitor header(String key, @Nullable Object value, Type type); - /** Sets a from param. */ - ResourceVisitor form(String key, @Nullable Object value); + /** Sets a from param with its type. */ + ResourceVisitor form(String key, @Nullable Object value, Type type); + + /** Sets the content data with its type. */ + ResourceVisitor data(Object data, Type typeInfo); - /** 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(Type type); interface Supplier { ResourceVisitor get(); diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/Type.java b/core/src/main/java/com/intendia/gwt/autorest/client/Type.java new file mode 100644 index 0000000..571917f --- /dev/null +++ b/core/src/main/java/com/intendia/gwt/autorest/client/Type.java @@ -0,0 +1,118 @@ +package com.intendia.gwt.autorest.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Recursive structure to model Java type meta-data + * associated with corresponding parameter or method + * return result. + */ +public class Type { + private final boolean array; + private final List typeParameters; + private final Class clazz; + + private Type(Class clazz) { + this(clazz, Collections.emptyList(), false); + } + + private Type(Class clazz, List typeParameters, boolean isArray) { + this.clazz = clazz; + this.typeParameters = typeParameters; + this.array = isArray; + } + + public boolean isArray() { + return array; + } + + public boolean isGeneric() { + return !typeParameters.isEmpty(); + } + + public boolean isDefined() { + return clazz == null; + } + + public static Type of(Class clazz) { + return new Type(clazz); + } + + public static Type array(Type type) { + return new Type(type.clazz, type.typeParameters, true); + } + + public static Type undefined() { + return new Type(null); + } + + public Type typeParam(Type type) { + List typeParams = new ArrayList<>(this.typeParameters); + typeParams.add(type); + return new Type(this.clazz, typeParams, type.array); + } + + public Class getClazz() { + return clazz; + } + + public List getTypeParams() { + return Collections.unmodifiableList(typeParameters); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(clazz.getSimpleName()); + + if (!typeParameters.isEmpty()) { + result.append("<"); + for(Type t: typeParameters) { + result.append(t.toString()); + result.append(", "); + } + result.delete(result.length()-2, result.length()); + result.append(">"); + } + + return result.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (array ? 1231 : 1237); + result = prime * result + ((clazz == null) ? 0 : clazz.hashCode()); + result = prime * result + ((typeParameters == null) ? 0 : typeParameters.hashCode()); + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) + return true; + + if(!(other instanceof Type)) + return false; + + Type otype = (Type)other; + if (array != otype.array) + return false; + + if (clazz == null) { + if (otype.clazz != null) + return false; + } else { + if (!clazz.equals(otype.clazz)) + return false; + } + + if (!typeParameters.equals(otype.typeParameters)) + return false; + + return true; + } +} 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..000eb4d 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.awt.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", Type.of(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"), Type.of(List.class).typeParam(Type.of(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(Type type) { return null; } } } 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..0c60aea 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,6 +14,7 @@ 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.Type; import com.intendia.gwt.autorest.example.client.ExampleService.Greeting; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; @@ -27,7 +28,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", Type.undefined())); 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..c26e09e 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 @@ -56,21 +56,21 @@ 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(Type type) { + if (Completable.class.equals(type.getClazz())) return (T) request().toCompletable(); + if (Maybe.class.equals(type.getClazz())) 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(type.getClazz())) 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(type.getClazz())) 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 " + type.getClazz()); } private @Nullable T decode(XMLHttpRequest ctx) { 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..bb206e1 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 @@ -40,8 +40,11 @@ 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(Type type) { + return json.fromJson( + request(), + type.getClazz(), + !type.getTypeParams().isEmpty()? type.getTypeParams().get(0).getClazz(): Void.class); } private Single request() { @@ -100,7 +103,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, Class container, Class type); } public static class GsonCodec implements JsonCodec { @@ -111,7 +114,7 @@ public static class GsonCodec implements JsonCodec { } @SuppressWarnings("unchecked") - @Override public T fromJson(Single req, Class container, Class type) { + @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; 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..88bafa1 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 @@ -14,11 +14,11 @@ 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.intendia.gwt.autorest.client.Type; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; @@ -40,11 +40,20 @@ import javax.inject.Inject; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.util.SimpleTypeVisitor6; import javax.tools.Diagnostic.Kind; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; @@ -100,7 +109,10 @@ private void processRestService(TypeElement restService) throws Exception { .addModifiers(Modifier.PUBLIC) .superclass(RestServiceModel.class) .addSuperinterface(TypeName.get(restService.asType())); - + + //Not a brilliant way to get import for Type (kind of deficiency in javapoet) + modelTypeBuilder.addStaticBlock(CodeBlock.builder().addStatement("$T dummy = null", Type.class).build()); + modelTypeBuilder.addMethod(MethodSpec.constructorBuilder() .addAnnotation(Inject.class) .addModifiers(PUBLIC) @@ -155,21 +167,18 @@ private void processRestService(TypeElement restService) throws Exception { .map(str -> "\"" + str + "\"").collect(Collectors.joining(", "))); // 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())); + builder.add(".param($S, $L, $L)", p.getAnnotation(QueryParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); // 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())); + builder.add(".header($S, $L, $L)", p.getAnnotation(HeaderParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); // 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())); + builder.add(".form($S, $L, $L)", p.getAnnotation(FormParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); // data method.getParameters().stream().filter(p -> !isParam(p)).findFirst() - .ifPresent(data -> builder.add(".data($L)", data.getSimpleName())); + .ifPresent(data -> builder.add(".data($L, $L)", data.getSimpleName(), createTypeInfo(data.asType()))); } - 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$]",createTypeInfo(method.getReturnType())); modelTypeBuilder.addMethod(MethodSpec.overriding(method).addCode(builder.build()).build()); } @@ -181,6 +190,81 @@ private void processRestService(TypeElement restService) throws Exception { file.skipJavaLangImports(skipJavaLangImports).build().writeTo(filer); } + private String createTypeInfo(TypeMirror type) { + StringBuilder result = new StringBuilder(); + processType(type, result); + + return result.toString(); + } + + private void processType(TypeMirror type, StringBuilder result) { + type.accept(new SimpleTypeVisitor6() { + + private PackageElement getPackage(Element type) { + while (type.getKind() != ElementKind.PACKAGE) { + type = type.getEnclosingElement(); + } + + return (PackageElement) type; + } + + private String getTypeName(TypeElement type) { + String packageName = getPackage(type).getQualifiedName().toString(); + String qualifiedName = type.getQualifiedName().toString(); + return qualifiedName.substring(packageName.length() !=0? packageName.length() + 1: 0); + } + + @Override + public Void visitDeclared(DeclaredType declaredType, Void v) { + result.append("Type.of("); + result.append(getTypeName((TypeElement) declaredType.asElement())); + result.append(".class)"); + + for (TypeMirror type: declaredType.getTypeArguments()) { + result.append(".typeParam("); + processType(type, result); + result.append(")"); + } + + return null; + } + + @Override + public Void visitPrimitive(PrimitiveType primitiveType, Void v) { + result.append("Type.of("); + result.append(primitiveType); + result.append(".class)"); + return null; + } + + @Override + public Void visitArray(ArrayType arrayType, Void v) { + result.append("Type.array("); + processType(arrayType.getComponentType(), result); + result.append(")"); + + return null; + } + + @Override + public Void visitTypeVariable(TypeVariable typeVariable, Void v) { + processType(processingEnv.getTypeUtils().erasure(typeVariable), result); + return null; + } + + @Override + public Void visitError(ErrorType errorType, Void v) { + return null; + } + + @Override + protected Void defaultAction(TypeMirror typeMirror, Void v) { + return null; + } + }, + null); + } + 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..064f68d 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,30 @@ 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 visitor_works() throws Exception { ResourceVisitor visitor = mock(ResourceVisitor.class, RETURNS_SELF); - when(visitor.as(List.class, String.class)).thenReturn(singletonList("done")); + when(visitor.as(Type.of(List.class).typeParam(Type.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).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", Type.of(String.class)); + inOrder.verify(visitor).param("qI", 1, Type.of(int.class)); + inOrder.verify(visitor).param("qIs", asList(1, 2, 3), Type.of(List.class).typeParam(Type.of(Integer.class))); + inOrder.verify(visitor).param("qIa", new Integer[] {1, 2, 3}, Type.array(Type.of(Integer.class))); + inOrder.verify(visitor).header("hS", "s", Type.of(String.class)); + inOrder.verify(visitor).header("hI", 1, Type.of(int.class)); + inOrder.verify(visitor).as(Type.of(List.class).typeParam(Type.of(String.class))); inOrder.verifyNoMoreInteractions(); } @@ -54,7 +60,7 @@ public interface TestService { @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..a34a4a7 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,15 @@ 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.Type;\n" + "import java.util.Optional;\n" + "import javax.inject.Inject;\n" + "\n" + "public class Rest_RestServiceModel extends RestServiceModel implements Rest {\n" + + " static {\n" + + " Type dummy = null;\n" + + " }\n" + + "\n" + " @Inject\n" + " public Rest_RestServiceModel(final ResourceVisitor.Supplier parent) {\n" + " super(new ResourceVisitor.Supplier() {\n" @@ -38,7 +43,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).path().produces().consumes().as(Type.of(Optional.class).typeParam(Type.of(String.class)));\n" + " }\n" + "}")); } From 589f2802ed713d726caf79ffafc51155f1f68fd8 Mon Sep 17 00:00:00 2001 From: Ivan Markov Date: Mon, 26 Nov 2018 12:27:04 +0200 Subject: [PATCH 2/8] - Type replaced with a generified TypeToken - AutorestProxy - option to use j.l.r.Proxy instead of code generation for JavaSE/Android --- core/pom.xml | 4 +- .../client/CollectorResourceVisitor.java | 64 ++-- .../gwt/autorest/client/GwtIncompatible.java | 16 + .../autorest/client/ReflectionTypeUtils.java | 62 ++++ .../gwt/autorest/client/ResourceVisitor.java | 15 +- .../intendia/gwt/autorest/client/Type.java | 118 -------- .../gwt/autorest/client/TypeToken.java | 252 ++++++++++++++++ .../autorest/client/ResourceVisitorTest.java | 8 +- .../gwt/autorest/client/TypeTokenTest.java | 68 +++++ .../example/client/ExampleEntryPoint.java | 1 - .../client/RequestResourceBuilder.java | 35 ++- jre/pom.xml | 6 +- .../autorest/client/AnnotationProcessor.java | 183 +++++++++++ .../gwt/autorest/client/AutorestProxy.java | 165 ++++++++++ .../autorest/client/JLRAnnotatedElement.java | 50 +++ .../autorest/client/JreResourceBuilder.java | 56 ++-- .../JreAutorestProxyResourceBuilderTest.java | 11 + .../client/JreResourceBuilderTest.java | 21 +- processor/pom.xml | 5 + .../processor/AutoRestGwtProcessor.java | 285 +++++++++--------- .../processor/JLMAnnotatedElement.java | 46 +++ .../autorest/client/ResourceVisitorTest.java | 16 +- 22 files changed, 1137 insertions(+), 350 deletions(-) create mode 100644 core/src/main/java/com/intendia/gwt/autorest/client/GwtIncompatible.java create mode 100644 core/src/main/java/com/intendia/gwt/autorest/client/ReflectionTypeUtils.java delete mode 100644 core/src/main/java/com/intendia/gwt/autorest/client/Type.java create mode 100644 core/src/main/java/com/intendia/gwt/autorest/client/TypeToken.java create mode 100644 core/src/test/java/com/intendia/gwt/autorest/client/TypeTokenTest.java create mode 100644 jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java create mode 100644 jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java create mode 100644 jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java create mode 100644 jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java create mode 100644 processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java diff --git a/core/pom.xml b/core/pom.xml index ade8c8f..4e3bf59 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -11,8 +11,8 @@ AutoREST :: core - 1.7 - 1.7 + 1.8 + 1.8 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 b43211e..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,33 +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 final Type t; - public Param(String k, Object v) { this(k, v, Type.undefined()); } - public Param(String k, Object v, Type t) { this.k = k; this.v = v; this.t = t;} + 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) { + 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 + ", 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 Type dataType; + protected Param data = null; private List expectedStatuses = DEFAULT_EXPECTED_STATUS; @Override public ResourceVisitor method(String method) { @@ -63,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; @@ -73,27 +77,26 @@ public ResourceVisitor path(String path) { return this; } - @Override public ResourceVisitor param(String key, @Nullable Object value, Type type) { + @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, type)); + if (value != null) queryParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor header(String key, @Nullable Object value, Type type) { + @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, type)); + if (value != null) headerParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor form(String key, @Nullable Object value, Type type) { + @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, type)); + if (value != null) formParams.add(new Param<>(key, value, typeToken)); return this; } - @Override public ResourceVisitor data(Object data, Type type) { - this.data = data; - this.dataType = type; + @Override public ResourceVisitor data(T data, TypeToken typeToken) { + this.data = new Param<>(null, data, typeToken); return this; } @@ -101,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(); } @@ -110,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 d81377e..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,6 +11,9 @@ 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); @@ -18,19 +21,19 @@ public interface ResourceVisitor { ResourceVisitor consumes(String... consumes); /** Sets a query param with its type */ - ResourceVisitor param(String key, @Nullable Object value, Type type); + ResourceVisitor param(String key, @Nullable T value, TypeToken typeToken); /** Sets a header param with its type. */ - ResourceVisitor header(String key, @Nullable Object value, Type type); + ResourceVisitor header(String key, @Nullable T value, TypeToken typeToken); - /** Sets a from param with its type. */ - ResourceVisitor form(String key, @Nullable Object value, Type type); + /** 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(Object data, Type typeInfo); + ResourceVisitor data(T data, TypeToken typeToken); /** Wrap the current resource state into a {@code type}. */ - T as(Type type); + T as(TypeToken typeToken); interface Supplier { ResourceVisitor get(); diff --git a/core/src/main/java/com/intendia/gwt/autorest/client/Type.java b/core/src/main/java/com/intendia/gwt/autorest/client/Type.java deleted file mode 100644 index 571917f..0000000 --- a/core/src/main/java/com/intendia/gwt/autorest/client/Type.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.intendia.gwt.autorest.client; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Recursive structure to model Java type meta-data - * associated with corresponding parameter or method - * return result. - */ -public class Type { - private final boolean array; - private final List typeParameters; - private final Class clazz; - - private Type(Class clazz) { - this(clazz, Collections.emptyList(), false); - } - - private Type(Class clazz, List typeParameters, boolean isArray) { - this.clazz = clazz; - this.typeParameters = typeParameters; - this.array = isArray; - } - - public boolean isArray() { - return array; - } - - public boolean isGeneric() { - return !typeParameters.isEmpty(); - } - - public boolean isDefined() { - return clazz == null; - } - - public static Type of(Class clazz) { - return new Type(clazz); - } - - public static Type array(Type type) { - return new Type(type.clazz, type.typeParameters, true); - } - - public static Type undefined() { - return new Type(null); - } - - public Type typeParam(Type type) { - List typeParams = new ArrayList<>(this.typeParameters); - typeParams.add(type); - return new Type(this.clazz, typeParams, type.array); - } - - public Class getClazz() { - return clazz; - } - - public List getTypeParams() { - return Collections.unmodifiableList(typeParameters); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append(clazz.getSimpleName()); - - if (!typeParameters.isEmpty()) { - result.append("<"); - for(Type t: typeParameters) { - result.append(t.toString()); - result.append(", "); - } - result.delete(result.length()-2, result.length()); - result.append(">"); - } - - return result.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (array ? 1231 : 1237); - result = prime * result + ((clazz == null) ? 0 : clazz.hashCode()); - result = prime * result + ((typeParameters == null) ? 0 : typeParameters.hashCode()); - return result; - } - - @Override - public boolean equals(Object other) { - if (other == this) - return true; - - if(!(other instanceof Type)) - return false; - - Type otype = (Type)other; - if (array != otype.array) - return false; - - if (clazz == null) { - if (otype.clazz != null) - return false; - } else { - if (!clazz.equals(otype.clazz)) - return false; - } - - if (!typeParameters.equals(otype.typeParameters)) - return false; - - return true; - } -} 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/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java b/core/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java index 000eb4d..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,7 +4,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; -import java.awt.List; +import java.util.List; import org.junit.Test; @@ -18,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", Type.of(String.class)); } + 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"), Type.of(List.class).typeParam(Type.of(String.class))); } + 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")); @@ -32,6 +32,6 @@ public class ResourceVisitorTest { private static class MyCollectorResourceVisitor extends CollectorResourceVisitor { @Override protected String encodeComponent(String str) { return str; } - @Override public T as(Type 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 0c60aea..59f2cfa 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,7 +14,6 @@ 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.Type; import com.intendia.gwt.autorest.example.client.ExampleService.Greeting; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; 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 c26e09e..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(Type type) { - if (Completable.class.equals(type.getClazz())) return (T) request().toCompletable(); - if (Maybe.class.equals(type.getClazz())) 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(type.getClazz())) 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(type.getClazz())) 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 " + type.getClazz()); + + 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/pom.xml b/jre/pom.xml index 207613d..b19ac1b 100644 --- a/jre/pom.xml +++ b/jre/pom.xml @@ -28,11 +28,15 @@ 2.8.2 + javax.ws.rs + jsr311-api + + junit junit diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java b/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java new file mode 100644 index 0000000..68cfa5e --- /dev/null +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java @@ -0,0 +1,183 @@ +package com.intendia.gwt.autorest.client; + +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.Map.Entry; +import java.util.Optional; +import java.util.function.Function; +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 { + /** + * 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 + * java.lang.reflection-based representations of a class, method and method parameters as well as their + * javax.lang.model-based analogs. + */ + public interface AnnotatedElement { + String getSimpleName(); + + T getAnnotationOverAnnotations(Class annotationClass); + T getAnnotation(Class annotationClass); + } + + 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) { + return Arrays.stream( + ofNullable(annotatedElement.getAnnotation(Path.class)) + .map(Path::value) + .orElse("") + .split("/")) + .filter(s -> !s.isEmpty()) + .map(path -> !path.startsWith("{")? + (Object)path: + parameters + .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/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java b/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java new file mode 100644 index 0000000..c7ada90 --- /dev/null +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java @@ -0,0 +1,165 @@ +package com.intendia.gwt.autorest.client; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; +import com.intendia.gwt.autorest.client.AnnotationProcessor.ParamInfo; + +/** + * A simple {@link Proxy}-based alternative to generating source for JAX-RS proxies via AutorestGwtProcessor. + * Since GWT/J2CL is supporting reflection, this factory can only be used with JavaSE/Android. + */ +public class AutorestProxy { + private static Object[] EMPTY = new Object[0]; + + private AutorestProxy() {} // Namespace + + private interface ParamVisitor { + void visit(String key, T value, TypeToken typeToken); + } + + private interface UnnamedParamVisitor { + void visit(T value, TypeToken typeToken); + } + + public static T create(Class restService, ResourceVisitor.Supplier path) { + return create(Thread.currentThread().getContextClassLoader(), restService, path); + } + + public static T create(ClassLoader classLoader, Class restService, ResourceVisitor.Supplier path) { + Stream methods = Arrays.stream(restService.getMethods()) + .filter(method -> !((method.getModifiers()&java.lang.reflect.Modifier.STATIC) != 0 || method.isDefault())); + + Map> restServiceMethodProxies = methods + .map(method -> new SimpleEntry<>(method, createMethodProxy(method, path))) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + return restService.cast(Proxy.newProxyInstance( + classLoader, + new Class[] {restService}, + (proxy, method, args) -> restServiceMethodProxies.get(method).apply(proxy, args))); + } + + private static BiFunction createMethodProxy(Method method, ResourceVisitor.Supplier path) { + Class restService = method.getDeclaringClass(); + + String name = method.getName(); + Parameter[] parameters = method.getParameters(); + + // Handle the non-JAX-RS methods first + if (name.equals("toString") && parameters.length == 0) + return (proxy, args) -> proxy.getClass().getName() + "::Proxy"; + else if (name.equals("hashCode") && parameters.length == 0) + return (proxy, args) -> 0; + else if (name.equals("equals") && parameters.length == 1 && parameters[0].getType() == Object.class) + return (proxy, args) -> proxy.equals(args[0]); + + // For each JAX-RS method, prepare a closure of precomputed data that will be used during the method invocation + // The data is obtained by analyzing the JAX-RS-annotated rest API interface + + AnnotatedElement restServiceAnnotatedElement = new JLRAnnotatedElement(restService.getSimpleName(), restService, null); + + AnnotationProcessor restServiceProcessor = new AnnotationProcessor(restServiceAnnotatedElement); + + Object[] restServicePaths = restServiceProcessor.getPaths(Stream.empty()).toArray(Object[]::new); + String[] restServiceProduces = restServiceProcessor.getProduces().toArray(String[]::new); + String[] restServiceConsumes = restServiceProcessor.getConsumes().toArray(String[]::new); + + AnnotatedElement methodAnnotatedElement = new JLRAnnotatedElement(method.getName(), method, method.getGenericReturnType()); + + AnnotationProcessor methodProcessor = new AnnotationProcessor(methodAnnotatedElement); + + Supplier>> parametersFactory = () -> + IntStream + .range(0, parameters.length) + .mapToObj(index -> new SimpleEntry<>( + index, + new JLRAnnotatedElement(parameters[index].getName(), parameters[index], parameters[index].getParameterizedType()))); + + String httpMethod = methodProcessor.getHttpMethod(); + + Object[] paths = methodProcessor.getPaths(parametersFactory.get()).toArray(Object[]::new); + + String[] produces = methodProcessor.getProduces(restServiceProduces).toArray(String[]::new); + String[] consumes = methodProcessor.getConsumes(restServiceConsumes).toArray(String[]::new); + + ParamInfo[] queryParams = methodProcessor.getQueryParams(parametersFactory.get()).toArray(ParamInfo[]::new); + ParamInfo[] headerParams = methodProcessor.getHeaderParams(parametersFactory.get()).toArray(ParamInfo[]::new); + ParamInfo[] formParams = methodProcessor.getFormParams(parametersFactory.get()).toArray(ParamInfo[]::new); + + ParamInfo data = methodProcessor.getData(parametersFactory.get()).orElse(null); + + return (proxy, args) -> { + ResourceVisitor visitor = path.get(); + + visitor + .path(restServicePaths) + .method(httpMethod); + + accept(paths, args, visitor); + + visitor + .produces(produces) + .consumes(consumes); + + if (args == null) + args = EMPTY; + + accept(queryParams, args, visitor::param); + accept(headerParams, args, visitor::header); + accept(formParams, args, visitor::form); + + if(data != null) + accept(data, args, visitor::data, createTypeToken(data.getAnnotatedElement())); + + return visitor.as(createTypeToken(methodAnnotatedElement)); + }; + } + + private static void accept(Object[] paths, Object[] args, ResourceVisitor visitor) { + Arrays.stream(paths).forEach(path -> { + if (path instanceof ParamInfo) { + ParamInfo paramInfo = (ParamInfo)path; + accept(paramInfo, args, (UnnamedParamVisitor)visitor::path, createTypeToken(paramInfo.getAnnotatedElement())); + } else + visitor.path(path); + }); + } + + private static void accept(ParamInfo[] params, Object[] args, ParamVisitor paramVisitor) { + Arrays.stream(params).forEach(paramInfo -> + accept(paramInfo, args, paramVisitor, createTypeToken(paramInfo.getAnnotatedElement()))); + } + + private static void accept(ParamInfo paramInfo, Object[] args, ParamVisitor paramVisitor, TypeToken typeToken) { + @SuppressWarnings("unchecked") + T arg = (T)args[paramInfo.getJavaArgumentIndex()]; + + paramVisitor.visit(paramInfo.getName(), arg, typeToken); + } + + private static void accept(ParamInfo paramInfo, Object[] args, UnnamedParamVisitor paramVisitor, TypeToken typeToken) { + @SuppressWarnings("unchecked") + T arg = (T)args[paramInfo.getJavaArgumentIndex()]; + + paramVisitor.visit(arg, typeToken); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static TypeToken createTypeToken(AnnotatedElement annotatedElement) { + JLRAnnotatedElement jlrAnnotatedElement = (JLRAnnotatedElement)annotatedElement; + + return (TypeToken)TypeToken.of(jlrAnnotatedElement.getJlrType()); + } +} diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java b/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java new file mode 100644 index 0000000..183f301 --- /dev/null +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java @@ -0,0 +1,50 @@ +package com.intendia.gwt.autorest.client; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Objects; + +import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; + +/** + * An {@link AnnotatedElement} implementation which delegates to {@link java.lang.reflect.AnnotatedElement}. + */ +public class JLRAnnotatedElement implements AnnotatedElement { + private String simpleName; + private java.lang.reflect.AnnotatedElement jlrAnnotatedElement; + private java.lang.reflect.Type jlrType; + + public JLRAnnotatedElement(String simpleName, java.lang.reflect.AnnotatedElement jlrAnnotatedElement, java.lang.reflect.Type jlrType) { + this.simpleName = simpleName; + this.jlrAnnotatedElement = jlrAnnotatedElement; + this.jlrType = jlrType; + } + + public java.lang.reflect.AnnotatedElement getJlrAnnotatedElement() { + return jlrAnnotatedElement; + } + + public java.lang.reflect.Type getJlrType() { + return jlrType; + } + + @Override + public String getSimpleName() { + return simpleName; + } + + @Override + public T getAnnotation(Class annotationClass) { + return jlrAnnotatedElement.getAnnotation(annotationClass); + } + + @Override + public T getAnnotationOverAnnotations(Class annotationClass) { + return Arrays.stream(jlrAnnotatedElement.getAnnotations()) + .map(Annotation::annotationType) + .map(at -> at.getAnnotation(annotationClass)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } +} 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 bb206e1..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,11 +42,8 @@ public JreResourceBuilder(String root, ConnectionFactory factory, JsonCodec code } catch (Exception e) { throw new RuntimeException(e); } } - @Override public T as(Type type) { - return json.fromJson( - request(), - type.getClazz(), - !type.getTypeParams().isEmpty()? type.getTypeParams().get(0).getClazz(): Void.class); + @Override public T as(TypeToken typeToken) { + return json.fromJson(request(), typeToken); } private Single request() { @@ -54,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); } @@ -103,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 { @@ -114,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) { @@ -138,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); } } @@ -153,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/JreAutorestProxyResourceBuilderTest.java b/jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java new file mode 100644 index 0000000..c32d247 --- /dev/null +++ b/jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java @@ -0,0 +1,11 @@ +package com.intendia.gwt.autorest.client; + +public class JreAutorestProxyResourceBuilderTest extends JreResourceBuilderTest { + @Override + protected TestRestService createRestService(ResourceVisitor.Supplier path) { + return AutorestProxy.create( + Thread.currentThread().getContextClassLoader(), + TestRestService.class, + path); + } +} 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..60986b1 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 @@ -3,21 +3,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import com.sun.net.httpserver.HttpServer; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Single; import java.io.OutputStream; import java.net.InetSocketAddress; + import javax.ws.rs.GET; import javax.ws.rs.Path; + import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -public class JreResourceBuilderTest { +import com.sun.net.httpserver.HttpServer; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Single; + +public class JreResourceBuilderTest { private static HttpServer httpServer; @BeforeClass public static void prepareServer() throws Exception { @@ -47,13 +50,17 @@ public class JreResourceBuilderTest { } private static String baseUrl; - private static TestRestService_RestServiceModel rest; + private static TestRestService rest; @Before public void prepareClient() { baseUrl = "http://localhost:" + httpServer.getAddress().getPort() + "/"; - rest = new TestRestService_RestServiceModel(() -> new JreResourceBuilder(baseUrl)); + rest = createRestService(() -> new JreResourceBuilder(baseUrl)); } + protected TestRestService createRestService(ResourceVisitor.Supplier path) { + return new TestRestService_RestServiceModel(path); + } + @Test public void zero() { assertNull(rest.zero().blockingGet()); } diff --git a/processor/pom.xml b/processor/pom.xml index 01e38b9..cd021ed 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -16,6 +16,11 @@ autorest-core ${project.version} + + com.intendia.gwt.autorest + autorest-jre + ${project.version} + javax.ws.rs jsr311-api 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 88bafa1..5951813 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,8 +1,6 @@ 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; @@ -14,61 +12,59 @@ import static javax.ws.rs.HttpMethod.POST; import static javax.ws.rs.HttpMethod.PUT; -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.Type; -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.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.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.RoundEnvironment; import javax.inject.Inject; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; -import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ErrorType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; -import javax.lang.model.util.SimpleTypeVisitor6; import javax.tools.Diagnostic.Kind; -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; +import com.google.common.base.Throwables; +import com.intendia.gwt.autorest.client.AutoRestGwt; +import com.intendia.gwt.autorest.client.AnnotationProcessor; +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.client.AnnotationProcessor.AnnotatedElement; +import com.intendia.gwt.autorest.client.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(); @Override public Set getSupportedOptions() { return singleton("debug"); } @@ -94,9 +90,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 JLMAnnotatedElement(restService.getSimpleName().toString(), restService); + + 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); @@ -110,8 +110,8 @@ private void processRestService(TypeElement restService) throws Exception { .superclass(RestServiceModel.class) .addSuperinterface(TypeName.get(restService.asType())); - //Not a brilliant way to get import for Type (kind of deficiency in javapoet) - modelTypeBuilder.addStaticBlock(CodeBlock.builder().addStatement("$T dummy = null", Type.class).build()); + // Add an import for TypeToken + modelTypeBuilder.addStaticBlock(CodeBlock.builder().addStatement("$T dummy = null", TypeToken.class).build()); modelTypeBuilder.addMethod(MethodSpec.constructorBuilder() .addAnnotation(Inject.class) @@ -129,56 +129,60 @@ private void processRestService(TypeElement restService) throws Exception { Set methodImports = new HashSet<>(); for (ExecutableElement method : methods) { - String methodName = method.getSimpleName().toString(); - + AnnotatedElement annotatedElement = new JLMAnnotatedElement(method.getSimpleName().toString(), method); + + AnnotationProcessor processor = new AnnotationProcessor(annotatedElement); + Optional incompatible = isIncompatible(method); if (incompatible.isPresent()) { modelTypeBuilder.addMethod(MethodSpec.overriding(method) .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.getParameters(); + + Supplier>> parametersFactory = () -> + IntStream + .range(0, parameters.size()) + .mapToObj(index -> new SimpleEntry<>( + index, + new JLMAnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.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, $L)", p.getAnnotation(QueryParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); + 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, $L)", p.getAnnotation(HeaderParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); + 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, $L)", p.getAnnotation(FormParam.class).value(), p.getSimpleName(), createTypeInfo(p.asType()))); + addParamLiterals(builder, "form", processor.getFormParams(parametersFactory.get())); + // data - method.getParameters().stream().filter(p -> !isParam(p)).findFirst() - .ifPresent(data -> builder.add(".data($L, $L)", data.getSimpleName(), createTypeInfo(data.asType()))); + processor.getData(parametersFactory.get()) + .ifPresent(dataInfo -> builder.add( + ".data($L, $L)", + dataInfo.getJavaArgumentName(), + asTypeTokenLiteral(dataInfo.getAnnotatedElement()))); } - builder.add(".as($L);\n$]",createTypeInfo(method.getReturnType())); + + builder.add(".as($L);\n$]", asTypeTokenLiteral(annotatedElement)); modelTypeBuilder.addMethod(MethodSpec.overriding(method).addCode(builder.build()).build()); } @@ -190,81 +194,92 @@ private void processRestService(TypeElement restService) throws Exception { file.skipJavaLangImports(skipJavaLangImports).build().writeTo(filer); } - private String createTypeInfo(TypeMirror type) { - StringBuilder result = new StringBuilder(); - processType(type, result); + private CodeBlock asTypeTokenLiteral(AnnotatedElement annotatedElement) { + CodeBlock.Builder builder = CodeBlock.builder(); + + JLMAnnotatedElement jlmAnnotatedElement = (JLMAnnotatedElement)annotatedElement; + + addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmElement().asType())); + + return builder.build(); + } + + private void addTypeTokenLiteral(CodeBlock.Builder builder, TypeName name) { + builder.add("new TypeToken<$L>(", 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) { + rawType = name; + typeArguments = Collections.emptyList(); + } else + throw new IllegalArgumentException("Unsupported type " + name); + + builder.add("$T.class", rawType); + + for (TypeName typeArgumentName: typeArguments) { + builder.add(", "); + addTypeTokenLiteral(builder, typeArgumentName); + } - return result.toString(); + builder.add(") {}"); } - - private void processType(TypeMirror type, StringBuilder result) { - type.accept(new SimpleTypeVisitor6() { - - private PackageElement getPackage(Element type) { - while (type.getKind() != ElementKind.PACKAGE) { - type = type.getEnclosingElement(); - } - return (PackageElement) type; - } + 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", ", "); - private String getTypeName(TypeElement type) { - String packageName = getPackage(type).getQualifiedName().toString(); - String qualifiedName = type.getQualifiedName().toString(); - return qualifiedName.substring(packageName.length() !=0? packageName.length() + 1: 0); - } + consumer.accept(builder, item); - @Override - public Void visitDeclared(DeclaredType declaredType, Void v) { - result.append("Type.of("); - result.append(getTypeName((TypeElement) declaredType.asElement())); - result.append(".class)"); - - for (TypeMirror type: declaredType.getTypeArguments()) { - result.append(".typeParam("); - processType(type, result); - result.append(")"); - } - - return null; - } - - @Override - public Void visitPrimitive(PrimitiveType primitiveType, Void v) { - result.append("Type.of("); - result.append(primitiveType); - result.append(".class)"); - return null; - } - - @Override - public Void visitArray(ArrayType arrayType, Void v) { - result.append("Type.array("); - processType(arrayType.getComponentType(), result); - result.append(")"); - - return null; - } - - @Override - public Void visitTypeVariable(TypeVariable typeVariable, Void v) { - processType(processingEnv.getTypeUtils().erasure(typeVariable), result); - return null; - } - - @Override - public Void visitError(ErrorType errorType, Void v) { - return null; - } - - @Override - protected Void defaultAction(TypeMirror typeMirror, Void v) { - return null; - } - }, - null); - } - + 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/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java new file mode 100644 index 0000000..fefe8a5 --- /dev/null +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java @@ -0,0 +1,46 @@ +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 com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; + +/** + * An {@link AnnotatedElement} implementation which delegates to {@link Element}. + */ +public class JLMAnnotatedElement implements AnnotatedElement { + private String simpleName; + private Element jlmElement; + + public JLMAnnotatedElement(String simpleName, Element jlmElement) { + this.simpleName = simpleName; + this.jlmElement = jlmElement; + } + + public Element getJlmElement() { + return jlmElement; + } + + @Override + public String getSimpleName() { + return simpleName; + } + + @Override + public T getAnnotation(Class annotationClass) { + return jlmElement.getAnnotation(annotationClass); + } + + @Override + 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/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java b/processor/src/test/java/com/intendia/gwt/autorest/client/ResourceVisitorTest.java index 064f68d..1d7eaae 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 @@ -32,7 +32,7 @@ public class ResourceVisitorTest { @Test public void visitor_works() throws Exception { ResourceVisitor visitor = mock(ResourceVisitor.class, RETURNS_SELF); - when(visitor.as(Type.of(List.class).typeParam(Type.of(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), new Integer[]{ 1, 2, 3}, "s", 1); InOrder inOrder = inOrder(visitor); @@ -40,13 +40,13 @@ public class ResourceVisitorTest { inOrder.verify(visitor).path("b", "s", 1, "c"); inOrder.verify(visitor).produces("application/json"); inOrder.verify(visitor).consumes("application/json"); - inOrder.verify(visitor).param("qS", "s", Type.of(String.class)); - inOrder.verify(visitor).param("qI", 1, Type.of(int.class)); - inOrder.verify(visitor).param("qIs", asList(1, 2, 3), Type.of(List.class).typeParam(Type.of(Integer.class))); - inOrder.verify(visitor).param("qIa", new Integer[] {1, 2, 3}, Type.array(Type.of(Integer.class))); - inOrder.verify(visitor).header("hS", "s", Type.of(String.class)); - inOrder.verify(visitor).header("hI", 1, Type.of(int.class)); - inOrder.verify(visitor).as(Type.of(List.class).typeParam(Type.of(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(); } From 7ed28dcb5c8a6dfb765edaa0e2b02fc1dc8b0e93 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Tue, 11 Dec 2018 16:15:07 +0200 Subject: [PATCH 3/8] Fix minor issues related to AnnotationProcessor and AutorestGwtProcessor, fix several unit tests --- .../example/client/ExampleEntryPoint.java | 4 ++- .../autorest/client/AnnotationProcessor.java | 6 +++- .../client/JreResourceBuilderTest.java | 2 +- .../processor/AutoRestGwtProcessor.java | 30 +++++++++++-------- .../autorest/client/ResourceVisitorTest.java | 2 +- .../processor/AutoRestGwtProcessorTest.java | 7 ++--- 6 files changed, 30 insertions(+), 21 deletions(-) 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 59f2cfa..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", Type.undefined())); + 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/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java b/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java index 68cfa5e..1ca2885 100644 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java @@ -5,9 +5,11 @@ 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; @@ -84,6 +86,8 @@ public String getHttpMethod() { } public Stream getPaths(Stream> parameters) { + List> paramList = parameters.collect(Collectors.toList()); + return Arrays.stream( ofNullable(annotatedElement.getAnnotation(Path.class)) .map(Path::value) @@ -92,7 +96,7 @@ public Stream getPaths(Stream .filter(s -> !s.isEmpty()) .map(path -> !path.startsWith("{")? (Object)path: - parameters + paramList.stream() .filter(entry -> ofNullable(entry.getValue().getAnnotation(PathParam.class)) .map(PathParam::value) .map(v -> path.equals("{" + v + "}")) 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 60986b1..9850abb 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 @@ -58,7 +58,7 @@ public class JreResourceBuilderTest { } protected TestRestService createRestService(ResourceVisitor.Supplier path) { - return new TestRestService_RestServiceModel(path); + return AutorestProxy.create(TestRestService.class, path); } @Test public void zero() { 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 5951813..d567ddc 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 @@ -36,6 +36,7 @@ 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.tools.Diagnostic.Kind; import javax.ws.rs.CookieParam; import javax.ws.rs.FormParam; @@ -109,16 +110,13 @@ private void processRestService(TypeElement restService) throws Exception { .addModifiers(Modifier.PUBLIC) .superclass(RestServiceModel.class) .addSuperinterface(TypeName.get(restService.asType())); - - // Add an import for TypeToken - modelTypeBuilder.addStaticBlock(CodeBlock.builder().addStatement("$T dummy = null", TypeToken.class).build()); - + 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() @@ -193,19 +191,23 @@ private void processRestService(TypeElement restService) throws Exception { boolean skipJavaLangImports = processingEnv.getOptions().containsKey("skipJavaLangImports"); file.skipJavaLangImports(skipJavaLangImports).build().writeTo(filer); } + private CodeBlock asTypeTokenLiteral(AnnotatedElement annotatedElement) { CodeBlock.Builder builder = CodeBlock.builder(); JLMAnnotatedElement jlmAnnotatedElement = (JLMAnnotatedElement)annotatedElement; - addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmElement().asType())); + if (jlmAnnotatedElement.getJlmElement().asType() instanceof ExecutableType) + addTypeTokenLiteral(builder, TypeName.get(((ExecutableType)jlmAnnotatedElement.getJlmElement().asType()).getReturnType())); + else + addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmElement().asType())); return builder.build(); } private void addTypeTokenLiteral(CodeBlock.Builder builder, TypeName name) { - builder.add("new TypeToken<$L>(", name); + builder.add("new $T<$L>(", TypeToken.class, name.isPrimitive()? name.box(): name); TypeName rawType; List typeArguments; @@ -219,13 +221,16 @@ private void addTypeTokenLiteral(CodeBlock.Builder builder, TypeName name) { rawType = null; typeArguments = Collections.singletonList(arrayTypeName.componentType); - } else if (name instanceof ClassName) { - rawType = name; + } else if (name instanceof ClassName || name instanceof TypeName) { + rawType = name.isPrimitive()? name.box(): name; typeArguments = Collections.emptyList(); } else throw new IllegalArgumentException("Unsupported type " + name); - builder.add("$T.class", rawType); + if(rawType == null) + builder.add("null"); + else + builder.add("$T.class", rawType); for (TypeName typeArgumentName: typeArguments) { builder.add(", "); @@ -251,14 +256,15 @@ private void addObjectOrParamLiterals(CodeBlock.Builder builder, String name, St if (item instanceof ParamInfo) { ParamInfo paramInfo = (ParamInfo)item; builder.add( - "$L($L, $L)", + ".$L($L, $L)", name, paramInfo.getJavaArgumentName(), asTypeTokenLiteral(paramInfo.getAnnotatedElement())); } else { - builder.add("$L($S)", name, item.toString()); + builder.add(".$L($S)", name, item.toString()); } }); + } private static void addStringLiterals(CodeBlock.Builder builder, String name, Stream strings) { 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 1d7eaae..93740ce 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 @@ -37,7 +37,7 @@ public class ResourceVisitorTest { 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", TypeToken.of(String.class)); 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 a34a4a7..371690d 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,14 +25,11 @@ 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.Type;\n" + + "import com.intendia.gwt.autorest.client.TypeToken;\n" + "import java.util.Optional;\n" + "import javax.inject.Inject;\n" + "\n" + "public class Rest_RestServiceModel extends RestServiceModel implements Rest {\n" - + " static {\n" - + " Type dummy = null;\n" - + " }\n" + "\n" + " @Inject\n" + " public Rest_RestServiceModel(final ResourceVisitor.Supplier parent) {\n" @@ -43,7 +40,7 @@ public class AutoRestGwtProcessorTest { + "\n" + " @Override" + " public Optional getStr() {\n" - + " return method(GET).path().produces().consumes().as(Type.of(Optional.class).typeParam(Type.of(String.class)));\n" + + " return method(GET).produces().consumes().as(new TypeToken>(Optional.class, new TypeToken(String.class){}){});\n" + " }\n" + "}")); } From 36d6a198037f5fc147c5fdc973349dc478e6acb7 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Mon, 17 Dec 2018 13:50:29 +0200 Subject: [PATCH 4/8] Extend gwt processor to support interface hierarchy --- .../processor/AutoRestGwtProcessor.java | 77 ++++++++++++++----- .../processor/JLMAnnotatedElement.java | 9 ++- .../autorest/client/ResourceVisitorTest.java | 40 +++++++++- 3 files changed, 103 insertions(+), 23 deletions(-) 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 d567ddc..6f5359c 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 @@ -5,6 +5,7 @@ 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; @@ -16,6 +17,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -27,6 +29,7 @@ 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; @@ -37,12 +40,20 @@ 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.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.POST; +import javax.ws.rs.PUT; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; @@ -67,13 +78,22 @@ 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 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() @@ -91,7 +111,7 @@ public class AutoRestGwtProcessor extends AbstractProcessor { } private void processRestService(TypeElement restService) throws Exception { - AnnotatedElement restServiceAnnotatedElement = new JLMAnnotatedElement(restService.getSimpleName().toString(), restService); + AnnotatedElement restServiceAnnotatedElement = new JLMAnnotatedElement(restService.getSimpleName().toString(), restService, restService.asType()); AnnotationProcessor restServiceProcessor = new AnnotationProcessor(restServiceAnnotatedElement); @@ -119,21 +139,17 @@ private void processRestService(TypeElement restService) throws Exception { 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) { - AnnotatedElement annotatedElement = new JLMAnnotatedElement(method.getSimpleName().toString(), method); - + for (Map.Entry method: methods.entrySet()) { + + AnnotatedElement annotatedElement = new JLMAnnotatedElement(method.getKey().getSimpleName().toString(), method.getKey(), method.getValue()); AnnotationProcessor processor = new AnnotationProcessor(annotatedElement); - Optional incompatible = isIncompatible(method); + 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, annotatedElement.getSimpleName()) .build()); @@ -142,14 +158,15 @@ private void processRestService(TypeElement restService) throws Exception { CodeBlock.Builder builder = CodeBlock.builder().add("$[return "); { - List parameters = method.getParameters(); - + List parameters = method.getKey().getParameters(); + List parameterTypes = method.getValue().getParameterTypes(); + Supplier>> parametersFactory = () -> IntStream .range(0, parameters.size()) .mapToObj(index -> new SimpleEntry<>( index, - new JLMAnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.get(index)))); + new JLMAnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.get(index), parameterTypes.get(index)))); // method type builder.add("method($L)", methodImport(methodImports, processor.getHttpMethod())); @@ -182,7 +199,7 @@ private void processRestService(TypeElement restService) throws Exception { 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(); @@ -192,16 +209,34 @@ private void processRestService(TypeElement restService) throws Exception { 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(); JLMAnnotatedElement jlmAnnotatedElement = (JLMAnnotatedElement)annotatedElement; - if (jlmAnnotatedElement.getJlmElement().asType() instanceof ExecutableType) - addTypeTokenLiteral(builder, TypeName.get(((ExecutableType)jlmAnnotatedElement.getJlmElement().asType()).getReturnType())); + if (jlmAnnotatedElement.getJlmType() instanceof ExecutableType) + addTypeTokenLiteral(builder, TypeName.get(((ExecutableType)jlmAnnotatedElement.getJlmType()).getReturnType())); else - addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmElement().asType())); + addTypeTokenLiteral(builder, TypeName.get(jlmAnnotatedElement.getJlmType())); return builder.build(); } diff --git a/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java index fefe8a5..6f2e40b 100644 --- a/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java @@ -6,6 +6,7 @@ import java.util.Objects; import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; @@ -15,16 +16,22 @@ public class JLMAnnotatedElement implements AnnotatedElement { private String simpleName; private Element jlmElement; + private TypeMirror jlmType; - public JLMAnnotatedElement(String simpleName, Element jlmElement) { + public JLMAnnotatedElement(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; + } + @Override public String getSimpleName() { return simpleName; 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 93740ce..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 @@ -30,6 +30,33 @@ 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(new TypeToken>(List.class, TypeToken.of(String.class)) {})).thenReturn(singletonList("done")); @@ -55,8 +82,19 @@ 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, From c0561a3e3e6e80da8c449d344339e1003cccc532 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Tue, 18 Dec 2018 11:17:18 +0200 Subject: [PATCH 5/8] Fix AutorestProxy to work with interface hierarchy --- .../gwt/autorest/client/AutorestProxy.java | 6 ++---- .../autorest/client/JreResourceBuilderTest.java | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java b/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java index c7ada90..c047b99 100644 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java +++ b/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java @@ -42,7 +42,7 @@ public static T create(ClassLoader classLoader, Class restService, Resour .filter(method -> !((method.getModifiers()&java.lang.reflect.Modifier.STATIC) != 0 || method.isDefault())); Map> restServiceMethodProxies = methods - .map(method -> new SimpleEntry<>(method, createMethodProxy(method, path))) + .map(method -> new SimpleEntry<>(method, createMethodProxy(restService, method, path))) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); return restService.cast(Proxy.newProxyInstance( @@ -51,9 +51,7 @@ public static T create(ClassLoader classLoader, Class restService, Resour (proxy, method, args) -> restServiceMethodProxies.get(method).apply(proxy, args))); } - private static BiFunction createMethodProxy(Method method, ResourceVisitor.Supplier path) { - Class restService = method.getDeclaringClass(); - + private static BiFunction createMethodProxy(Class restService, Method method, ResourceVisitor.Supplier path) { String name = method.getName(); Parameter[] parameters = method.getParameters(); 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 9850abb..7e64986 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 @@ -43,6 +43,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() { @@ -65,6 +72,10 @@ protected TestRestService createRestService(ResourceVisitor.Supplier path) { 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); } @@ -73,8 +84,12 @@ protected TestRestService createRestService(ResourceVisitor.Supplier path) { 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(); From 70ffc8f85f079100ab60006b30eb2f29874ce1f5 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Thu, 20 Dec 2018 13:02:25 +0200 Subject: [PATCH 6/8] Remove AutorestProxy from jre project --- jre/pom.xml | 6 +- .../gwt/autorest/client/AutorestProxy.java | 165 ------------------ .../autorest/client/JLRAnnotatedElement.java | 50 ------ .../JreAutorestProxyResourceBuilderTest.java | 11 -- .../client/JreResourceBuilderTest.java | 21 +-- processor/pom.xml | 5 - ...atedElement.java => AnnotatedElement.java} | 13 +- .../processor}/AnnotationProcessor.java | 16 +- .../processor/AutoRestGwtProcessor.java | 12 +- 9 files changed, 20 insertions(+), 279 deletions(-) delete mode 100644 jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java delete mode 100644 jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java delete mode 100644 jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java rename processor/src/main/java/com/intendia/gwt/autorest/processor/{JLMAnnotatedElement.java => AnnotatedElement.java} (72%) rename {jre/src/main/java/com/intendia/gwt/autorest/client => processor/src/main/java/com/intendia/gwt/autorest/processor}/AnnotationProcessor.java (87%) diff --git a/jre/pom.xml b/jre/pom.xml index b19ac1b..207613d 100644 --- a/jre/pom.xml +++ b/jre/pom.xml @@ -28,15 +28,11 @@ 2.8.2 - javax.ws.rs - jsr311-api - - + junit junit diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java b/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java deleted file mode 100644 index c7ada90..0000000 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/AutorestProxy.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.intendia.gwt.autorest.client; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.lang.reflect.Proxy; -import java.util.AbstractMap.SimpleEntry; -import java.util.Arrays; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; -import com.intendia.gwt.autorest.client.AnnotationProcessor.ParamInfo; - -/** - * A simple {@link Proxy}-based alternative to generating source for JAX-RS proxies via AutorestGwtProcessor. - * Since GWT/J2CL is supporting reflection, this factory can only be used with JavaSE/Android. - */ -public class AutorestProxy { - private static Object[] EMPTY = new Object[0]; - - private AutorestProxy() {} // Namespace - - private interface ParamVisitor { - void visit(String key, T value, TypeToken typeToken); - } - - private interface UnnamedParamVisitor { - void visit(T value, TypeToken typeToken); - } - - public static T create(Class restService, ResourceVisitor.Supplier path) { - return create(Thread.currentThread().getContextClassLoader(), restService, path); - } - - public static T create(ClassLoader classLoader, Class restService, ResourceVisitor.Supplier path) { - Stream methods = Arrays.stream(restService.getMethods()) - .filter(method -> !((method.getModifiers()&java.lang.reflect.Modifier.STATIC) != 0 || method.isDefault())); - - Map> restServiceMethodProxies = methods - .map(method -> new SimpleEntry<>(method, createMethodProxy(method, path))) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - - return restService.cast(Proxy.newProxyInstance( - classLoader, - new Class[] {restService}, - (proxy, method, args) -> restServiceMethodProxies.get(method).apply(proxy, args))); - } - - private static BiFunction createMethodProxy(Method method, ResourceVisitor.Supplier path) { - Class restService = method.getDeclaringClass(); - - String name = method.getName(); - Parameter[] parameters = method.getParameters(); - - // Handle the non-JAX-RS methods first - if (name.equals("toString") && parameters.length == 0) - return (proxy, args) -> proxy.getClass().getName() + "::Proxy"; - else if (name.equals("hashCode") && parameters.length == 0) - return (proxy, args) -> 0; - else if (name.equals("equals") && parameters.length == 1 && parameters[0].getType() == Object.class) - return (proxy, args) -> proxy.equals(args[0]); - - // For each JAX-RS method, prepare a closure of precomputed data that will be used during the method invocation - // The data is obtained by analyzing the JAX-RS-annotated rest API interface - - AnnotatedElement restServiceAnnotatedElement = new JLRAnnotatedElement(restService.getSimpleName(), restService, null); - - AnnotationProcessor restServiceProcessor = new AnnotationProcessor(restServiceAnnotatedElement); - - Object[] restServicePaths = restServiceProcessor.getPaths(Stream.empty()).toArray(Object[]::new); - String[] restServiceProduces = restServiceProcessor.getProduces().toArray(String[]::new); - String[] restServiceConsumes = restServiceProcessor.getConsumes().toArray(String[]::new); - - AnnotatedElement methodAnnotatedElement = new JLRAnnotatedElement(method.getName(), method, method.getGenericReturnType()); - - AnnotationProcessor methodProcessor = new AnnotationProcessor(methodAnnotatedElement); - - Supplier>> parametersFactory = () -> - IntStream - .range(0, parameters.length) - .mapToObj(index -> new SimpleEntry<>( - index, - new JLRAnnotatedElement(parameters[index].getName(), parameters[index], parameters[index].getParameterizedType()))); - - String httpMethod = methodProcessor.getHttpMethod(); - - Object[] paths = methodProcessor.getPaths(parametersFactory.get()).toArray(Object[]::new); - - String[] produces = methodProcessor.getProduces(restServiceProduces).toArray(String[]::new); - String[] consumes = methodProcessor.getConsumes(restServiceConsumes).toArray(String[]::new); - - ParamInfo[] queryParams = methodProcessor.getQueryParams(parametersFactory.get()).toArray(ParamInfo[]::new); - ParamInfo[] headerParams = methodProcessor.getHeaderParams(parametersFactory.get()).toArray(ParamInfo[]::new); - ParamInfo[] formParams = methodProcessor.getFormParams(parametersFactory.get()).toArray(ParamInfo[]::new); - - ParamInfo data = methodProcessor.getData(parametersFactory.get()).orElse(null); - - return (proxy, args) -> { - ResourceVisitor visitor = path.get(); - - visitor - .path(restServicePaths) - .method(httpMethod); - - accept(paths, args, visitor); - - visitor - .produces(produces) - .consumes(consumes); - - if (args == null) - args = EMPTY; - - accept(queryParams, args, visitor::param); - accept(headerParams, args, visitor::header); - accept(formParams, args, visitor::form); - - if(data != null) - accept(data, args, visitor::data, createTypeToken(data.getAnnotatedElement())); - - return visitor.as(createTypeToken(methodAnnotatedElement)); - }; - } - - private static void accept(Object[] paths, Object[] args, ResourceVisitor visitor) { - Arrays.stream(paths).forEach(path -> { - if (path instanceof ParamInfo) { - ParamInfo paramInfo = (ParamInfo)path; - accept(paramInfo, args, (UnnamedParamVisitor)visitor::path, createTypeToken(paramInfo.getAnnotatedElement())); - } else - visitor.path(path); - }); - } - - private static void accept(ParamInfo[] params, Object[] args, ParamVisitor paramVisitor) { - Arrays.stream(params).forEach(paramInfo -> - accept(paramInfo, args, paramVisitor, createTypeToken(paramInfo.getAnnotatedElement()))); - } - - private static void accept(ParamInfo paramInfo, Object[] args, ParamVisitor paramVisitor, TypeToken typeToken) { - @SuppressWarnings("unchecked") - T arg = (T)args[paramInfo.getJavaArgumentIndex()]; - - paramVisitor.visit(paramInfo.getName(), arg, typeToken); - } - - private static void accept(ParamInfo paramInfo, Object[] args, UnnamedParamVisitor paramVisitor, TypeToken typeToken) { - @SuppressWarnings("unchecked") - T arg = (T)args[paramInfo.getJavaArgumentIndex()]; - - paramVisitor.visit(arg, typeToken); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static TypeToken createTypeToken(AnnotatedElement annotatedElement) { - JLRAnnotatedElement jlrAnnotatedElement = (JLRAnnotatedElement)annotatedElement; - - return (TypeToken)TypeToken.of(jlrAnnotatedElement.getJlrType()); - } -} diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java b/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java deleted file mode 100644 index 183f301..0000000 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/JLRAnnotatedElement.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.intendia.gwt.autorest.client; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Objects; - -import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; - -/** - * An {@link AnnotatedElement} implementation which delegates to {@link java.lang.reflect.AnnotatedElement}. - */ -public class JLRAnnotatedElement implements AnnotatedElement { - private String simpleName; - private java.lang.reflect.AnnotatedElement jlrAnnotatedElement; - private java.lang.reflect.Type jlrType; - - public JLRAnnotatedElement(String simpleName, java.lang.reflect.AnnotatedElement jlrAnnotatedElement, java.lang.reflect.Type jlrType) { - this.simpleName = simpleName; - this.jlrAnnotatedElement = jlrAnnotatedElement; - this.jlrType = jlrType; - } - - public java.lang.reflect.AnnotatedElement getJlrAnnotatedElement() { - return jlrAnnotatedElement; - } - - public java.lang.reflect.Type getJlrType() { - return jlrType; - } - - @Override - public String getSimpleName() { - return simpleName; - } - - @Override - public T getAnnotation(Class annotationClass) { - return jlrAnnotatedElement.getAnnotation(annotationClass); - } - - @Override - public T getAnnotationOverAnnotations(Class annotationClass) { - return Arrays.stream(jlrAnnotatedElement.getAnnotations()) - .map(Annotation::annotationType) - .map(at -> at.getAnnotation(annotationClass)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } -} diff --git a/jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java b/jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java deleted file mode 100644 index c32d247..0000000 --- a/jre/src/test/java/com/intendia/gwt/autorest/client/JreAutorestProxyResourceBuilderTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.intendia.gwt.autorest.client; - -public class JreAutorestProxyResourceBuilderTest extends JreResourceBuilderTest { - @Override - protected TestRestService createRestService(ResourceVisitor.Supplier path) { - return AutorestProxy.create( - Thread.currentThread().getContextClassLoader(), - TestRestService.class, - path); - } -} 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 9850abb..0b3292f 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 @@ -3,24 +3,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import com.sun.net.httpserver.HttpServer; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Single; import java.io.OutputStream; import java.net.InetSocketAddress; - import javax.ws.rs.GET; import javax.ws.rs.Path; - import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import com.sun.net.httpserver.HttpServer; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Single; - public class JreResourceBuilderTest { + private static HttpServer httpServer; @BeforeClass public static void prepareServer() throws Exception { @@ -50,17 +47,13 @@ public class JreResourceBuilderTest { } private static String baseUrl; - private static TestRestService rest; + private static TestRestService_RestServiceModel rest; @Before public void prepareClient() { baseUrl = "http://localhost:" + httpServer.getAddress().getPort() + "/"; - rest = createRestService(() -> new JreResourceBuilder(baseUrl)); + rest = new TestRestService_RestServiceModel(() -> new JreResourceBuilder(baseUrl)); } - protected TestRestService createRestService(ResourceVisitor.Supplier path) { - return AutorestProxy.create(TestRestService.class, path); - } - @Test public void zero() { assertNull(rest.zero().blockingGet()); } diff --git a/processor/pom.xml b/processor/pom.xml index cd021ed..01e38b9 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -16,11 +16,6 @@ autorest-core ${project.version} - - com.intendia.gwt.autorest - autorest-jre - ${project.version} - javax.ws.rs jsr311-api diff --git a/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java similarity index 72% rename from processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java rename to processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java index 6f2e40b..93c4350 100644 --- a/processor/src/main/java/com/intendia/gwt/autorest/processor/JLMAnnotatedElement.java +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotatedElement.java @@ -8,17 +8,19 @@ import javax.lang.model.element.Element; import javax.lang.model.type.TypeMirror; -import com.intendia.gwt.autorest.client.AnnotationProcessor.AnnotatedElement; /** - * An {@link AnnotatedElement} implementation which delegates to {@link Element}. + * 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 JLMAnnotatedElement implements AnnotatedElement { +public class AnnotatedElement { private String simpleName; private Element jlmElement; private TypeMirror jlmType; - public JLMAnnotatedElement(String simpleName, Element jlmElement, TypeMirror jlmType) { + public AnnotatedElement(String simpleName, Element jlmElement, TypeMirror jlmType) { this.simpleName = simpleName; this.jlmElement = jlmElement; this.jlmType = jlmType; @@ -32,17 +34,14 @@ public TypeMirror getJlmType() { return jlmType; } - @Override public String getSimpleName() { return simpleName; } - @Override public T getAnnotation(Class annotationClass) { return jlmElement.getAnnotation(annotationClass); } - @Override public T getAnnotationOverAnnotations(Class annotationClass) { return jlmElement.getAnnotationMirrors().stream() .map(a -> asElement(a.getAnnotationType()).getAnnotation(annotationClass)) diff --git a/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java similarity index 87% rename from jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java rename to processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java index 1ca2885..0543e0b 100644 --- a/jre/src/main/java/com/intendia/gwt/autorest/client/AnnotationProcessor.java +++ b/processor/src/main/java/com/intendia/gwt/autorest/processor/AnnotationProcessor.java @@ -1,4 +1,4 @@ -package com.intendia.gwt.autorest.client; +package com.intendia.gwt.autorest.processor; import static java.util.Optional.ofNullable; import static javax.ws.rs.HttpMethod.GET; @@ -28,20 +28,6 @@ * HTTP method type, as well as data, query, header and form parameters' information. */ public class AnnotationProcessor { - /** - * 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 - * java.lang.reflection-based representations of a class, method and method parameters as well as their - * javax.lang.model-based analogs. - */ - public interface AnnotatedElement { - String getSimpleName(); - - T getAnnotationOverAnnotations(Class annotationClass); - T getAnnotation(Class annotationClass); - } - public static class ParamInfo { private String name; private AnnotatedElement annotatedElement; 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 6f5359c..e8d94ac 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 @@ -59,12 +59,10 @@ import com.google.common.base.Throwables; import com.intendia.gwt.autorest.client.AutoRestGwt; -import com.intendia.gwt.autorest.client.AnnotationProcessor; 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.client.AnnotationProcessor.AnnotatedElement; -import com.intendia.gwt.autorest.client.AnnotationProcessor.ParamInfo; +import com.intendia.gwt.autorest.processor.AnnotationProcessor.ParamInfo; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ArrayTypeName; import com.squareup.javapoet.ClassName; @@ -111,7 +109,7 @@ public class AutoRestGwtProcessor extends AbstractProcessor { } private void processRestService(TypeElement restService) throws Exception { - AnnotatedElement restServiceAnnotatedElement = new JLMAnnotatedElement(restService.getSimpleName().toString(), restService, restService.asType()); + AnnotatedElement restServiceAnnotatedElement = new AnnotatedElement(restService.getSimpleName().toString(), restService, restService.asType()); AnnotationProcessor restServiceProcessor = new AnnotationProcessor(restServiceAnnotatedElement); @@ -144,7 +142,7 @@ private void processRestService(TypeElement restService) throws Exception { Set methodImports = new HashSet<>(); for (Map.Entry method: methods.entrySet()) { - AnnotatedElement annotatedElement = new JLMAnnotatedElement(method.getKey().getSimpleName().toString(), method.getKey(), method.getValue()); + AnnotatedElement annotatedElement = new AnnotatedElement(method.getKey().getSimpleName().toString(), method.getKey(), method.getValue()); AnnotationProcessor processor = new AnnotationProcessor(annotatedElement); Optional incompatible = isIncompatible(method.getKey()); @@ -166,7 +164,7 @@ private void processRestService(TypeElement restService) throws Exception { .range(0, parameters.size()) .mapToObj(index -> new SimpleEntry<>( index, - new JLMAnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.get(index), parameterTypes.get(index)))); + new AnnotatedElement(parameters.get(index).getSimpleName().toString(), parameters.get(index), parameterTypes.get(index)))); // method type builder.add("method($L)", methodImport(methodImports, processor.getHttpMethod())); @@ -231,7 +229,7 @@ private Map getAllMethods(TypeElement restSe private CodeBlock asTypeTokenLiteral(AnnotatedElement annotatedElement) { CodeBlock.Builder builder = CodeBlock.builder(); - JLMAnnotatedElement jlmAnnotatedElement = (JLMAnnotatedElement)annotatedElement; + AnnotatedElement jlmAnnotatedElement = (AnnotatedElement)annotatedElement; if (jlmAnnotatedElement.getJlmType() instanceof ExecutableType) addTypeTokenLiteral(builder, TypeName.get(((ExecutableType)jlmAnnotatedElement.getJlmType()).getReturnType())); From ae87c3b0a824a6935944e99dab916f076c85e8b5 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Thu, 17 Jan 2019 17:50:55 +0200 Subject: [PATCH 7/8] autores-core packaged as gwt-lib --- core/pom.xml | 14 +++++++++++++- core/src/main/module.gwt.xml | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 core/src/main/module.gwt.xml diff --git a/core/pom.xml b/core/pom.xml index 4e3bf59..90aeebd 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ HEAD-SNAPSHOT autorest-core - jar + gwt-lib AutoREST :: core @@ -40,6 +40,18 @@ test + + + + net.ltgt.gwt.maven + gwt-maven-plugin + + com.intendia.gwt.autorest + autorest + + + + 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 @@ + + + From 19ae850b2f47cf66214255f4c042763c14994595 Mon Sep 17 00:00:00 2001 From: Teodor Naydenov Date: Wed, 13 Feb 2019 16:49:26 +0200 Subject: [PATCH 8/8] Add Javadoc & @Generated annotation for generated source --- .../gwt/autorest/processor/AutoRestGwtProcessor.java | 7 ++++++- .../gwt/autorest/processor/AutoRestGwtProcessorTest.java | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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 e8d94ac..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 @@ -27,6 +27,7 @@ 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; @@ -124,11 +125,15 @@ 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) 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 371690d..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 @@ -27,8 +27,13 @@ public class AutoRestGwtProcessorTest { + "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"