From 83444b97e3cf1006b3a3ceb2f095eb9942747c91 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 5 Sep 2018 15:59:33 +1000 Subject: [PATCH 1/3] tmp --- pom.xml | 22 + rest-client/deployment/pom.xml | 27 ++ .../restclient/RestClientProcessor.java | 45 ++ .../shamrock/restclient/RestClientSetup.java | 11 + ...rg.jboss.shamrock.deployment.ShamrockSetup | 1 + rest-client/pom.xml | 18 + rest-client/runtime/pom.xml | 41 ++ .../restclient/runtime/BuilderResolver.java | 29 ++ .../runtime/ConfigurationWrapper.java | 110 +++++ .../DefaultResponseExceptionMapper.java | 43 ++ .../restclient/runtime/ExceptionMapping.java | 83 ++++ .../restclient/runtime/PartialResponse.java | 213 +++++++++ .../runtime/ProxyInvocationHandler.java | 259 +++++++++++ .../runtime/RestClientBuilderImpl.java | 406 ++++++++++++++++++ .../runtime/RestClientDelegateBean.java | 167 +++++++ .../restclient/runtime/RestClientProxy.java | 38 ++ ....rest.client.spi.RestClientBuilderResolver | 1 + 17 files changed, 1514 insertions(+) create mode 100644 rest-client/deployment/pom.xml create mode 100644 rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java create mode 100644 rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientSetup.java create mode 100644 rest-client/deployment/src/main/resources/META-INF/services/org.jboss.shamrock.deployment.ShamrockSetup create mode 100644 rest-client/pom.xml create mode 100644 rest-client/runtime/pom.xml create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/BuilderResolver.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ConfigurationWrapper.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ExceptionMapping.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/PartialResponse.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientDelegateBean.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java create mode 100644 rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver diff --git a/pom.xml b/pom.xml index 1f6320d94681b..c9fb235deeb99 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 7.6.0.Final 2.2 2.1.12 + 1.0 @@ -76,6 +77,7 @@ bean-validation transactions agroal + rest-client @@ -211,6 +213,16 @@ shamrock-junit ${project.version} + + org.jboss.shamrock + shamrock-rest-client-deployment + ${project.version} + + + org.jboss.shamrock + shamrock-rest-client-runtime + ${project.version} + org.jboss.shamrock shamrock-shared-library-example @@ -422,6 +434,11 @@ microprofile-config-api 1.3 + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + ${microprofile-rest-client-api.version} + org.fakereplace fakereplace @@ -490,6 +507,11 @@ resteasy-cdi ${resteasy.version} + + org.jboss.resteasy + resteasy-client + ${resteasy.version} + org.jboss.resteasy resteasy-jaxrs diff --git a/rest-client/deployment/pom.xml b/rest-client/deployment/pom.xml new file mode 100644 index 0000000000000..84d44e9eae1bc --- /dev/null +++ b/rest-client/deployment/pom.xml @@ -0,0 +1,27 @@ + + + + shamrock-rest-client + org.jboss.shamrock + 1.0.0.Alpha1-SNAPSHOT + ../ + + 4.0.0 + + shamrock-rest-client-deployment + + + + org.jboss.shamrock + shamrock-core-deployment + + + org.jboss.shamrock + shamrock-rest-client-runtime + + + + + \ No newline at end of file diff --git a/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java new file mode 100644 index 0000000000000..e19b21eaa7640 --- /dev/null +++ b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java @@ -0,0 +1,45 @@ +package org.jboss.shamrock.restclient; + +import java.lang.reflect.Modifier; + +import javax.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.shamrock.deployment.ArchiveContext; +import org.jboss.shamrock.deployment.BeanDeployment; +import org.jboss.shamrock.deployment.ProcessorContext; +import org.jboss.shamrock.deployment.ResourceProcessor; +import org.jboss.shamrock.deployment.ShamrockConfig; + +class RestClientProcessor implements ResourceProcessor { + + private static final DotName REGISTER_REST_CLIENT = DotName.createSimple(RegisterRestClient.class.getName()); + @Inject + private BeanDeployment beanDeployment; + + @Inject + private ShamrockConfig config; + + + @Override + public void process(ArchiveContext archiveContext, ProcessorContext processorContext) throws Exception { + int count = 0; + for(AnnotationInstance annotation : archiveContext.getCombinedIndex().getAnnotations(REGISTER_REST_CLIENT)) { + AnnotationTarget target = annotation.target(); + ClassInfo clazz = target.asClass(); + if(!Modifier.isInterface(clazz.flags())) { + throw new RuntimeException("RegisterRestClient can only be applied to interfaces: " + clazz.name()); + } + + } + } + + @Override + public int getPriority() { + return 1; + } +} diff --git a/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientSetup.java b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientSetup.java new file mode 100644 index 0000000000000..b6d1de4956782 --- /dev/null +++ b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientSetup.java @@ -0,0 +1,11 @@ +package org.jboss.shamrock.restclient; + +import org.jboss.shamrock.deployment.SetupContext; +import org.jboss.shamrock.deployment.ShamrockSetup; + +public class RestClientSetup implements ShamrockSetup { + @Override + public void setup(SetupContext context) { + context.addResourceProcessor(new RestClientProcessor()); + } +} diff --git a/rest-client/deployment/src/main/resources/META-INF/services/org.jboss.shamrock.deployment.ShamrockSetup b/rest-client/deployment/src/main/resources/META-INF/services/org.jboss.shamrock.deployment.ShamrockSetup new file mode 100644 index 0000000000000..819367933b7c5 --- /dev/null +++ b/rest-client/deployment/src/main/resources/META-INF/services/org.jboss.shamrock.deployment.ShamrockSetup @@ -0,0 +1 @@ +org.jboss.shamrock.restclient.RestClientSetup diff --git a/rest-client/pom.xml b/rest-client/pom.xml new file mode 100644 index 0000000000000..5fbe0177b86d0 --- /dev/null +++ b/rest-client/pom.xml @@ -0,0 +1,18 @@ + + + + shamrock-parent + org.jboss.shamrock + 1.0.0.Alpha1-SNAPSHOT + + 4.0.0 + + shamrock-rest-client + pom + + deployment + runtime + + \ No newline at end of file diff --git a/rest-client/runtime/pom.xml b/rest-client/runtime/pom.xml new file mode 100644 index 0000000000000..c8763d4850c4d --- /dev/null +++ b/rest-client/runtime/pom.xml @@ -0,0 +1,41 @@ + + + + shamrock-rest-client + org.jboss.shamrock + 1.0.0.Alpha1-SNAPSHOT + ../ + + 4.0.0 + + shamrock-rest-client-runtime + + + + org.jboss.shamrock + shamrock-core-runtime + + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + + + org.jboss.resteasy + resteasy-jaxrs + + + org.jboss.resteasy + resteasy-client + + + + + + + maven-dependency-plugin + + + + \ No newline at end of file diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/BuilderResolver.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/BuilderResolver.java new file mode 100644 index 0000000000000..e9b09d59e58d9 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/BuilderResolver.java @@ -0,0 +1,29 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver; + +/** + * Created by hbraun on 22.01.18. + */ +public class BuilderResolver extends RestClientBuilderResolver { + @Override + public RestClientBuilder newBuilder() { + return new RestClientBuilderImpl(); + } +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ConfigurationWrapper.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ConfigurationWrapper.java new file mode 100644 index 0000000000000..05c44163a8627 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ConfigurationWrapper.java @@ -0,0 +1,110 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; + +/** + * Created by hbraun on 22.01.18. + */ +class ConfigurationWrapper implements Configuration { + + public ConfigurationWrapper(Configuration delegate) { + this.delegate = delegate; + } + + @Override + public RuntimeType getRuntimeType() { + return delegate.getRuntimeType(); + } + + @Override + public Map getProperties() { + return delegate.getProperties(); + } + + @Override + public Object getProperty(String name) { + return delegate.getProperty(name); + } + + @Override + public Collection getPropertyNames() { + return delegate.getPropertyNames(); + } + + @Override + public boolean isEnabled(Feature feature) { + return delegate.isEnabled(feature); + } + + @Override + public boolean isEnabled(Class featureClass) { + return delegate.isEnabled(featureClass); + } + + @Override + public boolean isRegistered(Object component) { + return delegate.isRegistered(component); + } + + @Override + public boolean isRegistered(Class componentClass) { + return delegate.isRegistered(componentClass); + } + + @Override + public Map, Integer> getContracts(Class componentClass) { + Map, Integer> contracts = new HashMap<>(); + contracts.putAll(getLocalContracts(componentClass)); + contracts.putAll(delegate.getContracts(componentClass)); + return contracts; + } + + private Map, ? extends Integer> getLocalContracts(Class componentClass) { + if (localClassContracts.containsKey(componentClass)) { + return localClassContracts.get(componentClass); + } else { + return Collections.emptyMap(); + } + } + + @Override + public Set> getClasses() { + return delegate.getClasses(); + } + + @Override + public Set getInstances() { + return delegate.getInstances(); + } + + void registerLocalContract(Class provider, Map, Integer> contracts) { + localClassContracts.put(provider, contracts); + } + + protected Map, Map, Integer>> localClassContracts = new HashMap<>(); + + private final Configuration delegate; +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java new file mode 100644 index 0000000000000..24745b8fa19bb --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java @@ -0,0 +1,43 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +/** + * Created by hbraun on 17.01.18. + */ +class DefaultResponseExceptionMapper implements ResponseExceptionMapper { + + @Override + public Throwable toThrowable(Response response) { + return new WebApplicationException("Unknown error, status code " + response.getStatus(), response); + } + + @Override + public boolean handles(int status, MultivaluedMap headers) { + return status >= 400; + } + + @Override + public int getPriority() { + return Integer.MAX_VALUE; + } +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ExceptionMapping.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ExceptionMapping.java new file mode 100644 index 0000000000000..3f1ff6e76fd12 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ExceptionMapping.java @@ -0,0 +1,83 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +/** + * Created by hbraun on 22.01.18. + */ +class ExceptionMapping implements ClientResponseFilter { + + ExceptionMapping(Set instances) { + this.instances = instances; + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + + Response response = new PartialResponse(responseContext); + + Map mappers = new HashMap<>(); + for (Object o : instances) { + if (o instanceof ResponseExceptionMapper) { + ResponseExceptionMapper candiate = (ResponseExceptionMapper) o; + if (candiate.handles(response.getStatus(), response.getHeaders())) { + mappers.put(candiate, candiate.getPriority()); + } + } + } + + if (mappers.size() > 0) { + Map, Integer> errors = new HashMap<>(); + + mappers.forEach((m, i) -> { + Optional t = Optional.ofNullable(m.toThrowable(response)); + errors.put(t, i); + }); + + Optional prioritised = Optional.empty(); + for (Optional throwable : errors.keySet()) { + if (throwable.isPresent()) { + if (!prioritised.isPresent()) { + prioritised = throwable; + } else if (errors.get(throwable) < errors.get(prioritised)) { + prioritised = throwable; + } + } + } + + if (prioritised.isPresent()) { // strange rule from the spec + throw (WebApplicationException) prioritised.get(); + } + } + + } + + private Set instances; +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/PartialResponse.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/PartialResponse.java new file mode 100644 index 0000000000000..2b32472df4dc8 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/PartialResponse.java @@ -0,0 +1,213 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.specimpl.MultivaluedMapImpl; + + +/** + * Created by hbraun on 22.01.18. + */ +public class PartialResponse extends Response implements Serializable { + + PartialResponse(ClientResponseContext responseContext) { + this.responseContext = responseContext; + } + + @Override + public int getStatus() { + return responseContext.getStatus(); + } + + @Override + public StatusType getStatusInfo() { + return responseContext.getStatusInfo(); + } + + @Override + public Object getEntity() { + throw notSupported(); + } + + private RuntimeException notSupported() { + RuntimeException ex = new RuntimeException("method call not supported"); + ex.printStackTrace(); + return ex; + } + + + @Override + public T readEntity(Class entityType) { + + if (entityType.isAssignableFrom(String.class)) { + return (T) readStringEntity(responseContext.getEntityStream()); + } else { + throw notSupported(); + } + } + + public static String readStringEntity(InputStream input) { + try { + try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { + return buffer.lines().collect(Collectors.joining("\n")); + } + } catch (IOException e) { + throw new WebApplicationException("Failed to read entity", e); + } + } + + + @Override + public T readEntity(GenericType entityType) { + throw notSupported(); + } + + @Override + public T readEntity(Class entityType, Annotation[] annotations) { + throw notSupported(); + } + + @Override + public T readEntity(GenericType entityType, Annotation[] annotations) { + throw notSupported(); + } + + @Override + public boolean hasEntity() { + return responseContext.hasEntity(); + } + + @Override + public boolean bufferEntity() { + throw new RuntimeException("method call not supported"); + } + + @Override + public void close() { + try { + responseContext.getEntityStream().close(); + } catch (Throwable e) { + // ignore + } + } + + @Override + public MediaType getMediaType() { + return responseContext.getMediaType(); + } + + @Override + public Locale getLanguage() { + return responseContext.getLanguage(); + } + + @Override + public int getLength() { + return responseContext.getLength(); + } + + @Override + public Set getAllowedMethods() { + return responseContext.getAllowedMethods(); + } + + @Override + public Map getCookies() { + return responseContext.getCookies(); + } + + @Override + public EntityTag getEntityTag() { + return responseContext.getEntityTag(); + } + + @Override + public Date getDate() { + return responseContext.getDate(); + } + + @Override + public Date getLastModified() { + return responseContext.getLastModified(); + } + + @Override + public URI getLocation() { + return responseContext.getLocation(); + } + + @Override + public Set getLinks() { + return responseContext.getLinks(); + } + + @Override + public boolean hasLink(String relation) { + return responseContext.hasLink(relation); + } + + @Override + public Link getLink(String relation) { + return responseContext.getLink(relation); + } + + @Override + public Link.Builder getLinkBuilder(String relation) { + throw new RuntimeException("method call not supported"); + } + + @Override + public MultivaluedMap getMetadata() { + MultivaluedMap metaData = new MultivaluedMapImpl(); + // TODO + return metaData; + } + + @Override + public MultivaluedMap getStringHeaders() { + return responseContext.getHeaders(); + } + + @Override + public String getHeaderString(String name) { + return responseContext.getHeaderString(name); + } + + private final transient ClientResponseContext responseContext; +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java new file mode 100644 index 0000000000000..800e1749fc6ff --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java @@ -0,0 +1,259 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.InterceptionType; +import javax.enterprise.inject.spi.Interceptor; +import javax.ws.rs.client.ResponseProcessingException; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; + +import RestClientProxy; + +/** + * Created by hbraun on 22.01.18. + */ +class ProxyInvocationHandler implements InvocationHandler { + + private static final Logger LOGGER = Logger.getLogger(ProxyInvocationHandler.class); + + private final Object target; + + private final Set providerInstances; + + private final Map> interceptorChains; + + private final ResteasyClient client; + + private final CreationalContext creationalContext; + + private final AtomicBoolean closed; + + public ProxyInvocationHandler(Class restClientInterface, Object target, Set providerInstances, ResteasyClient client) { + this.target = target; + this.providerInstances = providerInstances; + this.client = client; + this.closed = new AtomicBoolean(); + BeanManager beanManager = getBeanManager(restClientInterface); + if (beanManager != null) { + this.creationalContext = beanManager.createCreationalContext(null); + this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface); + } else { + this.creationalContext = null; + this.interceptorChains = Collections.emptyMap(); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (RestClientProxy.class.equals(method.getDeclaringClass())) { + return invokeRestClientProxyMethod(proxy, method, args); + } + if (closed.get()) { + throw new IllegalStateException("RestClientProxy is closed"); + } + + boolean replacementNeeded = false; + Object[] argsReplacement = args != null ? new Object[args.length] : null; + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + for (Object p : providerInstances) { + if (p instanceof ParamConverterProvider) { + + int index = 0; + for (Object arg : args) { + + if (parameterAnnotations[index].length > 0) { // does a parameter converter apply? + + ParamConverter converter = ((ParamConverterProvider) p).getConverter(arg.getClass(), null, parameterAnnotations[index]); + if (converter != null) { + Type[] genericTypes = getGenericTypes(converter.getClass()); + if (genericTypes.length == 1) { + + // minimum supported types + switch (genericTypes[0].getTypeName()) { + case "java.lang.String": + ParamConverter stringConverter = (ParamConverter) converter; + argsReplacement[index] = stringConverter.toString((String) arg); + replacementNeeded = true; + break; + case "java.lang.Integer": + ParamConverter intConverter = (ParamConverter) converter; + argsReplacement[index] = intConverter.toString((Integer) arg); + replacementNeeded = true; + break; + case "java.lang.Boolean": + ParamConverter boolConverter = (ParamConverter) converter; + argsReplacement[index] = boolConverter.toString((Boolean) arg); + replacementNeeded = true; + break; + default: + continue; + } + } + } + } else { + argsReplacement[index] = arg; + } + index++; + } + } + } + + if (replacementNeeded) { + args = argsReplacement; + } + + List chain = interceptorChains.get(method); + if (chain != null) { + // Invoke business method interceptors + return new InvocationContextImpl(target, method, args, chain).proceed(); + } else { + try { + return method.invoke(target, args); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof ResponseProcessingException) { + ResponseProcessingException rpe = (ResponseProcessingException) e.getCause(); + Throwable cause = rpe.getCause(); + if (cause instanceof RuntimeException) { + throw cause; + } + } + throw e; + } + } + } + + private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "getClient": + return client; + case "close": + close(); + return null; + default: + throw new IllegalStateException("Unsupported RestClientProxy method: " + method); + } + } + + private void close() { + if (closed.compareAndSet(false, true)) { + if (creationalContext != null) { + creationalContext.release(); + } + client.close(); + } + } + + private Type[] getGenericTypes(Class aClass) { + Type[] genericInterfaces = aClass.getGenericInterfaces(); + Type[] genericTypes = new Type[] {}; + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + genericTypes = ((ParameterizedType) genericInterface).getActualTypeArguments(); + } + } + return genericTypes; + } + + private static List getBindings(Annotation[] annotations, BeanManager beanManager) { + if (annotations.length == 0) { + return Collections.emptyList(); + } + List bindings = new ArrayList<>(); + for (Annotation annotation : annotations) { + if (beanManager.isInterceptorBinding(annotation.annotationType())) { + bindings.add(annotation); + } + } + return bindings; + } + + private static BeanManager getBeanManager(Class restClientInterface) { + try { + return CDI.current().getBeanManager(); + } catch (IllegalStateException e) { + LOGGER.warnf("CDI container is not available - interceptor bindings declared on %s will be ignored", restClientInterface.getSimpleName()); + return null; + } + } + + private static Map> initInterceptorChains(BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface) { + + Map> chains = new HashMap<>(); + // Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases + Map, Object> interceptorInstances = new HashMap<>(); + + List classLevelBindings = getBindings(restClientInterface.getAnnotations(), beanManager); + + for (Method method : restClientInterface.getMethods()) { + if (method.isDefault() || Modifier.isStatic(method.getModifiers())) { + continue; + } + List methodLevelBindings = getBindings(method.getAnnotations(), beanManager); + + if (!classLevelBindings.isEmpty() || !methodLevelBindings.isEmpty()) { + + Annotation[] interceptorBindings = merge(methodLevelBindings, classLevelBindings); + + List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE, interceptorBindings); + if (!interceptors.isEmpty()) { + List chain = new ArrayList<>(); + for (Interceptor interceptor : interceptors) { + chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor, + interceptorInstances.computeIfAbsent(interceptor, i -> beanManager.getReference(i, i.getBeanClass(), creationalContext)))); + } + chains.put(method, chain); + } + } + } + return chains.isEmpty() ? Collections.emptyMap() : chains; + } + + private static Annotation[] merge(List methodLevelBindings, List classLevelBindings) { + Set> types = methodLevelBindings.stream().map(a -> a.annotationType()).collect(Collectors.toSet()); + List merged = new ArrayList<>(methodLevelBindings); + for (Annotation annotation : classLevelBindings) { + if (!types.contains(annotation.annotationType())) { + merged.add(annotation); + } + } + return merged.toArray(new Annotation[] {}); + } + +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java new file mode 100644 index 0000000000000..708ad2893ce4f --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java @@ -0,0 +1,406 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.logging.Logger; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.specimpl.ResteasyUriBuilder; + + +/** + * Created by hbraun on 15.01.18. + */ +class RestClientBuilderImpl implements RestClientBuilder { + + private static final Logger LOGGER = Logger.getLogger(RestClientBuilderImpl.class); + + private static final String RESTEASY_PROPERTY_PREFIX = "resteasy."; + + private static final String DEFAULT_MAPPER_PROP = "microprofile.rest.client.disable.default.mapper"; + + RestClientBuilderImpl() { + ClientBuilder availableBuilder = ClientBuilder.newBuilder(); + + if (availableBuilder instanceof ResteasyClientBuilder) { + this.builderDelegate = (ResteasyClientBuilder) availableBuilder; + this.configurationWrapper = new ConfigurationWrapper(this.builderDelegate.getConfiguration()); + this.config = ConfigProvider.getConfig(); + } else { + throw new IllegalStateException("Incompatible client builder found " + availableBuilder.getClass()); + } + } + + public Configuration getConfigurationWrapper() { + return this.configurationWrapper; + } + + @Override + public RestClientBuilder baseUrl(URL url) { + try { + this.baseURI = url.toURI(); + return this; + } catch (URISyntaxException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + @Override + public T build(Class aClass) throws IllegalStateException, RestClientDefinitionException { + + // Interface validity + verifyInterface(aClass); + + // Provider annotations + RegisterProvider[] providers = aClass.getAnnotationsByType(RegisterProvider.class); + + for (RegisterProvider provider : providers) { + register(provider.value(), provider.priority()); + } + + // Default exception mapper + if (!isMapperDisabled()) { + register(DefaultResponseExceptionMapper.class); + } + + this.builderDelegate.register(new ExceptionMapping(localProviderInstances), 1); + + ClassLoader classLoader = aClass.getClassLoader(); + + List noProxyHosts = Arrays.asList( + System.getProperty("http.nonProxyHosts", "localhost|127.*|[::1]").split("|")); + String proxyHost = System.getProperty("http.proxyHost"); + + T actualClient; + ResteasyClient client; + + if (proxyHost != null && !noProxyHosts.contains(this.baseURI.getHost())) { + // Use proxy, if defined + client = this.builderDelegate.defaultProxy( + proxyHost, + Integer.parseInt(System.getProperty("http.proxyPort", "80"))) + .build(); + } else { + client = this.builderDelegate.build(); + } + + actualClient = client.target(this.baseURI) + .proxyBuilder(aClass) + .classloader(classLoader) + .defaultConsumes(MediaType.TEXT_PLAIN) + .defaultProduces(MediaType.TEXT_PLAIN).build(); + + Class[] interfaces = new Class[2]; + interfaces[0] = aClass; + interfaces[1] = RestClientProxy.class; + + return (T) Proxy.newProxyInstance(classLoader, interfaces, new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client)); + } + + private boolean isMapperDisabled() { + boolean disabled = false; + Optional defaultMapperProp = this.config.getOptionalValue(DEFAULT_MAPPER_PROP, Boolean.class); + + // disabled through config api + if (defaultMapperProp.isPresent() && defaultMapperProp.get().equals(Boolean.TRUE)) { + disabled = true; + } else if (!defaultMapperProp.isPresent()) { + + // disabled through jaxrs property + try { + Object property = this.builderDelegate.getConfiguration().getProperty(DEFAULT_MAPPER_PROP); + if (property != null) { + disabled = (Boolean) property; + } + } catch (Throwable e) { + // ignore cast exception + } + } + return disabled; + } + + private void verifyInterface(Class typeDef) { + + Method[] methods = typeDef.getMethods(); + + // multiple verbs + for (Method method : methods) { + boolean hasHttpMethod = false; + for (Annotation annotation : method.getAnnotations()) { + boolean isHttpMethod = (annotation.annotationType().getAnnotation(HttpMethod.class) != null); + if (!hasHttpMethod && isHttpMethod) { + hasHttpMethod = true; + } else if (hasHttpMethod && isHttpMethod) { + throw new RestClientDefinitionException("Ambiguous @Httpmethod defintion on type " + typeDef); + } + } + } + + // invalid parameter + Path classPathAnno = typeDef.getAnnotation(Path.class); + + final Set classLevelVariables = new HashSet<>(); + ResteasyUriBuilder classTemplate = null; + if (classPathAnno != null) { + classTemplate = (ResteasyUriBuilder) UriBuilder.fromUri(classPathAnno.value()); + classLevelVariables.addAll(classTemplate.getPathParamNamesInDeclarationOrder()); + } + ResteasyUriBuilder template; + for (Method method : methods) { + + Path methodPathAnno = method.getAnnotation(Path.class); + if (methodPathAnno != null) { + template = classPathAnno == null ? (ResteasyUriBuilder) UriBuilder.fromUri(methodPathAnno.value()) + : (ResteasyUriBuilder) UriBuilder.fromUri(classPathAnno.value() + "/" + methodPathAnno.value()); + } else { + template = classTemplate; + } + if (template == null) { + continue; + } + + // it's not executed, so this can be anything - but a hostname needs to present + template.host("localhost"); + + Set allVariables = new HashSet<>(template.getPathParamNamesInDeclarationOrder()); + Map paramMap = new HashMap<>(); + for (Parameter p : method.getParameters()) { + PathParam pathParam = p.getAnnotation(PathParam.class); + if (pathParam != null) { + paramMap.put(pathParam.value(), "foobar"); + } + } + + if (allVariables.size() != paramMap.size()) { + throw new RestClientDefinitionException("Parameters and variables don't match on " + typeDef + "::" + method.getName()); + } + + try { + template.resolveTemplates(paramMap, false).build(); + } catch (IllegalArgumentException ex) { + throw new RestClientDefinitionException("Parameter names don't match variable names on " + typeDef + "::" + method.getName(), ex); + } + + } + } + + + @Override + public Configuration getConfiguration() { + return getConfigurationWrapper(); + } + + @Override + public RestClientBuilder property(String name, Object value) { + if (name.startsWith(RESTEASY_PROPERTY_PREFIX)) { + // Allows to configure some of the ResteasyClientBuilder delegate properties + String builderMethodName = name.substring(RESTEASY_PROPERTY_PREFIX.length()); + try { + Method builderMethod = ResteasyClientBuilder.class.getMethod(builderMethodName, unwrapPrimitiveType(value)); + builderMethod.invoke(builderDelegate, value); + } catch (NoSuchMethodException e) { + LOGGER.warnf("ResteasyClientBuilder method %s not found", builderMethodName); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + LOGGER.errorf(e, "Unable to invoke ResteasyClientBuilder method %s", builderMethodName); + } + } + this.builderDelegate.property(name, value); + return this; + } + + private static Class unwrapPrimitiveType(Object value) { + if (value instanceof Integer) { + return int.class; + } else if (value instanceof Long) { + return long.class; + } else if (value instanceof Boolean) { + return boolean.class; + } + return value.getClass(); + } + + private static Object newInstanceOf(Class clazz) { + try { + return clazz.newInstance(); + } catch (Throwable t) { + throw new RuntimeException("Failed to register " + clazz, t); + } + } + + @Override + public RestClientBuilder register(Class aClass) { + this.register(newInstanceOf(aClass)); + return this; + } + + @Override + public RestClientBuilder register(Class aClass, int i) { + + this.register(newInstanceOf(aClass), i); + return this; + } + + @Override + public RestClientBuilder register(Class aClass, Class[] classes) { + this.register(newInstanceOf(aClass), classes); + return this; + } + + @Override + public RestClientBuilder register(Class aClass, Map, Integer> map) { + this.register(newInstanceOf(aClass), map); + return this; + } + + @Override + public RestClientBuilder register(Object o) { + if (o instanceof ResponseExceptionMapper) { + ResponseExceptionMapper mapper = (ResponseExceptionMapper) o; + register(mapper, mapper.getPriority()); + } else if (o instanceof ParamConverterProvider) { + register(o, Priorities.USER); + } else { + this.builderDelegate.register(o); + } + return this; + } + + @Override + public RestClientBuilder register(Object o, int i) { + if (o instanceof ResponseExceptionMapper) { + + // local + ResponseExceptionMapper mapper = (ResponseExceptionMapper) o; + HashMap, Integer> contracts = new HashMap<>(); + contracts.put(ResponseExceptionMapper.class, i); + registerLocalProviderInstance(mapper, contracts); + + // delegate + this.builderDelegate.register(mapper, i); + + } else if (o instanceof ParamConverterProvider) { + + // local + ParamConverterProvider converter = (ParamConverterProvider) o; + HashMap, Integer> contracts = new HashMap<>(); + contracts.put(ParamConverterProvider.class, i); + registerLocalProviderInstance(converter, contracts); + + // delegate + this.builderDelegate.register(converter, i); + + } else { + this.builderDelegate.register(o, i); + } + return this; + } + + @Override + public RestClientBuilder register(Object o, Class[] classes) { + + // local + for (Class aClass : classes) { + if (aClass.isAssignableFrom(ResponseExceptionMapper.class)) { + register(o); + } + } + + // other + this.builderDelegate.register(o, classes); + return this; + } + + @Override + public RestClientBuilder register(Object o, Map, Integer> map) { + + + if (o instanceof ResponseExceptionMapper) { + + //local + ResponseExceptionMapper mapper = (ResponseExceptionMapper) o; + HashMap, Integer> contracts = new HashMap<>(); + contracts.put(ResponseExceptionMapper.class, map.get(ResponseExceptionMapper.class)); + registerLocalProviderInstance(mapper, contracts); + + // other + this.builderDelegate.register(o, map); + + } else { + this.builderDelegate.register(o, map); + } + + return this; + } + + public Set getLocalProviderInstances() { + return localProviderInstances; + } + + public void registerLocalProviderInstance(Object provider, Map, Integer> contracts) { + for (Object registered : getLocalProviderInstances()) { + if (registered == provider) { + System.out.println("Provider already registered " + provider.getClass().getName()); + return; + } + } + + localProviderInstances.add(provider); + configurationWrapper.registerLocalContract(provider.getClass(), contracts); + } + + private final ResteasyClientBuilder builderDelegate; + + private final ConfigurationWrapper configurationWrapper; + + private final Config config; + + private URI baseURI; + + private Set localProviderInstances = new HashSet(); +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientDelegateBean.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientDelegateBean.java new file mode 100644 index 0000000000000..5a1a9f613a602 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientDelegateBean.java @@ -0,0 +1,167 @@ +/** + * Copyright 2015-2017 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.enterprise.context.Dependent; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.PassivationCapable; +import javax.enterprise.util.AnnotationLiteral; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +public class RestClientDelegateBean implements Bean, PassivationCapable { + + public static final String REST_URL_FORMAT = "%s/mp-rest/url"; + + public static final String REST_SCOPE_FORMAT = "%s/mp-rest/scope"; + + private final Class proxyType; + + private final Class scope; + + private final BeanManager beanManager; + + private final Config config; + + RestClientDelegateBean(Class proxyType, BeanManager beanManager) { + this.proxyType = proxyType; + this.beanManager = beanManager; + this.config = ConfigProvider.getConfig(); + this.scope = this.resolveScope(); + } + @Override + public String getId() { + return proxyType.getName(); + } + + @Override + public Class getBeanClass() { + return proxyType; + } + + @Override + public Set getInjectionPoints() { + return Collections.emptySet(); + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public Object create(CreationalContext creationalContext) { + RestClientBuilder builder = RestClientBuilder.newBuilder(); + String baseUrl = getBaseUrl(); + try { + return builder.baseUrl(new URL(baseUrl)).build(proxyType); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("The value of URL was invalid " + baseUrl); + } + } + + @Override + public void destroy(Object instance, CreationalContext creationalContext) { + + } + + @Override + public Set getTypes() { + return Collections.singleton(proxyType); + } + + @Override + public Set getQualifiers() { + Set qualifiers = new HashSet(); + qualifiers.add(new AnnotationLiteral() { }); + qualifiers.add(new AnnotationLiteral() { }); + qualifiers.add(RestClient.LITERAL); + return qualifiers; + } + + @Override + public Class getScope() { + return scope; + } + + @Override + public String getName() { + return proxyType.getName(); + } + + @Override + public Set> getStereotypes() { + return Collections.emptySet(); + } + + @Override + public boolean isAlternative() { + return false; + } + + private String getBaseUrl() { + String property = String.format(REST_URL_FORMAT, proxyType.getName()); + return config.getValue(property, String.class); + } + + private Class resolveScope() { + + String property = String.format(REST_SCOPE_FORMAT, proxyType.getName()); + String configuredScope = config.getOptionalValue(property, String.class).orElse(null); + + if (configuredScope != null) { + try { + return (Class) Class.forName(configuredScope); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid scope: " + configuredScope, e); + } + } + + List possibleScopes = new ArrayList<>(); + Annotation[] annotations = proxyType.getDeclaredAnnotations(); + for (Annotation annotation : annotations) { + if (beanManager.isScope(annotation.annotationType())) { + possibleScopes.add(annotation); + } + } + if (possibleScopes.isEmpty()) { + return Dependent.class; + } else if (possibleScopes.size() == 1) { + return possibleScopes.get(0).annotationType(); + } else { + throw new IllegalArgumentException("Ambiguous scope definition on " + proxyType + ": " + possibleScopes); + } + } + +} \ No newline at end of file diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java new file mode 100644 index 0000000000000..792e8ed2d6fba --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java @@ -0,0 +1,38 @@ +/** + * Copyright 2018 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.shamrock.restclient.runtime; + +import javax.ws.rs.client.Client; + +/** + * This interface is implemented by every proxy created by {@link io.smallrye.restclient.RestClientBuilderImpl}. + * + * @author Martin Kouba + */ +public interface RestClientProxy { + + /** + * Release/close all associated resources, including the underlying {@link Client} instance. + */ + void close(); + + /** + * + * @return the underlying {@link Client} instance + */ + Client getClient(); + +} diff --git a/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver b/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver new file mode 100644 index 0000000000000..994bfa3c819b5 --- /dev/null +++ b/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver @@ -0,0 +1 @@ +io.smallrye.restclient.BuilderResolver From d9883c5f089572b2a5f1c9bf7e9dd746fb62b0f0 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Thu, 6 Sep 2018 16:09:09 +1000 Subject: [PATCH 2/3] tmp --- examples/strict/pom.xml | 4 + .../shamrock/example/rest/RestInterface.java | 12 + .../shamrock/example/rest/TestResource.java | 2 + .../runtime/ProxyInvocationHandler.java | 259 ------------------ .../runtime/RestClientBuilderImpl.java | 8 +- 5 files changed, 25 insertions(+), 260 deletions(-) create mode 100644 examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java delete mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java diff --git a/examples/strict/pom.xml b/examples/strict/pom.xml index 17db2fdf66d8f..0d16d5495f6e1 100644 --- a/examples/strict/pom.xml +++ b/examples/strict/pom.xml @@ -86,6 +86,10 @@ io.reactivex.rxjava2 rxjava + + org.jboss.shamrock + shamrock-rest-client-deployment + diff --git a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java new file mode 100644 index 0000000000000..6b598105019c8 --- /dev/null +++ b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java @@ -0,0 +1,12 @@ +package org.jboss.shamrock.example.rest; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/foo") +public interface RestInterface { + + @GET + void get(); + +} diff --git a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java index c81a17c737549..48bcc2be07497 100644 --- a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java +++ b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java @@ -9,12 +9,14 @@ import javax.xml.bind.annotation.XmlRootElement; import io.reactivex.Single; +import org.eclipse.microprofile.rest.client.RestClientBuilder; @Path("/test") public class TestResource { @GET public String getTest() { + RestInterface iface = RestClientBuilder.newBuilder().build(RestInterface.class); return "TEST"; } diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java deleted file mode 100644 index 800e1749fc6ff..0000000000000 --- a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/ProxyInvocationHandler.java +++ /dev/null @@ -1,259 +0,0 @@ -/** - * Copyright 2015-2017 Red Hat, Inc, and individual contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jboss.shamrock.restclient.runtime; - -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -import javax.enterprise.context.spi.CreationalContext; -import javax.enterprise.inject.spi.BeanManager; -import javax.enterprise.inject.spi.CDI; -import javax.enterprise.inject.spi.InterceptionType; -import javax.enterprise.inject.spi.Interceptor; -import javax.ws.rs.client.ResponseProcessingException; -import javax.ws.rs.ext.ParamConverter; -import javax.ws.rs.ext.ParamConverterProvider; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; - -import RestClientProxy; - -/** - * Created by hbraun on 22.01.18. - */ -class ProxyInvocationHandler implements InvocationHandler { - - private static final Logger LOGGER = Logger.getLogger(ProxyInvocationHandler.class); - - private final Object target; - - private final Set providerInstances; - - private final Map> interceptorChains; - - private final ResteasyClient client; - - private final CreationalContext creationalContext; - - private final AtomicBoolean closed; - - public ProxyInvocationHandler(Class restClientInterface, Object target, Set providerInstances, ResteasyClient client) { - this.target = target; - this.providerInstances = providerInstances; - this.client = client; - this.closed = new AtomicBoolean(); - BeanManager beanManager = getBeanManager(restClientInterface); - if (beanManager != null) { - this.creationalContext = beanManager.createCreationalContext(null); - this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface); - } else { - this.creationalContext = null; - this.interceptorChains = Collections.emptyMap(); - } - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (RestClientProxy.class.equals(method.getDeclaringClass())) { - return invokeRestClientProxyMethod(proxy, method, args); - } - if (closed.get()) { - throw new IllegalStateException("RestClientProxy is closed"); - } - - boolean replacementNeeded = false; - Object[] argsReplacement = args != null ? new Object[args.length] : null; - Annotation[][] parameterAnnotations = method.getParameterAnnotations(); - - for (Object p : providerInstances) { - if (p instanceof ParamConverterProvider) { - - int index = 0; - for (Object arg : args) { - - if (parameterAnnotations[index].length > 0) { // does a parameter converter apply? - - ParamConverter converter = ((ParamConverterProvider) p).getConverter(arg.getClass(), null, parameterAnnotations[index]); - if (converter != null) { - Type[] genericTypes = getGenericTypes(converter.getClass()); - if (genericTypes.length == 1) { - - // minimum supported types - switch (genericTypes[0].getTypeName()) { - case "java.lang.String": - ParamConverter stringConverter = (ParamConverter) converter; - argsReplacement[index] = stringConverter.toString((String) arg); - replacementNeeded = true; - break; - case "java.lang.Integer": - ParamConverter intConverter = (ParamConverter) converter; - argsReplacement[index] = intConverter.toString((Integer) arg); - replacementNeeded = true; - break; - case "java.lang.Boolean": - ParamConverter boolConverter = (ParamConverter) converter; - argsReplacement[index] = boolConverter.toString((Boolean) arg); - replacementNeeded = true; - break; - default: - continue; - } - } - } - } else { - argsReplacement[index] = arg; - } - index++; - } - } - } - - if (replacementNeeded) { - args = argsReplacement; - } - - List chain = interceptorChains.get(method); - if (chain != null) { - // Invoke business method interceptors - return new InvocationContextImpl(target, method, args, chain).proceed(); - } else { - try { - return method.invoke(target, args); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof ResponseProcessingException) { - ResponseProcessingException rpe = (ResponseProcessingException) e.getCause(); - Throwable cause = rpe.getCause(); - if (cause instanceof RuntimeException) { - throw cause; - } - } - throw e; - } - } - } - - private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[] args) throws Throwable { - switch (method.getName()) { - case "getClient": - return client; - case "close": - close(); - return null; - default: - throw new IllegalStateException("Unsupported RestClientProxy method: " + method); - } - } - - private void close() { - if (closed.compareAndSet(false, true)) { - if (creationalContext != null) { - creationalContext.release(); - } - client.close(); - } - } - - private Type[] getGenericTypes(Class aClass) { - Type[] genericInterfaces = aClass.getGenericInterfaces(); - Type[] genericTypes = new Type[] {}; - for (Type genericInterface : genericInterfaces) { - if (genericInterface instanceof ParameterizedType) { - genericTypes = ((ParameterizedType) genericInterface).getActualTypeArguments(); - } - } - return genericTypes; - } - - private static List getBindings(Annotation[] annotations, BeanManager beanManager) { - if (annotations.length == 0) { - return Collections.emptyList(); - } - List bindings = new ArrayList<>(); - for (Annotation annotation : annotations) { - if (beanManager.isInterceptorBinding(annotation.annotationType())) { - bindings.add(annotation); - } - } - return bindings; - } - - private static BeanManager getBeanManager(Class restClientInterface) { - try { - return CDI.current().getBeanManager(); - } catch (IllegalStateException e) { - LOGGER.warnf("CDI container is not available - interceptor bindings declared on %s will be ignored", restClientInterface.getSimpleName()); - return null; - } - } - - private static Map> initInterceptorChains(BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface) { - - Map> chains = new HashMap<>(); - // Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases - Map, Object> interceptorInstances = new HashMap<>(); - - List classLevelBindings = getBindings(restClientInterface.getAnnotations(), beanManager); - - for (Method method : restClientInterface.getMethods()) { - if (method.isDefault() || Modifier.isStatic(method.getModifiers())) { - continue; - } - List methodLevelBindings = getBindings(method.getAnnotations(), beanManager); - - if (!classLevelBindings.isEmpty() || !methodLevelBindings.isEmpty()) { - - Annotation[] interceptorBindings = merge(methodLevelBindings, classLevelBindings); - - List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE, interceptorBindings); - if (!interceptors.isEmpty()) { - List chain = new ArrayList<>(); - for (Interceptor interceptor : interceptors) { - chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor, - interceptorInstances.computeIfAbsent(interceptor, i -> beanManager.getReference(i, i.getBeanClass(), creationalContext)))); - } - chains.put(method, chain); - } - } - } - return chains.isEmpty() ? Collections.emptyMap() : chains; - } - - private static Annotation[] merge(List methodLevelBindings, List classLevelBindings) { - Set> types = methodLevelBindings.stream().map(a -> a.annotationType()).collect(Collectors.toSet()); - List merged = new ArrayList<>(methodLevelBindings); - for (Annotation annotation : classLevelBindings) { - if (!types.contains(annotation.annotationType())) { - merged.add(annotation); - } - } - return merged.toArray(new Annotation[] {}); - } - -} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java index 708ad2893ce4f..4d6f4f685843c 100644 --- a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java @@ -16,6 +16,7 @@ package org.jboss.shamrock.restclient.runtime; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -140,7 +141,12 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi interfaces[0] = aClass; interfaces[1] = RestClientProxy.class; - return (T) Proxy.newProxyInstance(classLoader, interfaces, new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client)); + return (T) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return method.invoke(actualClient, args); + } + }); } private boolean isMapperDisabled() { From 73e5636878c0c343926cfc0a7c749ffeb4dfd8ce Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 7 Sep 2018 09:04:05 +1000 Subject: [PATCH 3/3] tmp --- .../deployment/BuildTimeGenerator.java | 27 ++++- .../shamrock/deployment/ProcessorContext.java | 2 + examples/strict/pom.xml | 2 + .../shamrock/example/rest/RestInterface.java | 4 +- .../shamrock/example/rest/TestResource.java | 16 ++- .../example/test/RestClientITCase.java | 9 ++ .../example/test/RestClientTestCase.java | 29 +++++ jaxrs/deployment/pom.xml | 4 + .../jboss/shamrock/maven/NativeImageMojo.java | 7 ++ .../restclient/RestClientProcessor.java | 57 ++++++++-- rest-client/runtime/pom.xml | 4 + .../DefaultResponseExceptionMapper.java | 2 +- .../runtime/RestClientBuilderImpl.java | 101 +++++++++++++++++- .../restclient/runtime/RestClientProxy.java | 4 - .../graal/ClientBuilderReplacement.java | 17 +++ .../RestClientBuilderResolverReplacement.java | 16 +++ ....rest.client.spi.RestClientBuilderResolver | 2 +- 17 files changed, 280 insertions(+), 23 deletions(-) create mode 100644 examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientITCase.java create mode 100644 examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientTestCase.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/ClientBuilderReplacement.java create mode 100644 rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/RestClientBuilderResolverReplacement.java diff --git a/core/deployment/src/main/java/org/jboss/shamrock/deployment/BuildTimeGenerator.java b/core/deployment/src/main/java/org/jboss/shamrock/deployment/BuildTimeGenerator.java index fbe5166fc48ba..bf3024aa3e880 100644 --- a/core/deployment/src/main/java/org/jboss/shamrock/deployment/BuildTimeGenerator.java +++ b/core/deployment/src/main/java/org/jboss/shamrock/deployment/BuildTimeGenerator.java @@ -26,8 +26,8 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -37,7 +37,6 @@ import org.jboss.jandex.Indexer; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; -import org.jboss.protean.gizmo.BytecodeCreator; import org.jboss.protean.gizmo.CatchBlockCreator; import org.jboss.protean.gizmo.ClassCreator; import org.jboss.protean.gizmo.ExceptionTable; @@ -169,6 +168,7 @@ private final class ProcessorContextImpl implements ProcessorContext { private final Set resources = new HashSet<>(); private final Set resourceBundles = new HashSet<>(); private final Set runtimeInitializedClasses = new HashSet<>(); + private final Set> proxyClasses = new HashSet<>(); @Override public BytecodeRecorder addStaticInitTask(int priority) { @@ -256,6 +256,12 @@ public void addRuntimeInitializedClasses(String... classes) { runtimeInitializedClasses.addAll(Arrays.asList(classes)); } + @Override + public void addProxyDefinition(String... proxyClasses) { + this.proxyClasses.add(Arrays.asList(proxyClasses)); + } + + void writeMainClass() throws IOException { Collections.sort(tasks); @@ -313,7 +319,7 @@ void writeReflectionAutoFeature() throws IOException { //TODO: at some point we are going to need to break this up, as if it get too big it will hit the method size limit - if(!runtimeInitializedClasses.isEmpty()) { + if (!runtimeInitializedClasses.isEmpty()) { ExceptionTable tc = beforeAn.addTryCatch(); ResultHandle array = beforeAn.newArray(Class.class, beforeAn.load(runtimeInitializedClasses.size())); int count = 0; @@ -330,6 +336,21 @@ void writeReflectionAutoFeature() throws IOException { tc.complete(); } + if (!proxyClasses.isEmpty()) { + ResultHandle proxySupportClass = beforeAn.loadClass("com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry"); + ResultHandle proxySupport = beforeAn.invokeStaticMethod(ofMethod("org.graalvm.nativeimage.ImageSingletons", "lookup", Object.class, Class.class), proxySupportClass); + for (List proxy : proxyClasses) { + ResultHandle array = beforeAn.newArray(Class.class, beforeAn.load(proxy.size())); + int i = 0; + for (String p : proxy) { + ResultHandle clazz = beforeAn.invokeStaticMethod(ofMethod(Class.class, "forName", Class.class, String.class), beforeAn.load(p)); + beforeAn.writeArrayValue(array, beforeAn.load(i++), clazz); + + } + beforeAn.invokeInterfaceMethod(ofMethod("com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry", "addProxyClass", void.class, Class[].class), proxySupport, array); + } + } + for (String i : resources) { beforeAn.invokeStaticMethod(ofMethod(ResourceHelper.class, "registerResources", void.class, String.class), beforeAn.load(i)); } diff --git a/core/deployment/src/main/java/org/jboss/shamrock/deployment/ProcessorContext.java b/core/deployment/src/main/java/org/jboss/shamrock/deployment/ProcessorContext.java index b8b80adb034f7..1fdcb1514ddb2 100644 --- a/core/deployment/src/main/java/org/jboss/shamrock/deployment/ProcessorContext.java +++ b/core/deployment/src/main/java/org/jboss/shamrock/deployment/ProcessorContext.java @@ -90,4 +90,6 @@ public interface ProcessorContext { void addResourceBundle(String bundle); void addRuntimeInitializedClasses(String ... classes); + + void addProxyDefinition(String ... proxyClasses); } diff --git a/examples/strict/pom.xml b/examples/strict/pom.xml index 0d16d5495f6e1..5f56a12ec1003 100644 --- a/examples/strict/pom.xml +++ b/examples/strict/pom.xml @@ -89,6 +89,7 @@ org.jboss.shamrock shamrock-rest-client-deployment + provided @@ -166,6 +167,7 @@ true + true diff --git a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java index 6b598105019c8..2e127b07796b4 100644 --- a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java +++ b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/RestInterface.java @@ -3,10 +3,10 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; -@Path("/foo") +@Path("/test") public interface RestInterface { @GET - void get(); + String get(); } diff --git a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java index 48bcc2be07497..284fca0475970 100644 --- a/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java +++ b/examples/strict/src/main/java/org/jboss/shamrock/example/rest/TestResource.java @@ -1,5 +1,7 @@ package org.jboss.shamrock.example.rest; +import java.net.URL; + import javax.json.Json; import javax.json.JsonObject; import javax.ws.rs.GET; @@ -8,18 +10,28 @@ import javax.ws.rs.Produces; import javax.xml.bind.annotation.XmlRootElement; -import io.reactivex.Single; import org.eclipse.microprofile.rest.client.RestClientBuilder; +import io.reactivex.Single; + @Path("/test") public class TestResource { @GET public String getTest() { - RestInterface iface = RestClientBuilder.newBuilder().build(RestInterface.class); return "TEST"; } + @GET + @Path("/client") + public String client() throws Exception { + + RestInterface iface = RestClientBuilder.newBuilder() + .baseUrl(new URL("http", "localhost", 8080, "/rest")) + .build(RestInterface.class); + return iface.get(); + } + @GET @Path("/int/{val}") public Integer getInt(@PathParam("val") Integer val) { diff --git a/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientITCase.java b/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientITCase.java new file mode 100644 index 0000000000000..899cb493ebeed --- /dev/null +++ b/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientITCase.java @@ -0,0 +1,9 @@ +package org.jboss.shamrock.example.test; + +import org.jboss.shamrock.junit.GraalTest; +import org.junit.runner.RunWith; + +@RunWith(GraalTest.class) +public class RestClientITCase extends RestClientTestCase { + +} diff --git a/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientTestCase.java b/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientTestCase.java new file mode 100644 index 0000000000000..0d6ceb3c0963c --- /dev/null +++ b/examples/strict/src/test/java/org/jboss/shamrock/example/test/RestClientTestCase.java @@ -0,0 +1,29 @@ +package org.jboss.shamrock.example.test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +import org.jboss.shamrock.junit.ShamrockTest; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(ShamrockTest.class) +public class RestClientTestCase { + + @Test + public void testMicroprofileClient() throws Exception { + URL uri = new URL("http://localhost:8080/rest/test/client"); + URLConnection connection = uri.openConnection(); + InputStream in = connection.getInputStream(); + byte[] buf = new byte[100]; + int r; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + while ((r = in.read(buf)) > 0) { + out.write(buf, 0, r); + } + Assert.assertEquals("TEST", new String(out.toByteArray())); + } +} diff --git a/jaxrs/deployment/pom.xml b/jaxrs/deployment/pom.xml index 630e153ed9892..dbe89bc9eedee 100644 --- a/jaxrs/deployment/pom.xml +++ b/jaxrs/deployment/pom.xml @@ -25,6 +25,10 @@ org.jboss.shamrock shamrock-jaxrs-runtime + + org.jboss.graalvm + graal-annotations + diff --git a/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java b/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java index a76fbad47a655..0d285ce599145 100644 --- a/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java +++ b/maven/src/main/java/org/jboss/shamrock/maven/NativeImageMojo.java @@ -44,6 +44,8 @@ public class NativeImageMojo extends AbstractMojo { @Parameter(defaultValue = "${native-image.new-server}") private boolean cleanupServer; + @Parameter + private boolean enableHttpUrlHandler; @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -75,6 +77,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { } command.add("-jar"); command.add(finalName + "-runner.jar"); + //https://github.com/oracle/graal/issues/660 + command.add("-J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1"); if (reportErrorsAtRuntime) { command.add("-H:+ReportUnsupportedElementsAtRuntime"); } @@ -87,6 +91,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { command.add("-J-Djava.compiler=NONE"); command.add("-J-Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y"); } + if(enableHttpUrlHandler) { + command.add("-H:EnableURLProtocols=http"); + } //command.add("-H:+AllowVMInspection"); System.out.println(command); Process process = Runtime.getRuntime().exec(command.toArray(new String[0]), null, outputDirectory); diff --git a/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java index e19b21eaa7640..550433a43c4cf 100644 --- a/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java +++ b/rest-client/deployment/src/main/java/org/jboss/shamrock/restclient/RestClientProcessor.java @@ -3,17 +3,26 @@ import java.lang.reflect.Modifier; import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseFilter; +import org.apache.commons.logging.impl.Jdk14Logger; +import org.apache.commons.logging.impl.LogFactoryImpl; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.resteasy.client.jaxrs.internal.proxy.ResteasyClientProxy; +import org.jboss.resteasy.spi.ResteasyConfiguration; import org.jboss.shamrock.deployment.ArchiveContext; import org.jboss.shamrock.deployment.BeanDeployment; import org.jboss.shamrock.deployment.ProcessorContext; import org.jboss.shamrock.deployment.ResourceProcessor; import org.jboss.shamrock.deployment.ShamrockConfig; +import org.jboss.shamrock.restclient.runtime.DefaultResponseExceptionMapper; +import org.jboss.shamrock.restclient.runtime.RestClientProxy; class RestClientProcessor implements ResourceProcessor { @@ -24,17 +33,51 @@ class RestClientProcessor implements ResourceProcessor { @Inject private ShamrockConfig config; + private static final DotName[] CLIENT_ANNOTATIONS = { + DotName.createSimple("javax.ws.rs.GET"), + DotName.createSimple("javax.ws.rs.HEAD"), + DotName.createSimple("javax.ws.rs.DELETE"), + DotName.createSimple("javax.ws.rs.OPTIONS"), + DotName.createSimple("javax.ws.rs.PATCH"), + DotName.createSimple("javax.ws.rs.POST"), + DotName.createSimple("javax.ws.rs.PUT"), + DotName.createSimple("javax.ws.rs.PUT"), + DotName.createSimple(RegisterRestClient.class.getName()), + DotName.createSimple(Path.class.getName()) + }; @Override public void process(ArchiveContext archiveContext, ProcessorContext processorContext) throws Exception { - int count = 0; - for(AnnotationInstance annotation : archiveContext.getCombinedIndex().getAnnotations(REGISTER_REST_CLIENT)) { - AnnotationTarget target = annotation.target(); - ClassInfo clazz = target.asClass(); - if(!Modifier.isInterface(clazz.flags())) { - throw new RuntimeException("RegisterRestClient can only be applied to interfaces: " + clazz.name()); - } + processorContext.addReflectiveClass(false, false, + DefaultResponseExceptionMapper.class.getName(), + LogFactoryImpl.class.getName(), + Jdk14Logger.class.getName()); + processorContext.addReflectiveClass(false, false, ClientRequestFilter[].class.getName()); + processorContext.addReflectiveClass(false, false, ClientResponseFilter[].class.getName()); + processorContext.addResource("META-INF/services/javax.ws.rs.ext.Providers"); + //TODO: fix this, we don't want to just add all the providers + processorContext.addReflectiveClass(false, false, "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); + processorContext.addReflectiveClass(false, false, "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); + processorContext.addProxyDefinition(ResteasyConfiguration.class.getName()); + for (DotName type : CLIENT_ANNOTATIONS) { + for (AnnotationInstance annotation : archiveContext.getCombinedIndex().getAnnotations(type)) { + AnnotationTarget target = annotation.target(); + ClassInfo theInfo; + if (target.kind() == AnnotationTarget.Kind.CLASS) { + theInfo = target.asClass(); + } else if (target.kind() == AnnotationTarget.Kind.METHOD) { + theInfo = target.asMethod().declaringClass(); + } else { + continue; + } + if (!Modifier.isInterface(theInfo.flags())) { + continue; + } + processorContext.addProxyDefinition( theInfo.name().toString(), ResteasyClientProxy.class.getName()); + processorContext.addProxyDefinition( theInfo.name().toString(), RestClientProxy.class.getName()); + processorContext.addReflectiveClass(true, false, theInfo.name().toString()); + } } } diff --git a/rest-client/runtime/pom.xml b/rest-client/runtime/pom.xml index c8763d4850c4d..58f7568e21806 100644 --- a/rest-client/runtime/pom.xml +++ b/rest-client/runtime/pom.xml @@ -29,6 +29,10 @@ org.jboss.resteasy resteasy-client + + org.jboss.graalvm + graal-annotations + diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java index 24745b8fa19bb..25f6922e96896 100644 --- a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/DefaultResponseExceptionMapper.java @@ -24,7 +24,7 @@ /** * Created by hbraun on 17.01.18. */ -class DefaultResponseExceptionMapper implements ResponseExceptionMapper { +public class DefaultResponseExceptionMapper implements ResponseExceptionMapper { @Override public Throwable toThrowable(Response response) { diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java index 4d6f4f685843c..ccecdb378fa7f 100644 --- a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientBuilderImpl.java @@ -15,15 +15,22 @@ */ package org.jboss.shamrock.restclient.runtime; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.Proxy; +import java.net.InetAddress; +import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.Provider; +import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -32,6 +39,14 @@ import java.util.Optional; import java.util.Set; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -99,10 +114,13 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi verifyInterface(aClass); // Provider annotations - RegisterProvider[] providers = aClass.getAnnotationsByType(RegisterProvider.class); + Annotation[] providers = aClass.getAnnotations(); - for (RegisterProvider provider : providers) { - register(provider.value(), provider.priority()); + for (Annotation provider : providers) { + if(provider instanceof RegisterProvider) { + RegisterProvider p = (RegisterProvider) provider; + register(p.value(), p.priority()); + } } // Default exception mapper @@ -120,7 +138,84 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi T actualClient; ResteasyClient client; + this.builderDelegate.sslContext(new SSLContext(new SSLContextSpi() { + @Override + protected void engineInit(KeyManager[] keyManagers, TrustManager[] trustManagers, SecureRandom secureRandom) throws KeyManagementException { + + } + + @Override + protected SSLSocketFactory engineGetSocketFactory() { + return new SSLSocketFactory() { + @Override + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + return null; + } + + @Override + public Socket createSocket(String s, int i) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return null; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + return null; + } + }; + } + + @Override + protected SSLServerSocketFactory engineGetServerSocketFactory() { + return null; + } + + @Override + protected SSLEngine engineCreateSSLEngine() { + return null; + } + @Override + protected SSLEngine engineCreateSSLEngine(String s, int i) { + return null; + } + + @Override + protected SSLSessionContext engineGetServerSessionContext() { + return null; + } + + @Override + protected SSLSessionContext engineGetClientSessionContext() { + return null; + } + }, new Provider("Dummy", 1, "Dummy") { + @Override + public String getName() { + return super.getName(); + } + }, "BOGUS") { + + }); if (proxyHost != null && !noProxyHosts.contains(this.baseURI.getHost())) { // Use proxy, if defined client = this.builderDelegate.defaultProxy( diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java index 792e8ed2d6fba..ed49e33013fc8 100644 --- a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/RestClientProxy.java @@ -24,10 +24,6 @@ */ public interface RestClientProxy { - /** - * Release/close all associated resources, including the underlying {@link Client} instance. - */ - void close(); /** * diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/ClientBuilderReplacement.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/ClientBuilderReplacement.java new file mode 100644 index 0000000000000..cc6bfc87b463f --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/ClientBuilderReplacement.java @@ -0,0 +1,17 @@ +package org.jboss.shamrock.restclient.runtime.graal; + +import javax.ws.rs.client.ClientBuilder; + +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(ClientBuilder.class) +final class ClientBuilderReplacement { + + @Substitute + public static ClientBuilder newBuilder() { + return new ResteasyClientBuilder(); + } +} diff --git a/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/RestClientBuilderResolverReplacement.java b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/RestClientBuilderResolverReplacement.java new file mode 100644 index 0000000000000..6285a7a842543 --- /dev/null +++ b/rest-client/runtime/src/main/java/org/jboss/shamrock/restclient/runtime/graal/RestClientBuilderResolverReplacement.java @@ -0,0 +1,16 @@ +package org.jboss.shamrock.restclient.runtime.graal; + +import org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver; +import org.jboss.shamrock.restclient.runtime.BuilderResolver; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(RestClientBuilderResolver.class) +final class RestClientBuilderResolverReplacement { + + @Substitute + private static RestClientBuilderResolver loadSpi(ClassLoader cl) { + return new BuilderResolver(); + } +} diff --git a/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver b/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver index 994bfa3c819b5..a4a3dbfb02ada 100644 --- a/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver +++ b/rest-client/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver @@ -1 +1 @@ -io.smallrye.restclient.BuilderResolver +org.jboss.shamrock.restclient.runtime.BuilderResolver \ No newline at end of file