Return-Path: Delivered-To: apmail-incubator-wink-commits-archive@minotaur.apache.org Received: (qmail 50839 invoked from network); 23 Jun 2009 10:03:19 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.3) by minotaur.apache.org with SMTP; 23 Jun 2009 10:03:19 -0000 Received: (qmail 51879 invoked by uid 500); 23 Jun 2009 10:03:29 -0000 Delivered-To: apmail-incubator-wink-commits-archive@incubator.apache.org Received: (qmail 51860 invoked by uid 500); 23 Jun 2009 10:03:29 -0000 Mailing-List: contact wink-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: wink-dev@incubator.apache.org Delivered-To: mailing list wink-commits@incubator.apache.org Delivered-To: moderator for wink-commits@incubator.apache.org Received: (qmail 96423 invoked by uid 99); 23 Jun 2009 05:42:56 -0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r787557 [12/12] - in /incubator/wink/contrib/ibm-jaxrs: ./ lib/ src/ src/com/ src/com/ibm/ src/com/ibm/ws/ src/com/ibm/ws/jaxrs/ src/com/ibm/ws/jaxrs/annotations/ src/com/ibm/ws/jaxrs/context/ src/com/ibm/ws/jaxrs/core/ src/com/ibm/ws/jaxrs... Date: Tue, 23 Jun 2009 05:41:55 -0000 To: wink-commits@incubator.apache.org From: ngallardo@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20090623054202.5050723889BB@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: incubator/wink/contrib/ibm-jaxrs/src/org/apache/cxf/jaxrs/utils/JAXRSUtils.java URL: http://svn.apache.org/viewvc/incubator/wink/contrib/ibm-jaxrs/src/org/apache/cxf/jaxrs/utils/JAXRSUtils.java?rev=787557&view=auto ============================================================================== --- incubator/wink/contrib/ibm-jaxrs/src/org/apache/cxf/jaxrs/utils/JAXRSUtils.java (added) +++ incubator/wink/contrib/ibm-jaxrs/src/org/apache/cxf/jaxrs/utils/JAXRSUtils.java Tue Jun 23 05:41:49 2009 @@ -0,0 +1,1506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.jaxrs.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.activation.CommandMap; +import javax.activation.DataContentHandler; +import javax.activation.DataHandler; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Providers; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.jaxrs.impl.MetadataMap; +import org.apache.cxf.jaxrs.impl.PathSegmentImpl; +import org.apache.cxf.jaxrs.model.ClassResourceInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfoComparator; +import org.apache.cxf.jaxrs.model.ProviderInfo; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.cxf.jaxrs.provider.ProviderFactory; + +import com.ibm.ws.jaxrs.context.ContextConstants; +import com.ibm.ws.jaxrs.context.RESTContext; +import com.ibm.ws.jaxrs.core.HttpHeadersImpl; +import com.ibm.ws.jaxrs.core.RequestImpl; +import com.ibm.ws.jaxrs.core.SecurityContextImpl; +import com.ibm.ws.jaxrs.core.UriInfoImpl; +import com.ibm.ws.jaxrs.ext.ProvidersImpl; +import com.ibm.ws.jaxrs.i18n.Messages; +import com.ibm.ws.jaxrs.integration.DefaultResponseWriter; +import com.ibm.ws.jaxrs.integration.IntegrationRegistry; +import com.ibm.ws.jaxrs.integration.OptionsResponseProvider; +import com.ibm.ws.jaxrs.integration.ResponseWriter; +import com.ibm.ws.jaxrs.io.ContentMonitor; +import com.ibm.ws.jaxrs.io.LogInputStream; +import com.ibm.ws.jaxrs.provider.RESTDataSource; + +public final class JAXRSUtils { + + private static Log log = LogFactory.getLog(JAXRSUtils.class); + + // private static final ResourceBundle BUNDLE = + // BundleUtils.getBundle(JAXRSUtils.class); + + private JAXRSUtils() { + } + + public static List getPathSegments(String thePath, boolean decode) { + List theList = new ArrayList(); + if (thePath != null && thePath.equals("/")) { + theList.add(new PathSegmentImpl(thePath, decode)); + } else if (thePath != null) { + String[] segments = thePath.split("/"); + for (String path : segments) { + if (!StringUtils.isEmpty(path)) { + theList.add(new PathSegmentImpl(path, decode)); + } + } + } + return theList; + } + + public static List getMediaTypes(String[] values) { + if (values == null) { + return Collections.singletonList(MediaType.WILDCARD_TYPE); + } + List supportedMimeTypes = new ArrayList( + values.length); + for (int i = 0; i < values.length; i++) { + supportedMimeTypes.add(MediaType.valueOf(values[i])); + } + return supportedMimeTypes; + } + + public static ClassResourceInfo findSubResourceClass(ClassResourceInfo resource, Class subResourceClassType) { + for (ClassResourceInfo subCri : resource.getSubClassResourceInfo()) { + if (subCri.getResourceClass() + .isAssignableFrom(subResourceClassType)) { + return subCri; + } + } + return null; + } + + public static ClassResourceInfo selectResourceClass(List resources, String path, MultivaluedMap values) { + + if (resources.size() == 1) { + return resources.get(0).getURITemplate().match(path, values) ? resources + .get(0) + : null; + } + + SortedMap> candidateList = new TreeMap>( + new Comparator() { + + public int compare(ClassResourceInfo cr1, ClassResourceInfo cr2) { + + String l1 = cr1.getURITemplate().getLiteralChars(); + String l2 = cr2.getURITemplate().getLiteralChars(); + if (!l1.equals(l2)) { + // descending order + return l1.length() < l2.length() ? 1 : -1; + } + + int g1 = cr1.getURITemplate().getNumberOfGroups(); + int g2 = cr2.getURITemplate().getNumberOfGroups(); + // descending order + return g1 < g2 ? 1 : g1 > g2 ? -1 : 0; + } + + }); + + for (ClassResourceInfo resource : resources) { + MultivaluedMap map = new MetadataMap(); + if (resource.getURITemplate().match(path, map)) { + candidateList.put(resource, map); + } + } + + if (!candidateList.isEmpty()) { + Map.Entry> firstEntry = candidateList + .entrySet().iterator().next(); + values.putAll(firstEntry.getValue()); + return firstEntry.getKey(); + } + + return null; + } + + /** + * Method to select a ClassResourceInfo based on its @Alias annotation(s) value. + */ + public static ClassResourceInfo selectResourceClassFromAliases(List resources, String path, MultivaluedMap values, RESTContext context) { + + // keep mapping of URITemplates to the ClassResourceInfo they were constructed from + Map templateMapping = new HashMap(); + + // sorted map for all URITemplates + SortedMap> candidateList = new TreeMap>( + new Comparator() { + + public int compare(URITemplate cr1, URITemplate cr2) { + + String l1 = cr1.getLiteralChars(); + String l2 = cr2.getLiteralChars(); + if (!l1.equals(l2)) { + // descending order + return l1.length() < l2.length() ? 1 : -1; + } + + int g1 = cr1.getNumberOfGroups(); + int g2 = cr2.getNumberOfGroups(); + // descending order + return g1 < g2 ? 1 : g1 > g2 ? -1 : 0; + } + + }); + + // remove any matrix parameters + String matrixFreePath = path; + if (matrixFreePath.contains(";")) { + matrixFreePath = matrixFreePath.substring(0, matrixFreePath + .indexOf(";")); + } + + for (ClassResourceInfo resource : resources) { + MultivaluedMap map = new MetadataMap(); + List templates = resource.getURIAliasTemplates(); + if (templates != null) { + for (URITemplate template : templates) { + + // if we have a match, store the URITemplate and values map, + // also store mapping of URITemplate to ClassResourceInfo + if (template.match(matrixFreePath, map)) { + candidateList.put(template, map); + templateMapping.put(template, resource); + } + } + } + } + + // if we have a list, get the first entry and store the matched alias + // path on the provided RESTContext + if (!candidateList.isEmpty()) { + Map.Entry> firstEntry = candidateList + .entrySet().iterator().next(); + values.putAll(firstEntry.getValue()); + context.setProperty(ContextConstants.MATCHED_ALIAS_PATH, firstEntry + .getKey().getValue()); + return templateMapping.get(firstEntry.getKey()); + } + + return null; + } + + public static OperationResourceInfo findTargetMethod(ClassResourceInfo resource, String path, String httpMethod, MultivaluedMap values, String requestContentType, List acceptContentTypes) { + SortedMap> candidateList = new TreeMap>( + new OperationResourceInfoComparator()); + MediaType requestType = requestContentType == null ? null : MediaType + .valueOf(requestContentType); + boolean didMatchRequestType = false; + boolean didMatchMethodType = false; + boolean didMatchResource = false; + Set possibleMethodOperations = new HashSet(); + if (resource.getMethodDispatcher().getOperationResourceInfos() == null + || resource.getMethodDispatcher().getOperationResourceInfos() + .isEmpty()) { + if (StringUtils.isEmpty(path) || "/".equals(path)) { + // no operations exist at this level, fast fail + if (log.isErrorEnabled()) { + log.error(Messages.getMessage("resourceNotFound00", + resource.getResourceClass().getName(), httpMethod, + path, (requestContentType == null) ? "UNKNOWN" + : requestContentType)); + } + throw new WebApplicationException(405); + } + // did not match the final group yet so there didn't exist a subresource to match + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + for (MediaType acceptType : acceptContentTypes) { + // used to save off possible GET match for a HEAD request + for (OperationResourceInfo ori : resource.getMethodDispatcher() + .getOperationResourceInfos()) { + URITemplate uriTemplate = ori.getURITemplate(); + + MultivaluedMap map = cloneMap(values); + if (uriTemplate != null && uriTemplate.match(path, map)) { + String finalGroup = map + .getFirst(URITemplate.FINAL_MATCH_GROUP); + if (ori.isSubResourceLocator()) { + candidateList.put(ori, map); + } else if (finalGroup == null + || StringUtils.isEmpty(finalGroup) + || finalGroup.equals("/")) { + didMatchResource = true; + + possibleMethodOperations.add(ori.getHttpMethod() + .toUpperCase()); + if (ori.getHttpMethod().equalsIgnoreCase(httpMethod)) { + didMatchMethodType = true; + if (matchContentMediaMimeTypes(ori.getConsumes(), + requestType)) { + didMatchRequestType = true; + if (matchContentMediaMimeTypes(ori + .getProduces(), acceptType)) { + if (log.isDebugEnabled()) { + log + .debug("Added the " + + ori + .getMethodToInvoke() + .getName() + + " method to the candidate list for the request with path: " + + path + + " HTTP method: " + + httpMethod + + " Consumes: " + + typesAsString(ori + .getConsumes()) + + " Produces: " + + typesAsString(ori + .getProduces())); + } + candidateList.put(ori, map); + } + } + } else if (httpMethod.equalsIgnoreCase("HEAD") + && ori.getHttpMethod().equalsIgnoreCase("GET") + && matchContentMediaMimeTypes( + ori.getConsumes(), requestType) + && matchContentMediaMimeTypes( + ori.getProduces(), acceptType)) { + candidateList.put(ori, map); + } + } + } + } + if (!candidateList.isEmpty()) { + Map.Entry> firstEntry = candidateList + .entrySet().iterator().next(); + values.clear(); + values.putAll(firstEntry.getValue()); + OperationResourceInfo ori = firstEntry.getKey(); + if (log.isDebugEnabled()) { + log.debug("Found the " + ori.getMethodToInvoke().getName() + + " method for the " + "request with path: " + path + + " and HTTP method: " + httpMethod); + } + return ori; + } + } + + if (httpMethod.equalsIgnoreCase("OPTIONS")) { + // check to see if there is an OPTIONS method, but the request + // did not match + if (resource.getMethodDispatcher().getOperationResourceInfos() != null) { + for (OperationResourceInfo opInfo : resource + .getMethodDispatcher().getOperationResourceInfos()) { + + // if we find an @OPTIONS annotated method, but it did not match the request, + // it is time to return a 406 + if ("OPTIONS".equalsIgnoreCase(opInfo.getHttpMethod())) { + throw new WebApplicationException( + Response.Status.NOT_ACCEPTABLE); + } + } + } + // return null for options support later in the runtime + return null; + } + + if (didMatchRequestType) { + // but did not match accept type + throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE); + } + + if (didMatchMethodType) { + // but did not match request type + throw new WebApplicationException( + Response.Status.UNSUPPORTED_MEDIA_TYPE); + } + + if (didMatchResource) { + // but did not match request httpmethod + possibleMethodOperations.add("HEAD"); + possibleMethodOperations.add("OPTIONS"); + ResponseBuilder respBuilder = Response.status(405); + for (String s : possibleMethodOperations) { + respBuilder.header("Allow", s); + } + throw new WebApplicationException(respBuilder.build()); + } + + // did not match a URI template, so throw 404 (the original class resource exists but no subclass) + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + + static String typesAsString(List types) { + StringBuffer sb = new StringBuffer(); + String result = ""; + if (types != null) { + for (MediaType type : types) { + sb.append(type.toString() + " "); + } + result = sb.toString().trim(); + } + return result; + } + + public static List getConsumeTypes(Consumes cm) { + return cm == null ? Collections.singletonList(MediaType.WILDCARD_TYPE) + : getMediaTypes(cm.value()); + } + + public static List getProduceTypes(Produces pm) { + return pm == null ? Collections.singletonList(MediaType.WILDCARD_TYPE) + : getMediaTypes(pm.value()); + } + + public static int compareMediaTypesQValue(MediaType mt1, MediaType mt2) { + float q1 = getMediaTypeQualityFactor(mt1.getParameters().get("q")); + float q2 = getMediaTypeQualityFactor(mt2.getParameters().get("q")); + int result = Float.compare(q1, q2); + return (result == 0 ? result : -result); + } + + public static int compareMediaTypes(MediaType mt1, MediaType mt2) { + + if ((!mt1.isWildcardSubtype() && !mt1.isWildcardType() + && !mt2.isWildcardSubtype() && !mt2.isWildcardType()) + || (mt1.isWildcardSubtype() && mt1.isWildcardType() + && mt2.isWildcardSubtype() && mt2.isWildcardType())) { + + float q1 = getMediaTypeQualityFactor(mt1.getParameters().get("q")); + float q2 = getMediaTypeQualityFactor(mt2.getParameters().get("q")); + int result = Float.compare(q1, q2); + if (result == 0) { + int mt1pSize = mt1.getParameters().size(); + int mt2pSize = mt2.getParameters().size(); + if (mt1pSize > mt2pSize) { + return -1; + } else if (mt1pSize < mt2pSize) { + return 1; + } else { + return 0; + } + } + return -result; + } + + if (mt1.isWildcardType() && !mt2.isWildcardType()) { + return 1; + } + if (!mt1.isWildcardType() && mt2.isWildcardType()) { + return -1; + } + + if (mt1.getType().equals(mt2.getType())) { + if (mt1.isWildcardSubtype() && !mt2.isWildcardSubtype()) { + return 1; + } + if (!mt1.isWildcardSubtype() && mt2.isWildcardSubtype()) { + return -1; + } + } + return mt1.toString().compareTo(mt2.toString()); + + } + + public static float getMediaTypeQualityFactor(final String q) { + if (q == null) { + return 1; + } + String qualityFactor = q; + if (qualityFactor.charAt(0) == '.') { + qualityFactor = '0' + qualityFactor; + } + try { + return Float.parseFloat(qualityFactor); + } catch (NumberFormatException ex) { + // default value will do + } + return 1; + } + + //Message contains following information: PATH, HTTP_REQUEST_METHOD,CONTENT_TYPE, InputStream. + public static List processParameters(OperationResourceInfo ori, RESTContext context) + throws Exception { + Method method = ori.getAnnotatedMethod(); + Class[] parameterTypes = method.getParameterTypes(); + Type[] genericParameterTypes = method.getGenericParameterTypes(); + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + List params = new ArrayList(parameterTypes.length); + + for (int i = 0; i < parameterTypes.length; i++) { + Object param = processParameter(parameterTypes[i], + genericParameterTypes[i], parameterAnnotations[i], context, + ori); + params.add(param); + } + + return params; + } + + private static Object processParameter(Class parameterClass, Type parameterType, Annotation[] parameterAnns, RESTContext context, OperationResourceInfo ori) + throws Exception { + InputStream is = (InputStream) context + .getProperty(ContextConstants.REQUEST_INPUT_STREAM); + + MultivaluedMap values = (MultivaluedMap) context + .getProperty(ContextConstants.TEMPLATE_VALUES); + + String path = (String) context + .getProperty(ContextConstants.HTTP_PATH_INFO); + + boolean isJAXRSParamAnnotationsFound = false; + if (parameterAnns != null && parameterAnns.length > 0) { + for (Annotation ann : parameterAnns) { + if (AnnotationUtils + .isParamAnnotationClass(ann.annotationType())) { + isJAXRSParamAnnotationsFound = true; + break; + } + } + } + + if (isJAXRSParamAnnotationsFound) { + + for (Annotation ann : parameterAnns) { + if (ann.annotationType().equals(Context.class)) { + return createContextValue(context, parameterType, + parameterClass); + } + } + + return createHttpParameterValue(parameterAnns, parameterClass, + parameterType, context, values, path, ori); + } + + // we can't really limit it to just PUT and POST + if (log.isDebugEnabled()) { + log.debug("No parameter annotations found."); + } + + String contentType = (String) context + .getProperty(ContextConstants.CONTENT_TYPE); + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM; + } + + if (ori.isFormParamFound()) { + return new MetadataMap( + (Map>) context + .getProperty(ContextConstants.FORM_VALUES)); + } + + return readFromMessageBody(parameterClass, parameterType, + parameterAnns, is, MediaType.valueOf(contentType), ori, context); + } + + private static Object createHttpParameterValue(Annotation[] anns, Class parameterClass, Type genericParam, RESTContext context, MultivaluedMap values, String path, OperationResourceInfo ori) + throws Exception { + return InjectionUtils.handleNonContextParam(parameterClass, + genericParam, context, anns); + } + + public static MultivaluedMap getMatrixParams(String path, boolean decode) { + int index = path.indexOf(';'); + MultivaluedMap matrixParams = index == -1 ? new MetadataMap() + : JAXRSUtils.getStructuredParams(path.substring(index + 1), + ";", decode); + formatMatrixParams(matrixParams); + return matrixParams; + } + + /** + * This method is a convenience method to ensure any embedded matrix parameters + * are correctly formatted. We should remove the trailing part off of any embedded + * matrix parameter value. + */ + static void formatMatrixParams(MultivaluedMap matrixParams) { + if (matrixParams != null && !matrixParams.isEmpty()) { + Collection> valueColl = matrixParams.values(); + for (List values : valueColl) { + if (values != null && !values.isEmpty()) { + int i = 0; + for (String value : values) { + + // if we find a '/' and it is not the end of the string, + // we'll take it off and replace it with the value before + // the '/' + if (value.indexOf("/") != -1 + && value.indexOf("/") != value.length() - 1) { + value = values.remove(i); + value = value.substring(0, value.indexOf("/")); + values.add(i, value); + } + } + } + } + } + } + + public static Object createContextValue(RESTContext context, Type genericType, Class clazz) { + + Object o = null; + if (UriInfo.class.equals(clazz)) { + o = new UriInfoImpl(context); + } else if (HttpHeaders.class.equals(clazz)) { + o = new HttpHeadersImpl(context); + } else if (Request.class.equals(clazz)) { + o = new RequestImpl(context); + } else if (SecurityContext.class.equals(clazz)) { + o = new SecurityContextImpl(context); + } else if (ContextResolver.class.equals(clazz)) { + o = createContextResolver(genericType, context); + } else if (Providers.class.equals(clazz)) { + o = new ProvidersImpl(context); + } + + return o == null ? createServletResourceValue(context, clazz) : o; + } + + public static ContextResolver createContextResolver(Type genericType, RESTContext context) { + if (genericType instanceof ParameterizedType) { + return ProviderFactory + .getInstance(context) + .createContextResolver( + ((ParameterizedType) genericType) + .getActualTypeArguments()[0], null, context); + } + return null; + } + + public static Object createServletResourceValue(RESTContext context, Class clazz) { + if (HttpServletRequest.class.equals(clazz)) { + return context.getProperty(ContextConstants.SERVLET_REQUEST); + } + if (HttpServletResponse.class.equals(clazz)) { + return context.getProperty(ContextConstants.SERVLET_RESPONSE); + } + if (ServletContext.class.equals(clazz)) { + return context.getProperty(ContextConstants.SERVLET_CONTEXT); + } + if (ServletConfig.class.equals(clazz)) { + return context.getProperty(ContextConstants.SERVLET_CONFIG); + } + + return null; + } + + /** + * Retrieve map of query parameters from the passed in message + * + * @param message + * @return a Map of query parameters. + */ + + public static MultivaluedMap getStructuredParams(String query, String sep, boolean decode) { + MultivaluedMap queries = new MetadataMap( + new LinkedHashMap>()); + + if (!StringUtils.isEmpty(query)) { + List parts = Arrays.asList(query.split(sep)); + for (String part : parts) { + String[] values = part.split("="); + queries.add(decode ? uriDecode(values[0]) : values[0], + values.length == 1 ? "" : decode ? uriDecode(values[1]) + : values[1]); + } + } + return queries; + } + + /** + * This method will accept an array of Objects and return a List that + * contains the result of calling 'toString' on each of the Objects. + * + */ + + public static List convertToStringList(Object... values) { + List stringList = new ArrayList(); + for (Object value : values) { + stringList.add(value.toString()); + } + return stringList; + } + + public static String uriDecode(String query) { + try { + return URLDecoder.decode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Swallow unsupported decoding exception + } + return query; + } + + @SuppressWarnings("unchecked") + private static Object readFromMessageBody(Class targetTypeClass, Type parameterType, Annotation[] parameterAnnotations, InputStream is, MediaType contentType, OperationResourceInfo ori, final RESTContext context) + throws IOException { + + // OperationResourceInfo is passed for error logging reasons. We only use the consumesTypes for real function: + List consumeTypes = ori.getConsumes(); + + List types = JAXRSUtils.intersectMimeTypes(consumeTypes, + contentType); + MessageBodyReader provider = null; + for (MediaType type : types) { + provider = ProviderFactory.getInstance(context) + .createMessageBodyReader(targetTypeClass, parameterType, + parameterAnnotations, type, context); + + // TODO : make the exceptions + if (provider != null) { + break; + } + } + MultivaluedMap headers = new MultivaluedMap() { + + private MultivaluedMap delegate = (MultivaluedMap) context + .getProperty(ContextConstants.HTTP_HEADER_VALUES); + + public void add(String arg0, String arg1) { + throw new UnsupportedOperationException(); + } + + public String getFirst(String arg0) { + return delegate.getFirst(arg0.toLowerCase()); + } + + public void putSingle(String arg0, String arg1) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public boolean containsKey(Object key) { + if (key instanceof String) { + return delegate.containsKey(((String) key).toLowerCase()); + } + return delegate.containsKey(key); + } + + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + public Set>> entrySet() { + Set>> ret = delegate + .entrySet(); + if (ret == null) { + return null; + } + return Collections.unmodifiableSet(ret); + } + + public List get(Object key) { + List ret = null; + if (key instanceof String) { + ret = delegate.get(((String) key).toLowerCase()); + } else { + ret = delegate.get(key); + } + + if (ret != null) { + return Collections.unmodifiableList(ret); + } + return ret; + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public Set keySet() { + return delegate.keySet(); + } + + public List put(String key, List value) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map> map) { + throw new UnsupportedOperationException(); + } + + public List remove(Object key) { + throw new UnsupportedOperationException(); + } + + public int size() { + return delegate.size(); + } + + public Collection> values() { + return delegate.values(); + } + + }; + + if (provider != null) { + if (log.isDebugEnabled()) { + log.debug("Attempting to use MessageBodyReader: " + provider + + " to read from message"); + } + try { + Object obj = provider.readFrom(targetTypeClass, parameterType, + parameterAnnotations, contentType, headers, is); + return obj; + } finally { + // log the input if logging enabled + if (log.isDebugEnabled()) { + logInput(context); + } + } + } + + // if we could not find a MessageBodyReader to handle the type, let's look + // for a DataHandler via the JAF + if (log.isDebugEnabled()) { + log.debug("Attempting to use DataHandler to read from message"); + } + + byte[] bytes = getBytesFromStream(is); + RESTDataSource source = new RESTDataSource(bytes, null, contentType + .toString()); + DataHandler handler = new DataHandler(source); + Object obj = handler.getContent(); + + // if there was no DataHandler for the Object type, the InputStream + // will + // simply be returned + if (!(obj instanceof InputStream)) { + if (log.isDebugEnabled()) { + log + .debug("Successfully read message with DataHandler for mimeType: " + + contentType.toString()); + logInput(context); + } + return obj; + } + + // at this point, no reader or data handler was found for the method parameter. Log it and throw 415 + if (log.isErrorEnabled()) { + log.error(Messages.getMessage("readerOrHandlerNotFound", + (String) context.getProperty(ContextConstants.HTTP_METHOD), + (String) context + .getProperty(ContextConstants.HTTP_REQUEST_URL), + ori.getClassResourceInfo().getResourceClass().getName() + + "." + ori.getMethodToInvoke().getName(), + parameterType.toString(), contentType.toString())); + } + throw new WebApplicationException(415); + + } + + /** + * Convenience method to log the contents of the request InputStream. This + * method should only be called when logging is enabled. + */ + static void logInput(RESTContext context) { + InputStream is = (InputStream) context + .getProperty(ContextConstants.REQUEST_INPUT_STREAM); + if (is instanceof LogInputStream) { + ((LogInputStream) is).logBytes(); + } + } + + /** + * This method will be used to get an array of bytes from an InputStream. + */ + static byte[] getBytesFromStream(InputStream inputStream) + throws IOException { + byte[] inputBytes = new byte[inputStream.available()]; + int next = inputStream.read(); + int i = 0; + while (next != -1) { + if (i == inputBytes.length) { + int size = i == 0 ? 1 : i; + byte[] inputCopy = new byte[2 * size]; + System + .arraycopy(inputBytes, 0, inputCopy, 0, + inputBytes.length); + inputBytes = inputCopy; + } + inputBytes[i] = (byte) next; + next = inputStream.read(); + i++; + } + return inputBytes; + } + + public static boolean matchContentMediaMimeTypes(List oriMediaTypes, MediaType contentType) { + if (contentType == null) { + if (oriMediaTypes == null + || (oriMediaTypes.isEmpty() || oriMediaTypes + .contains(MediaType.WILDCARD_TYPE))) { + return true; + } + return false; + } + return intersectMimeTypes(oriMediaTypes, contentType).size() != 0; + } + + public static List parseMediaTypes(String types) { + List acceptValues = new ArrayList(); + + if (types != null) { + String parsedTypes = types; + while (parsedTypes.length() > 0) { + String tp = parsedTypes; + int index = parsedTypes.indexOf(','); + if (index != -1) { + tp = parsedTypes.substring(0, index); + parsedTypes = parsedTypes.substring(index + 1).trim(); + } else { + parsedTypes = ""; + } + acceptValues.add(MediaType.valueOf(tp)); + } + } else { + acceptValues.add(MediaType.WILDCARD_TYPE); + } + + return acceptValues; + } + + /** + * Intersect two mime types lists, preferring to keep the media type parameters on the second list of media types. + * + * @param requiredMediaTypes + * @param userMediaTypes + * @return return a list of intersected mime types but which has no sorted order + */ + public static List intersectMimeTypes(List requiredMediaTypes, List userMediaTypes) { + Set supportedMimeTypeList = new LinkedHashSet(); + + for (MediaType requiredType : requiredMediaTypes) { + for (MediaType userType : userMediaTypes) { + if (requiredType.isCompatible(userType)) { + /* + * TODO: should not add types where the quality parameter is 0.0 + */ + String type = requiredType.getType().equals( + MediaType.MEDIA_TYPE_WILDCARD) ? userType.getType() + : requiredType.getType(); + String subtype = requiredType.getSubtype().equals( + MediaType.MEDIA_TYPE_WILDCARD) ? userType + .getSubtype() : requiredType.getSubtype(); + + /* + * go ahead and add in the userType parameters but go back + * and add in any requiredType's parameters that do not + * conflict + */ + Map typeParams = new HashMap( + userType.getParameters()); + Map requiredTypeParameters = requiredType + .getParameters(); + for (String key : requiredTypeParameters.keySet()) { + if (!typeParams.containsKey(key)) { + typeParams + .put(key, requiredTypeParameters.get(key)); + } + } + + supportedMimeTypeList.add(new MediaType(type, subtype, + typeParams)); + } + } + } + + return new ArrayList(supportedMimeTypeList); + } + + public static List intersectMimeTypes(List mimeTypesA, MediaType mimeTypeB) { + return intersectMimeTypes(mimeTypesA, Collections + .singletonList(mimeTypeB)); + } + + public static List intersectMimeTypes(String mimeTypesA, String mimeTypesB) { + return intersectMimeTypes(parseMediaTypes(mimeTypesA), + parseMediaTypes(mimeTypesB)); + } + + public static List sortMediaTypes(String mediaTypes) { + return sortMediaTypes(JAXRSUtils.parseMediaTypes(mediaTypes)); + } + + public static List sortMediaTypesQValue(String mediaTypes) { + return sortMediaTypesQValue(JAXRSUtils.parseMediaTypes(mediaTypes)); + } + + public static List sortMediaTypes(List types) { + if (types.size() > 1) { + Collections.sort(types, new Comparator() { + + public int compare(MediaType mt1, MediaType mt2) { + return JAXRSUtils.compareMediaTypes(mt1, mt2); + } + + }); + } + return types; + } + + public static List sortMediaTypesQValue(List types) { + if (types.size() > 1) { + Collections.sort(types, new Comparator() { + + public int compare(MediaType mt1, MediaType mt2) { + return JAXRSUtils.compareMediaTypesQValue(mt1, mt2); + } + + }); + } + return types; + } + + private static MultivaluedMap cloneMap(MultivaluedMap map1) { + + MultivaluedMap map2 = new MetadataMap(); + for (Map.Entry> entry : map1.entrySet()) { + map2.put(entry.getKey(), new ArrayList(entry.getValue())); + } + return map2; + } + + /** + * This utility method will drive the processing of a response to a + * request sent to our REST runtime. + * + */ + public static void processResponse(RESTContext context) throws Exception { + // if servlet response is already committed, break out + HttpServletResponse servletResponse = (HttpServletResponse) context + .getProperty(ContextConstants.SERVLET_RESPONSE); + if (servletResponse != null && servletResponse.isCommitted()) { + // don't process the response any further + // but flush the buffer one last time + // in case there was some tailing output that hasn't been flushed yet + servletResponse.flushBuffer(); + return; + } + + // grab the needed objects + Object responseObj = context + .getProperty(ContextConstants.INVOCATION_RESULT); + OperationResourceInfo operation = (OperationResourceInfo) context + .getProperty(ContextConstants.OPERATION_RESOURCE); + List acceptContentTypes = (List) context + .getProperty(ContextConstants.ACCEPT_CONTENT_TYPES); + + Response response = (Response) context + .getProperty(ContextConstants.RESPONSE); + + ContentMonitor monitor = (ContentMonitor) context + .getProperty(ContextConstants.CONTENT_MONITOR); + + // in some cases, such as error flows, the response may already have been + // built and set on the RESTContext + if (response == null) { + if (responseObj instanceof Response) { + response = (Response) responseObj; + } else if (responseObj == null) { + response = Response.noContent().build(); + } else { + response = Response.ok(responseObj).build(); + } + } + + String path = (String) context + .getProperty(ContextConstants.HTTP_PATH_INFO); + String httpMethod = (String) context + .getProperty(ContextConstants.HTTP_METHOD); + + String contentType = (String) response.getMetadata().getFirst( + HttpHeaders.CONTENT_TYPE); + context.setProperty(ContextConstants.RESPONSE, response); + context.setProperty(ContextConstants.HTTP_HEADER_VALUES, response + .getMetadata()); + OutputStream os = (OutputStream) context + .getProperty(ContextConstants.RESPONSE_OUTPUT_STREAM); + + ResponseWriter responseWriter = (ResponseWriter) IntegrationRegistry + .getIntegrationProvider( + context + .getProperty(ContextConstants.INTEGRATION_REGISTRATION_KEY), + ResponseWriter.class); + if (responseWriter == null) { + responseWriter = new DefaultResponseWriter(); + } + if (log.isDebugEnabled()) { + log.debug("Found this ResponseWriter: " + responseWriter); + } + + responseWriter.preWrite(context); + + responseObj = response.getEntity(); + if (responseObj == null) { + responseWriter.postWrite(context); + return; + } + + List availableContentTypes = computeAvailableContentTypes( + contentType, response, operation, acceptContentTypes); + + // if we did not find interesecting content types AND this is an options + // request, we have two options: first, check the Accept header and set + // that to the content type if it is present, otherwise, the */* type + // becomes the content type + if (availableContentTypes == null || availableContentTypes.isEmpty()) { + + if ("OPTIONS".equalsIgnoreCase(httpMethod)) { + if (acceptContentTypes != null && !acceptContentTypes.isEmpty()) { + availableContentTypes = acceptContentTypes; + } else { + availableContentTypes = new ArrayList(); + availableContentTypes.add(MediaType.WILDCARD_TYPE); + } + } + } + + Method invoked = operation == null ? null : operation + .getMethodToInvoke(); + Type genericType = invoked != null ? invoked.getGenericReturnType() + : null; + Object objToInvokeWriterWith = responseObj; + Class targetType = responseObj.getClass(); + if (GenericEntity.class.isAssignableFrom(responseObj.getClass())) { + GenericEntity ge = (GenericEntity) responseObj; + targetType = ge.getRawType(); + genericType = ge.getType(); + objToInvokeWriterWith = ge.getEntity(); + } else if (invoked != null) { + if (invoked.getGenericReturnType() instanceof Class) { + Class retType = (Class) invoked.getGenericReturnType(); + if (Response.class.isAssignableFrom(retType)) { + targetType = responseObj.getClass(); + genericType = responseObj.getClass(); + } + } + } + + MessageBodyWriter writer = null; + MediaType responseType = null; + + boolean written = false; + try { + for (MediaType type : availableContentTypes) { + writer = ProviderFactory.getInstance(context) + .createMessageBodyWriter( + targetType, + genericType, + invoked != null ? invoked.getAnnotations() + : new Annotation[] {}, type, context); + + if (writer != null) { + responseType = (MediaType) context + .getProperty(ContextConstants.RESPONSE_CONTENT_TYPE); + break; + } + } + + if (writer != null) { + responseType = checkFinalContentType(responseType); + responseWriter.writeWithEntityProvider(context, writer, + objToInvokeWriterWith, targetType, genericType, + (invoked != null ? invoked.getAnnotations() + : new Annotation[] {}), responseType, response + .getMetadata(), os); + written = true; + } + // if we could not find a MessageBodyWriter via JAF, let's find a + // DataHandler via the JAF + else if (!written) { + for (MediaType type : availableContentTypes) { + DataContentHandler dch = CommandMap.getDefaultCommandMap() + .createDataContentHandler(type.toString()); + if (dch != null) { + if (log.isDebugEnabled()) { + log + .debug("Writing application response with DataHandler for mimeType: " + + type.toString()); + } + responseWriter.writeWithDataHandler(context, + responseObj, type.toString(), dch, os); + written = true; + break; + } + } + } + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.debug("Throwable caught during processResponse: " + t, t); + } + written = processWriteException(context, t); + } + + if (!written) { + // give as much info in the log as possible so app developer knows what needs to be implemented + String invokedString = getMethodInfoForErrorLog(invoked); + log.error(Messages.getMessage("writerNotFound", httpMethod, path, + invokedString, targetType.getName())); + context.setProperty(ContextConstants.RESPONSE, Response.status( + Response.Status.INTERNAL_SERVER_ERROR).build()); + processResponse(context); + } + + // let's try to set the content length of the response + if (monitor != null) { + Integer contentLength = monitor.getContentLength(); + if (log.isDebugEnabled()) { + log.debug("Setting response content length: " + contentLength); + } + context.setProperty(ContextConstants.RESPONSE_CONTENT_LENGTH, + contentLength); + } + responseWriter.postWrite(context); + } + + /** + * This processes an exception that occurs during response serialization. If + * the exception has not been mapped, we'll attempt to map it and allow the + * mapper to handle it. Otherwise, the exception will be thrown to the underlying + * container. If this is an exception that is occurring from processing an + * exception response, we will throw an error indicating a 500 status. + */ + static boolean processWriteException(RESTContext context, Throwable t) + throws Exception { + Boolean mappedException = (Boolean) context + .getProperty(ContextConstants.MAPPED_EXCEPTION); + + if (log.isErrorEnabled()) { + log.error(t.getMessage(), t); + } + + // if this is not an error that occurred while processing a previous error + if (mappedException == null || !mappedException.booleanValue()) { + ExceptionMapper mapper = ProviderFactory.getInstance(context) + .createExceptionMapper(t.getClass(), context); + + // if we find the mapper, let it handle the exception + if (mapper != null) { + Response response = null; + try { + response = mapper.toResponse(t); + } catch (Throwable t2) { + if (log.isDebugEnabled()) { + log.debug(mapper.getClass().getName() + ".toResponse(" + + t.getClass() + ") threw throwable " + + t2.getClass().getName() + " with message \"" + + t2.getMessage() + "\""); + } + response = Response.status( + Response.Status.INTERNAL_SERVER_ERROR).build(); + } + context.setProperty(ContextConstants.RESPONSE, response); + context.setProperty(ContextConstants.MAPPED_EXCEPTION, + Boolean.TRUE); + processResponse(context); + return true; + } + + if (log.isDebugEnabled()) { + log + .debug("ExceptionMapper could not be found for the throwable class: " + + t.getClass()); + } + + /* + * need to re-throw again if there was no mapper to let the underlying container handle the "real" exception + */ + if (t instanceof Exception) { + throw (Exception) t; + } + throw (Error) t; + } + + // if this exception occurred serializing an exception response, just + // return a 500 status code + else if (mappedException.booleanValue()) { + if (log.isDebugEnabled()) { + log + .debug("An exception was already thrown once and mapped to a response so returning a 500 Internal server error"); + } + + context.setProperty(ContextConstants.MAPPED_EXCEPTION, null); + context.setProperty(ContextConstants.RESPONSE, Response.status( + Response.Status.INTERNAL_SERVER_ERROR).build()); + return true; + } + return false; + } + + /** + * This method will construct a String that represents a MIME type and representation + * class for hte MIME type. This will be used to construct a DataFlavor when dealing + * with the JAF layer. + * + */ + public static String constructFlavorString(String mimeType, String className) { + return mimeType + ";class=" + className; + } + + /** + * Determines the response's media type. + * @param contentType the Response object's content type + * @param response the actual Response object + * @param operation the operation resource info + * @param acceptContentTypes the request's Accept header media types in sorted order + * @return a list of media types that are acceptable + */ + public static List computeAvailableContentTypes(String contentType, Response response, OperationResourceInfo operation, List acceptContentTypes) { + List produceTypes = null; + List operationProduces = (operation != null) ? operation + .getProduces() : null; + if (contentType != null) { + return Collections.singletonList(MediaType.valueOf(contentType + .toString())); + } else if (operationProduces != null) { + produceTypes = operationProduces; + } else { + produceTypes = Collections.singletonList(MediaType.WILDCARD_TYPE); + } + List acceptTypes = acceptContentTypes; + if (acceptTypes == null) { + acceptTypes = Collections.singletonList(MediaType.WILDCARD_TYPE); + } + return JAXRSUtils.intersectMimeTypes(acceptTypes, produceTypes); + + } + + public static MediaType checkFinalContentType(MediaType mt) { + if (mt.isWildcardType() && mt.isWildcardSubtype()) { + return MediaType.APPLICATION_OCTET_STREAM_TYPE; + } else if ("application".equals(mt.getType()) + && "*".equals(mt.getSubtype())) { + return MediaType.APPLICATION_OCTET_STREAM_TYPE; + } else { + return mt; + } + } + + /** + * Helper method to return a ClassResourceInfo for the class name supplied by + * looking at all the subresources on the ClassResourceInfo list supplied. + * + */ + public static ClassResourceInfo getSubResourceInfo(String className, List classInfoList) { + ClassResourceInfo targetInfo = null; + for (ClassResourceInfo classInfo : classInfoList) { + List subInfoList = classInfo + .getSubClassResourceInfo(); + if (subInfoList != null) { + for (ClassResourceInfo subResource : subInfoList) { + if (subResource.getServiceClass().getName().equals( + className)) { + targetInfo = subResource; + break; + } + } + } + } + return targetInfo; + } + + /** + * This is a helper method to determine the matched parts of an OperationResourceInfo + * objects uri template with the final matched group that is supplied. + * + */ + public static String getPathForOperation(String finalGroup, String currentPath) { + + String pathForOp = ""; + if (finalGroup == null || finalGroup.equals("/")) { + pathForOp = currentPath; + } else if (currentPath.indexOf(finalGroup) != -1) { + pathForOp = currentPath.substring(0, currentPath + .lastIndexOf(finalGroup)); + } + + pathForOp = removePrecedingSlash(pathForOp); + pathForOp = removeTrailingSlash(pathForOp); + + return pathForOp; + } + + /** + * This will remove a preceding '/', if one exists, from a String. + * + */ + public static String removePrecedingSlash(String input) { + if (input != null && input.startsWith("/") && !input.equals("/")) { + return input.substring(1); + } + return input; + } + + /** + * This will remove a trailing '/', if one exists, from a String. + * + */ + public static String removeTrailingSlash(String input) { + if (input.endsWith("/") && !input.equals("/")) { + return input.substring(0, input.length() - 1); + } + return input; + } + + public static List getProviderTypes(Class providerClass) { + java.lang.reflect.Type[] interfaceTypes = providerClass + .getGenericInterfaces(); + List providerTypes = null; + if (interfaceTypes != null) { + providerTypes = new ArrayList(); + for (java.lang.reflect.Type interfaceType : interfaceTypes) { + if (interfaceType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) interfaceType; + if (parameterizedType.getRawType() instanceof Class) { + Class interfaze = (Class) parameterizedType + .getRawType(); + + // first determine the type of provider this repersents + if (MessageBodyWriter.class.isAssignableFrom(interfaze)) { + providerTypes + .add(ProviderInfo.Type.MessageBodyWriter); + } + if (MessageBodyReader.class.isAssignableFrom(interfaze)) { + providerTypes + .add(ProviderInfo.Type.MessageBodyReader); + } + if (ContextResolver.class.isAssignableFrom(interfaze)) { + providerTypes + .add(ProviderInfo.Type.ContextResolver); + } + if (ExceptionMapper.class.isAssignableFrom(interfaze)) { + providerTypes + .add(ProviderInfo.Type.ExceptionMapper); + } + } + } + } + } + return providerTypes; + } + + /* + * Looks for an OptionsResponseProvider based on the 'Accept' media types. + */ + public static OptionsResponseProvider getOptionsResponseProvider(RESTContext context) { + List mTypes = (List) context + .getProperty(ContextConstants.ACCEPT_CONTENT_TYPES); + if (mTypes == null) { + mTypes = new ArrayList(); + mTypes.add(MediaType.WILDCARD_TYPE); + } + List providers = (List) IntegrationRegistry + .getIntegrationProvider( + context + .getProperty(ContextConstants.INTEGRATION_REGISTRATION_KEY), + OptionsResponseProvider.class); + OptionsResponseProvider provider = null; + if (providers != null) { + for (MediaType mType : mTypes) { + for (OptionsResponseProvider orp : providers) { + if (orp.isWriteable(mType)) { + provider = orp; + break; + } + } + if (provider != null) { + break; + } + } + } + return provider; + } + + /** + * Utility method to give JAX-RS app developer as much info as possible about the method being invoked + * @return + */ + private static String getMethodInfoForErrorLog(Method method) { + String methodSignatureString = "UNKNOWN"; + if (method != null) { + String paramTypesString = ""; + for (int i = 0; i < method.getParameterTypes().length; i++) { + paramTypesString += method.getParameterTypes()[i].getName(); + if ((i + 1) != method.getParameterTypes().length) { + paramTypesString += ", "; + } + } + methodSignatureString = method.getDeclaringClass().getName() + "." + + method.getName() + "(" + paramTypesString + ")"; + } + return methodSignatureString; + } + +}