/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2010 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 * https://glassfish.dev.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. */ package com.sun.jersey.client.hypermedia; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResourceLinkHeaders; import com.sun.jersey.api.representation.Form; import com.sun.jersey.core.header.LinkHeader; import com.sun.jersey.core.hypermedia.Action; import com.sun.jersey.core.hypermedia.HypermediaController; import com.sun.jersey.core.hypermedia.Name; import com.sun.jersey.core.util.MultivaluedMapImpl; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; import javax.ws.rs.FormParam; import javax.ws.rs.HeaderParam; import javax.ws.rs.HttpMethod; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriBuilder; import org.jvnet.ws.wadl.Application; import org.jvnet.ws.wadl.Param; import org.jvnet.ws.wadl2java.Wadl2Java; import org.jvnet.ws.wadl2java.ast.MethodNode; import org.jvnet.ws.wadl2java.ast.RepresentationNode; import org.jvnet.ws.wadl2java.ast.ResourceNode; /** * ControllerInvocationHandler class. * * @author Santiago.PericasGeertsen@sun.com */ public class ControllerInvocationHandler<T> implements InvocationHandler { private Client client; private T model; private Class<?> modelClass; private Class<?> ctrlClass; private ClientResponse response; private WebResourceLinkHeaders actionHeaders; private Object entity; private MultivaluedMapImpl queryParams; private Map<URI, MethodNode> wadlMetadataCache; public ControllerInvocationHandler(Client client, T instance, ClientResponse response, Class<?> ctrlClass) { // Check if annotation is present in controller if (!ctrlClass.isAnnotationPresent(HypermediaController.class)) { throw new IllegalArgumentException("Hypermedia controller " + "class must have @HypermediaController annotation"); } this.client = client; this.model = instance; this.response = response; this.modelClass = instance.getClass(); this.ctrlClass = ctrlClass; updateActionHeaders(); queryParams = new MultivaluedMapImpl(); } /** * Maps a method invocation in a controller interface to an * HTTP request on an action resource. Use WADL if not enough * static information is available for the mapping. */ public Object invoke(Object o, Method method, Object[] params) throws Throwable { // Clear entity reference and query values from previous call entity = null; queryParams.clear(); // Trying to access local model (non-action method) Action action = method.getAnnotation(Action.class); if (action == null && method.getReturnType() == modelClass) { return model; } // Check if @Action annotation is present if (action == null) { throw new RuntimeException( "Method " + method.getName() + " in " + o.getClass().getInterfaces()[0].getName() + " must have @Action or @Model annotation"); } // Empty set of action headers? if (actionHeaders == null) { throw new RuntimeException( "Action '" + action.value() + "' is not available" + " in current context"); } // Check if action is available in current context LinkHeader h = actionHeaders.getLink(action.value()); if (h == null) { throw new RuntimeException( "Action '" + action.value() + "' is not available" + " in current context"); } // If HTTP method annotation, must match 'op' in link header String httpMethod = getHttpMethod(ctrlClass, method); if (httpMethod == null) { httpMethod = h.getOp(); assert httpMethod != null; } else if (!httpMethod.equals(h.getOp())) { throw new RuntimeException( "HTTP method annotation " + httpMethod + " in method '" + method.getName() + "' does not match HTTP method " + h.getOp() + " in link header"); } // Create client request builder ClientRequest.Builder rb = ClientRequest.create(); // Map method params to HTTP request mapParametersToClientRequest(rb, method, params); // Create URI builder and add query params UriBuilder ub = UriBuilder.fromUri(h.getUri()); for (Map.Entry<String, List<String>> e : queryParams.entrySet()) { for (String v : e.getValue()) { ub.queryParam(e.getKey(), v); } } // Client side: produces is Accept Produces produces = method.getAnnotation(Produces.class); if (produces != null) { rb.accept(produces.value()); } // Is there an entity? if (entity != null) { // Client side: consumes is Content-Type String contentType = null; Consumes consumes = method.getAnnotation(Consumes.class); if (consumes != null) { String[] contentTypes = consumes.value(); if (contentTypes.length > 1) { throw new RuntimeException("Annotation @Consumes in" + " action '" + action.value() + " ' must have a single media type"); } contentType = contentTypes[0]; } else if (entity instanceof MultivaluedMap) { // Guess content type if this is a Form contentType = MediaType.APPLICATION_FORM_URLENCODED; } else { contentType = getContentTypeFromWadl(action, method); assert contentType != null; } // Send HTTP request with entity ClientRequest cr = rb.entity(entity, contentType) .build(ub.build(), httpMethod); response = client.handle(cr); } else { // Send HTTP request without an entity ClientRequest cr = rb.build(ub.build(), httpMethod); response = client.handle(cr); } // Update action headers based on last response updateActionHeaders(); // Update local model if returned HypermediaController hc = ctrlClass.getAnnotation(HypermediaController.class); assert hc != null; if (method.getReturnType() == hc.model()) { model = (T) response.getEntity(modelClass); response.close(); return model; } else if (method.getReturnType() != void.class) { Object tmp = response.getEntity(method.getReturnType()); response.close(); return tmp; } else { return null; } } /** * Parses action headers and updates internal map. */ private void updateActionHeaders() { actionHeaders = response.getLinks(); } /** * Finds an annotation annotated with @HttpMethod in the * controller class (such as @GET, @PUT, etc.) or any of * its superclasses. */ private String getHttpMethod(Class<?> ctrlClass, Method method) { // Find annotation in this class for (Annotation annot : method.getDeclaredAnnotations()) { HttpMethod httpMethod = annot.annotationType().getAnnotation(HttpMethod.class); if (httpMethod != null) { return httpMethod.value(); } } // Try super interfaces then Class<?>[] superInterfaces = ctrlClass.getInterfaces(); for (Class<?> superInterface : superInterfaces) { for (Method superMethod : superInterface.getDeclaredMethods()) { if (superMethod.getName().equals(method.getName()) && superMethod.getReturnType() == method.getReturnType() && Arrays.equals(superMethod.getParameterTypes(), method.getParameterTypes())) { return getHttpMethod(superInterface, superMethod); } } } // Not found return null; } /** * Determines if an object is of any of the collection * types supported by JAX-RS for parameters. */ private boolean isJaxrsCollectionValue(Object object) { return (object instanceof List || object instanceof Set || object instanceof SortedSet); } /** * Maps a method parameter in a controller interface to an * HTTP parameter or the entity. If not enough information * is available statically, use WADL to determine mapping. */ private void mapParametersToClientRequest(ClientRequest.Builder rb, Method method, Object[] values) { // Inspect parameter annotations in method if (values != null) { Annotation[][] annotations = method.getParameterAnnotations(); // Search for entity looking for params without annotations int j = 0; for (Object value: values) { if (annotations[j++].length == 0) { entity = value; break; } } // Collect all request values j = 0; for (Object value : values) { // If more than one annotation, report error if (annotations[j].length > 1) { throw new IllegalArgumentException("At most one annotation" + " is permitted in method '" + method.getName() + "' of interface '" + ctrlClass.getName() + "'"); } // Determine mapping for this value for (Annotation annot : annotations[j]) { Class paramType = annot.annotationType(); // If @Name, determine param type looking at WADL if (paramType == Name.class) { mapParamUsingWadl(method, (Name) annot, value, rb); } else if (paramType == HeaderParam.class) { final HeaderParam hp = (HeaderParam) annot; setHeaderParam(hp.value(), value, rb); } else if (paramType == QueryParam.class) { final QueryParam qp = (QueryParam) annot; setQueryParam(qp.value(), value); } else if (paramType == CookieParam.class) { setCookieParam(value, rb); } else if (paramType == FormParam.class) { MultivaluedMap form = null; final FormParam fp = (FormParam) annot; // If entity found, check its type if (entity != null) { if (entity instanceof Form || entity instanceof MultivaluedMap) { form = (MultivaluedMap) entity; } else { throw new IllegalArgumentException( "Unannotated parameter in method '" + method.getName() + "' in interface '" + ctrlClass.getName() + "' must be an instance of Form or " + " MultivaluedMap to support @FormParam"); } } else { entity = form = new Form(); // create entity } setFormParam(fp.value(), value, form); } else { throw new IllegalArgumentException("Annotation " + paramType.getName() + " is not permitted in interface '" + ctrlClass.getName() + "'"); } } j++; } } } /** * Maps a named method parameter in a controller interface * to an HTTP parameter or the entity using WADL. */ private void mapParamUsingWadl(Method method, Name name, Object value, ClientRequest.Builder rb) { Action action = method.getAnnotation(Action.class); LinkHeader h = actionHeaders.getLink(action.value()); MethodNode methodNode = getWadlMetadata(h); if (methodNode == null) { throw new RuntimeException("Unable to find WADL meta-data to " + "map hypermedia action '" + action.value() + "'"); } // Determine if this is a query param List<Param> params = methodNode.getQueryParameters(); if (params != null) { for (Param param : params) { final String paramName = param.getName(); if (paramName.equals(name.value())) { setQueryParam(paramName, value); return; } } } // Determine if this is a header or cookie param // Cookie params have name="Cookie" path="<name>" params = methodNode.getHeaderParameters(); if (params != null) { for (Param param : params) { final String paramName = param.getName(); if (paramName.equals(name.value())) { setHeaderParam(paramName, value, rb); return; } else if (paramName.equals("Cookie") && param.getPath().equals(name.value())) { setCookieParam(value, rb); return; } } } // Determine if this is a form param List<RepresentationNode> reps = methodNode.getSupportedInputs(); if (reps != null) { for (RepresentationNode rep : reps) { // Find param in representation for (Param param : rep.getParam()) { if (param.getName().equals(name.value())) { MultivaluedMap form = null; // If entity found, check its type if (entity != null) { if (entity instanceof Form || entity instanceof MultivaluedMap) { form = (MultivaluedMap) entity; } else { throw new IllegalArgumentException( "Unannotated parameter in method '" + method.getName() + "' in interface '" + ctrlClass.getName() + "' must be an instance of Form or " + " MultivaluedMap to support @FormParam"); } } else { entity = form = new Form(); // create entity } setFormParam(name.value(), value, form); return; } } } } // Couldn't map, throw exception throw new RuntimeException( "Unable to map parameter '" + name.value() + "' in method '" + method.getName() + "' of interface '" + ctrlClass.getName() + "'"); } /** * Returns the expected content-type of an action from its * WADL description. It reports an error if multiple media * types are supported as input or if unable to determine * what inputs are supported. */ private String getContentTypeFromWadl(Action action, Method method) { // Get WADL method node for this action LinkHeader h = actionHeaders.getLink(action.value()); MethodNode methodNode = getWadlMetadata(h); if (methodNode == null) { throw new RuntimeException("Unable to find WADL meta-data to " + "map hypermedia action '" + action.value() + "'"); } // Get list of inputs and make sure it's unambiguous List<RepresentationNode> l = methodNode.getSupportedInputs(); if (l == null || l.size() > 1) { throw new RuntimeException("Unable to determine content type" + " for action '" + action.value() + "' -- use @Consumes annotation"); } return l.get(0).getMediaType(); } /** * Returns a <code>MethodNode</code> that represents the meta-data * for the link in header <code>h</code>, or <code>null</code> * if no meta-data was found. Uses OPTIONS to retrieve a WADL * fragment for the corresponding action. */ private MethodNode getWadlMetadata(LinkHeader h) { MethodNode result = null; // Meta-data in cache? if (wadlMetadataCache == null) { wadlMetadataCache = new HashMap<URI, MethodNode>(); } else { result = wadlMetadataCache.get(h.getUri()); } // Attempt to fetch meta-data from server if (result == null) { Wadl2Java wm = new Wadl2Java(null, null, false); try { WebResource r = client.resource(h.getUri()); InputStream is = r.options(InputStream.class); // Requires WadlFragmentGetFilter on server side Application a = wm.processDescription(h.getUri(), is); List<ResourceNode> rs = wm.buildAst(a, h.getUri()); ResourceNode rn = rs.get(0).getChildResources().get(0); // Find method whose operation matches the link header's for (MethodNode mn : rn.getMethods()) { if (mn.getName().equals(h.getOp())) { result = mn; } } } catch (Exception ex) { throw new RuntimeException(ex); } wadlMetadataCache.put(h.getUri(), result); } return result; } /** * Sets a query param in request based on the type of value. */ private void setQueryParam(String name, Object value) { if (isJaxrsCollectionValue(value)) { for (Object o : (Collection) value) { queryParams.add(name, o.toString()); } } else { queryParams.add(name, value.toString()); } } /** * Sets a header param in request based on the type of value. */ private void setHeaderParam(String name, Object value, ClientRequest.Builder rb) { if (isJaxrsCollectionValue(value)) { for (Object o : (Collection) value) { rb.header(name, o); } } else { rb.header(name, value); } } /** * Sets a cookie param in request based on the type of value. */ private void setCookieParam(Object value, ClientRequest.Builder rb) { if (isJaxrsCollectionValue(value)) { for (Object o : (Collection) value) { rb.cookie(o instanceof Cookie ? (Cookie) o : Cookie.valueOf(o.toString())); } } else { rb.cookie(value instanceof Cookie ? (Cookie) value : Cookie.valueOf(value.toString())); } } /** * Sets a form param in request based on the type of value. */ private void setFormParam(String name, Object value, MultivaluedMap form) { if (isJaxrsCollectionValue(value)) { for (Object o : (Collection) value) { form.add(name, o); } } else { form.add(name, value); } } }