
net.winterly.rxjersey.client.WebResourceFactory Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012-2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*
* Portions Copyright 2016 Alex Shpak
* Portions Copyright 2017 Alex Shpak
*/
package net.winterly.rxjersey.client;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import javax.ws.rs.*;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.*;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.security.AccessController;
import java.util.*;
/**
* Factory for client-side representation of a resource.
* See the package overview
* for an example on how to use this class.
*
* @author Martin Matula
*/
public final class WebResourceFactory implements InvocationHandler {
private static final String[] EMPTY = {};
private static final MultivaluedMap EMPTY_HEADERS = new MultivaluedHashMap<>();
private static final Form EMPTY_FORM = new Form();
private static final List PARAM_ANNOTATION_CLASSES = Arrays.asList(PathParam.class, QueryParam.class,
HeaderParam.class, CookieParam.class, MatrixParam.class, FormParam.class, BeanParam.class);
private final WebTarget target;
private final MultivaluedMap headers;
private final List cookies;
private final Form form;
private final ClientMethodInvoker invoker;
private WebResourceFactory(final WebTarget target, final MultivaluedMap headers,
final List cookies, final Form form, final ClientMethodInvoker invoker) {
this.target = target;
this.headers = headers;
this.cookies = cookies;
this.form = form;
this.invoker = invoker;
}
/**
* Creates a new client-side representation of a resource described by
* the interface passed in the first argument.
*
* Calling this method has the same effect as calling {@code WebResourceFactory.newResource(resourceInterface, rootTarget,
* false)}.
*
* @param Type of the resource to be created.
* @param resourceInterface Interface describing the resource to be created.
* @param target WebTarget pointing to the resource or the parent of the resource.
* @param invoker Method invoker
* @return Instance of a class implementing the resource interface that can
* be used for making requests to the server.
*/
public static C newResource(final Class resourceInterface, final WebTarget target, final ClientMethodInvoker invoker) {
return newResource(resourceInterface, target, false, EMPTY_HEADERS, Collections.emptyList(), EMPTY_FORM, invoker);
}
/**
* Creates a new client-side representation of a resource described by
* the interface passed in the first argument.
*
* @param Type of the resource to be created.
* @param resourceInterface Interface describing the resource to be created.
* @param target WebTarget pointing to the resource or the parent of the resource.
* @param ignoreResourcePath If set to true, ignores path annotation on the resource interface (this is used when creating
* sub-resources)
* @param headers Header params collected from parent resources (used when creating a sub-resource)
* @param cookies Cookie params collected from parent resources (used when creating a sub-resource)
* @param form Form params collected from parent resources (used when creating a sub-resource)
* @param invoker Method invoker
* @return Instance of a class implementing the resource interface that can
* be used for making requests to the server.
*/
@SuppressWarnings("unchecked")
public static C newResource(final Class resourceInterface,
final WebTarget target,
final boolean ignoreResourcePath,
final MultivaluedMap headers,
final List cookies,
final Form form,
final ClientMethodInvoker invoker) {
return (C) Proxy.newProxyInstance(AccessController.doPrivileged(ReflectionHelper.getClassLoaderPA(resourceInterface)),
new Class[]{resourceInterface},
new WebResourceFactory(ignoreResourcePath ? target : addPathFromAnnotation(resourceInterface, target),
headers, cookies, form, invoker));
}
private static WebTarget addPathFromAnnotation(final AnnotatedElement ae, WebTarget target) {
final Path p = ae.getAnnotation(Path.class);
if (p != null) {
target = target.path(p.value());
}
return target;
}
private static String getHttpMethodName(final AnnotatedElement ae) {
final HttpMethod a = ae.getAnnotation(HttpMethod.class);
return a == null ? null : a.value();
}
@Override
@SuppressWarnings("unchecked")
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (args == null && method.getName().equals("toString")) {
return toString();
}
// get the interface describing the resource
final Class> proxyIfc = proxy.getClass().getInterfaces()[0];
// response type
final Class> responseType = method.getReturnType();
// determine method name
String httpMethod = getHttpMethodName(method);
if (httpMethod == null) {
for (final Annotation ann : method.getAnnotations()) {
httpMethod = getHttpMethodName(ann.annotationType());
if (httpMethod != null) {
break;
}
}
}
// create a new UriBuilder appending the @Path attached to the method
WebTarget newTarget = addPathFromAnnotation(method, target);
if (httpMethod == null) {
if (newTarget == target) {
// no path annotation on the method -> fail
throw new UnsupportedOperationException("Not a resource method.");
} else if (!responseType.isInterface()) {
// the method is a subresource locator, but returns class,
// not interface - can't help here
throw new UnsupportedOperationException("Return type not an interface");
}
}
// process method params (build maps of (Path|Form|Cookie|Matrix|Header..)Params
// and extract entity type
final MultivaluedHashMap headers = new MultivaluedHashMap(this.headers);
final LinkedList cookies = new LinkedList<>(this.cookies);
final Form form = new Form();
form.asMap().putAll(this.form.asMap());
final Annotation[][] paramAnns = method.getParameterAnnotations();
Object entity = null;
Type entityType = null;
for (int i = 0; i < paramAnns.length; i++) {
final Map anns = getAnnotationsMap(paramAnns[i]);
Object value = args[i];
if (!hasAnyParamAnnotation(anns)) {
entityType = method.getGenericParameterTypes()[i];
entity = value;
} else {
newTarget = setupParameter(method.getParameterTypes()[i], anns, headers, cookies,
form, newTarget, value);
}
}
if (httpMethod == null) {
// the method is a subresource locator
return WebResourceFactory.newResource(responseType, newTarget, true, headers, cookies, form, null);
}
// accepted media types
Produces produces = method.getAnnotation(Produces.class);
if (produces == null) {
produces = proxyIfc.getAnnotation(Produces.class);
}
final String[] accepts = (produces == null) ? EMPTY : produces.value();
// determine content type
String contentType = null;
if (entity != null) {
final List