This is an automated email from the ASF dual-hosted git repository. dblevins pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 03813826979a53a17f77b38545cb562fd9c7d57f Author: David Blevins <[email protected]> AuthorDate: Mon Apr 12 21:35:02 2021 -0700 Unmodified CXF source. Will be patched shortly for TOMEE-3157 TOMEE-3158 --- .../org/apache/cxf/jaxrs/utils/JAXRSUtils.java | 2008 ++++++++++++++++++++ 1 file changed, 2008 insertions(+) diff --git a/tomee/apache-tomee/src/patch/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java b/tomee/apache-tomee/src/patch/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java new file mode 100644 index 0000000..585711d --- /dev/null +++ b/tomee/apache-tomee/src/patch/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java @@ -0,0 +1,2008 @@ +/** + * 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.Reader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.Consumes; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.ResourceContext; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Cookie; +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.Response.ResponseBuilder; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.StreamingOutput; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Providers; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; +import javax.xml.namespace.QName; + +import org.apache.cxf.common.i18n.BundleUtils; +import org.apache.cxf.common.logging.LogUtils; +import org.apache.cxf.common.util.PackageUtils; +import org.apache.cxf.common.util.PropertyUtils; +import org.apache.cxf.common.util.ReflectionUtil; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.helpers.DOMUtils; +import org.apache.cxf.helpers.IOUtils; +import org.apache.cxf.helpers.LoadingByteArrayOutputStream; +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.io.CacheAndWriteOutputStream; +import org.apache.cxf.io.ReaderInputStream; +import org.apache.cxf.jaxrs.JAXRSServiceImpl; +import org.apache.cxf.jaxrs.ext.ContextProvider; +import org.apache.cxf.jaxrs.ext.DefaultMethod; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.jaxrs.ext.MessageContextImpl; +import org.apache.cxf.jaxrs.ext.ProtocolHeaders; +import org.apache.cxf.jaxrs.ext.ProtocolHeadersImpl; +import org.apache.cxf.jaxrs.ext.multipart.MultipartBody; +import org.apache.cxf.jaxrs.impl.AsyncResponseImpl; +import org.apache.cxf.jaxrs.impl.ContainerRequestContextImpl; +import org.apache.cxf.jaxrs.impl.ContainerResponseContextImpl; +import org.apache.cxf.jaxrs.impl.HttpHeadersImpl; +import org.apache.cxf.jaxrs.impl.MediaTypeHeaderProvider; +import org.apache.cxf.jaxrs.impl.MetadataMap; +import org.apache.cxf.jaxrs.impl.PathSegmentImpl; +import org.apache.cxf.jaxrs.impl.ProvidersImpl; +import org.apache.cxf.jaxrs.impl.ReaderInterceptorContextImpl; +import org.apache.cxf.jaxrs.impl.ReaderInterceptorMBR; +import org.apache.cxf.jaxrs.impl.RequestImpl; +import org.apache.cxf.jaxrs.impl.ResourceContextImpl; +import org.apache.cxf.jaxrs.impl.ResourceInfoImpl; +import org.apache.cxf.jaxrs.impl.ResponseBuilderImpl; +import org.apache.cxf.jaxrs.impl.ResponseImpl; +import org.apache.cxf.jaxrs.impl.SecurityContextImpl; +import org.apache.cxf.jaxrs.impl.UriInfoImpl; +import org.apache.cxf.jaxrs.impl.WriterInterceptorContextImpl; +import org.apache.cxf.jaxrs.impl.WriterInterceptorMBW; +import org.apache.cxf.jaxrs.model.BeanParamInfo; +import org.apache.cxf.jaxrs.model.BeanResourceInfo; +import org.apache.cxf.jaxrs.model.ClassResourceInfo; +import org.apache.cxf.jaxrs.model.ClassResourceInfoComparator; +import org.apache.cxf.jaxrs.model.MethodInvocationInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfoComparator; +import org.apache.cxf.jaxrs.model.OperationResourceInfoStack; +import org.apache.cxf.jaxrs.model.Parameter; +import org.apache.cxf.jaxrs.model.ParameterType; +import org.apache.cxf.jaxrs.model.ProviderInfo; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.cxf.jaxrs.provider.AbstractConfigurableProvider; +import org.apache.cxf.jaxrs.provider.ProviderFactory; +import org.apache.cxf.jaxrs.provider.ServerProviderFactory; +import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils; +import org.apache.cxf.message.Exchange; +import org.apache.cxf.message.Message; +import org.apache.cxf.message.MessageUtils; +import org.apache.cxf.phase.PhaseInterceptorChain; +import org.apache.cxf.service.Service; + +public final class JAXRSUtils { + + public static final MediaType ALL_TYPES = new MediaType(); + public static final String ROOT_RESOURCE_CLASS = "root.resource.class"; + public static final String IGNORE_MESSAGE_WRITERS = "ignore.message.writers"; + public static final String ROOT_INSTANCE = "service.root.instance"; + public static final String ROOT_PROVIDER = "service.root.provider"; + public static final String EXCEPTION_FROM_MAPPER = "exception.from.mapper"; + public static final String SECOND_JAXRS_EXCEPTION = "second.jaxrs.exception"; + public static final String PARTIAL_HIERARCHICAL_MEDIA_SUBTYPE_CHECK = + "media.subtype.partial.check"; + public static final String DOC_LOCATION = "wadl.location"; + public static final String MEDIA_TYPE_Q_PARAM = "q"; + public static final String MEDIA_TYPE_QS_PARAM = "qs"; + private static final String MEDIA_TYPE_DISTANCE_PARAM = "d"; + private static final String DEFAULT_CONTENT_TYPE = "default.content.type"; + private static final String KEEP_SUBRESOURCE_CANDIDATES = "keep.subresource.candidates"; + private static final Logger LOG = LogUtils.getL7dLogger(JAXRSUtils.class); + private static final ResourceBundle BUNDLE = BundleUtils.getBundle(JAXRSUtils.class); + private static final String PATH_SEGMENT_SEP = "/"; + private static final String REPORT_FAULT_MESSAGE_PROPERTY = "org.apache.cxf.jaxrs.report-fault-message"; + private static final String NO_CONTENT_EXCEPTION = "javax.ws.rs.core.NoContentException"; + private static final String HTTP_CHARSET_PARAM = "charset"; + private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; + private static final Set<Class<?>> STREAMING_OUT_TYPES = new HashSet<>( + Arrays.asList(InputStream.class, Reader.class, StreamingOutput.class)); + + private JAXRSUtils() { + } + + public static boolean isStreamingOutType(Class<?> type) { + return STREAMING_OUT_TYPES.contains(type); + } + + public static List<PathSegment> getPathSegments(String thePath, boolean decode) { + return getPathSegments(thePath, decode, true); + } + + public static List<PathSegment> getPathSegments(String thePath, boolean decode, + boolean ignoreLastSlash) { + List<PathSegment> theList = + Arrays.asList(thePath.split("/")).stream() + .filter(StringUtils.notEmpty()) + .map(p -> new PathSegmentImpl(p, decode)) + .collect(Collectors.toList()); + + int len = thePath.length(); + if (len > 0 && thePath.charAt(len - 1) == '/') { + String value = ignoreLastSlash ? "" : "/"; + theList.add(new PathSegmentImpl(value, false)); + } + return theList; + } + + private static String[] getUserMediaTypes(Object provider, boolean consumes) { + String[] values = null; + if (AbstractConfigurableProvider.class.isAssignableFrom(provider.getClass())) { + List<String> types = null; + if (consumes) { + types = ((AbstractConfigurableProvider)provider).getConsumeMediaTypes(); + } else { + types = ((AbstractConfigurableProvider)provider).getProduceMediaTypes(); + } + if (types != null) { + values = !types.isEmpty() ? types.toArray(new String[0]) + : new String[]{"*/*"}; + } + } + return values; + } + + public static List<MediaType> getProviderConsumeTypes(MessageBodyReader<?> provider) { + String[] values = getUserMediaTypes(provider, true); + + if (values == null) { + return getConsumeTypes(provider.getClass().getAnnotation(Consumes.class)); + } + return JAXRSUtils.getMediaTypes(values); + } + + public static List<MediaType> getProviderProduceTypes(MessageBodyWriter<?> provider) { + String[] values = getUserMediaTypes(provider, false); + if (values == null) { + return getProduceTypes(provider.getClass().getAnnotation(Produces.class)); + } + return JAXRSUtils.getMediaTypes(values); + } + + public static List<MediaType> getMediaTypes(String[] values) { + List<MediaType> supportedMimeTypes = new ArrayList<>(values.length); + for (int i = 0; i < values.length; i++) { + supportedMimeTypes.addAll(parseMediaTypes(values[i])); + } + return supportedMimeTypes; + } + + public static void injectParameters(OperationResourceInfo ori, + Object requestObject, + Message message) { + injectParameters(ori, ori.getClassResourceInfo(), requestObject, message); + } + + @SuppressWarnings("unchecked") + public static void injectParameters(OperationResourceInfo ori, + BeanResourceInfo bri, + Object requestObject, + Message message) { + + if (bri.isSingleton() + && (!bri.getParameterMethods().isEmpty() || !bri.getParameterFields().isEmpty())) { + LOG.fine("Injecting request parameters into singleton resource is not thread-safe"); + } + // Param methods + MultivaluedMap<String, String> values = + (MultivaluedMap<String, String>)message.get(URITemplate.TEMPLATE_PARAMETERS); + for (Method m : bri.getParameterMethods()) { + Parameter p = ResourceUtils.getParameter(0, m.getAnnotations(), + m.getParameterTypes()[0]); + Object o; + + if (p.getType() == ParameterType.BEAN) { + o = createBeanParamValue(message, m.getParameterTypes()[0], ori); + } else { + o = createHttpParameterValue(p, + m.getParameterTypes()[0], + m.getGenericParameterTypes()[0], + m.getParameterAnnotations()[0], + message, + values, + ori); + } + InjectionUtils.injectThroughMethod(requestObject, m, o, message); + } + // Param fields + for (Field f : bri.getParameterFields()) { + Parameter p = ResourceUtils.getParameter(0, f.getAnnotations(), + f.getType()); + Object o = null; + + if (p.getType() == ParameterType.BEAN) { + o = createBeanParamValue(message, f.getType(), ori); + } else { + o = createHttpParameterValue(p, + f.getType(), + f.getGenericType(), + f.getAnnotations(), + message, + values, + ori); + } + InjectionUtils.injectFieldValue(f, requestObject, o); + } + } + + public static Map<ClassResourceInfo, MultivaluedMap<String, String>> selectResourceClass( + List<ClassResourceInfo> resources, String path, Message message) { + + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("START_CRI_MATCH", + BUNDLE, + path).toString()); + if (resources.size() == 1) { + MultivaluedMap<String, String> values = new MetadataMap<>(); + return resources.get(0).getURITemplate().match(path, values) + ? Collections.singletonMap(resources.get(0), values) : null; + } + + SortedMap<ClassResourceInfo, MultivaluedMap<String, String>> candidateList = + new TreeMap<ClassResourceInfo, MultivaluedMap<String, String>>( + new ClassResourceInfoComparator(message)); + + for (ClassResourceInfo cri : resources) { + MultivaluedMap<String, String> map = new MetadataMap<>(); + if (cri.getURITemplate().match(path, map)) { + candidateList.put(cri, map); + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("CRI_SELECTED_POSSIBLY", + BUNDLE, + cri.getServiceClass().getName(), + path, + cri.getURITemplate().getValue()).toString()); + } else { + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("CRI_NO_MATCH", + BUNDLE, + path, + cri.getServiceClass().getName()).toString()); + } + } + + if (!candidateList.isEmpty()) { + Map<ClassResourceInfo, MultivaluedMap<String, String>> cris = + new LinkedHashMap<>(candidateList.size()); + ClassResourceInfo firstCri = null; + for (Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> entry : candidateList.entrySet()) { + ClassResourceInfo cri = entry.getKey(); + if (cris.isEmpty()) { + firstCri = cri; + cris.put(cri, entry.getValue()); + } else if (firstCri != null + && URITemplate.compareTemplates(firstCri.getURITemplate(), cri.getURITemplate()) == 0) { + cris.put(cri, entry.getValue()); + } else { + break; + } + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("CRI_SELECTED", + BUNDLE, + cri.getServiceClass().getName(), + path, cri.getURITemplate().getValue()).toString()); + } + return cris; + } + + return null; + } + public static OperationResourceInfo findTargetMethod( + Map<ClassResourceInfo, MultivaluedMap<String, String>> matchedResources, + Message message, + String httpMethod, + MultivaluedMap<String, String> matchedValues, + String requestContentType, + List<MediaType> acceptContentTypes) { + return findTargetMethod(matchedResources, message, httpMethod, matchedValues, + requestContentType, acceptContentTypes, true, true); + } + //CHECKSTYLE:OFF + public static OperationResourceInfo findTargetMethod( + Map<ClassResourceInfo, MultivaluedMap<String, String>> matchedResources, + Message message, + String httpMethod, + MultivaluedMap<String, String> matchedValues, + String requestContentType, + List<MediaType> acceptContentTypes, + boolean throwException, + boolean recordMatchedUri) { + //CHECKSTYLE:ON + final boolean getMethod = HttpMethod.GET.equals(httpMethod); + + MediaType requestType; + try { + requestType = toMediaType(requestContentType); + } catch (IllegalArgumentException ex) { + throw ExceptionUtils.toNotSupportedException(ex, null); + } + + SortedMap<OperationResourceInfo, MultivaluedMap<String, String>> candidateList = + new TreeMap<OperationResourceInfo, MultivaluedMap<String, String>>( + new OperationResourceInfoComparator(message, httpMethod, + getMethod, requestType, acceptContentTypes)); + + int pathMatched = 0; + int methodMatched = 0; + int consumeMatched = 0; + + List<OperationResourceInfo> finalPathSubresources = null; + for (Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> rEntry : matchedResources.entrySet()) { + ClassResourceInfo resource = rEntry.getKey(); + MultivaluedMap<String, String> values = rEntry.getValue(); + + String path = getCurrentPath(values); + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("START_OPER_MATCH", + BUNDLE, + resource.getServiceClass().getName()).toString()); + + for (OperationResourceInfo ori : resource.getMethodDispatcher().getOperationResourceInfos()) { + boolean added = false; + + URITemplate uriTemplate = ori.getURITemplate(); + MultivaluedMap<String, String> map = new MetadataMap<>(values); + if (uriTemplate != null && uriTemplate.match(path, map)) { + String finalGroup = map.getFirst(URITemplate.FINAL_MATCH_GROUP); + boolean finalPath = StringUtils.isEmpty(finalGroup) || PATH_SEGMENT_SEP.equals(finalGroup); + + if (ori.isSubResourceLocator()) { + candidateList.put(ori, map); + if (finalPath) { + if (finalPathSubresources == null) { + finalPathSubresources = new LinkedList<>(); + } + finalPathSubresources.add(ori); + } + added = true; + } else if (finalPath) { + pathMatched++; + if (matchHttpMethod(ori.getHttpMethod(), httpMethod)) { + methodMatched++; + //CHECKSTYLE:OFF + if (getMethod || matchConsumeTypes(requestType, ori)) { + consumeMatched++; + for (MediaType acceptType : acceptContentTypes) { + if (matchProduceTypes(acceptType, ori)) { + candidateList.put(ori, map); + added = true; + break; + } + } + } + //CHECKSTYLE:ON + } + } + } + LOG.fine(matchMessageLogSupplier(ori, path, httpMethod, requestType, acceptContentTypes, added)); + } + } + if (finalPathSubresources != null && pathMatched > 0 + && !MessageUtils.getContextualBoolean(message, KEEP_SUBRESOURCE_CANDIDATES, false)) { + for (OperationResourceInfo key : finalPathSubresources) { + candidateList.remove(key); + } + } + if (!candidateList.isEmpty()) { + Map.Entry<OperationResourceInfo, MultivaluedMap<String, String>> firstEntry = + candidateList.entrySet().iterator().next(); + matchedValues.clear(); + matchedValues.putAll(firstEntry.getValue()); + OperationResourceInfo ori = firstEntry.getKey(); + if (headMethodPossible(ori.getHttpMethod(), httpMethod)) { + LOG.info(new org.apache.cxf.common.i18n.Message("GET_INSTEAD_OF_HEAD", + BUNDLE, ori.getClassResourceInfo().getServiceClass().getName(), + ori.getMethodToInvoke().getName()).toString()); + } + LOG.fine(() -> new org.apache.cxf.common.i18n.Message("OPER_SELECTED", + BUNDLE, ori.getMethodToInvoke().getName(), + ori.getClassResourceInfo().getServiceClass().getName()).toString()); + if (!ori.isSubResourceLocator()) { + MediaType responseMediaType = intersectSortMediaTypes(acceptContentTypes, + ori.getProduceTypes(), + false).get(0); + message.getExchange().put(Message.CONTENT_TYPE, mediaTypeToString(responseMediaType, + MEDIA_TYPE_Q_PARAM, + MEDIA_TYPE_QS_PARAM)); + } + if (recordMatchedUri) { + pushOntoStack(ori, matchedValues, message); + } + return ori; + } + + if (!throwException) { + return null; + } + + int status; + + // criteria matched the least number of times will determine the error code; + // priority : path, method, consumes, produces; + if (pathMatched == 0) { + status = 404; + } else if (methodMatched == 0) { + status = 405; + } else if (consumeMatched == 0) { + status = 415; + } else { + // Not a single Produces match + status = 406; + } + Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> firstCri = + matchedResources.entrySet().iterator().next(); + String name = firstCri.getKey().isRoot() ? "NO_OP_EXC" : "NO_SUBRESOURCE_METHOD_FOUND"; + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message(name, + BUNDLE, + message.get(Message.REQUEST_URI), + getCurrentPath(firstCri.getValue()), + httpMethod, + mediaTypeToString(requestType), + convertTypesToString(acceptContentTypes)); + if (!"OPTIONS".equalsIgnoreCase(httpMethod)) { + Level logLevel = getExceptionLogLevel(message, ClientErrorException.class); + LOG.log(logLevel == null ? Level.FINE : logLevel, () -> errorMsg.toString()); + } + Response response = + createResponse(getRootResources(message), message, errorMsg.toString(), status, methodMatched == 0); + throw ExceptionUtils.toHttpException(null, response); + + } + + + + public static Level getExceptionLogLevel(Message message, Class<? extends WebApplicationException> exClass) { + Level logLevel = null; + Object logLevelProp = message.get(exClass.getName() + ".log.level"); + if (logLevelProp != null) { + if (logLevelProp instanceof Level) { + logLevel = (Level)logLevelProp; + } else { + try { + logLevel = Level.parse(logLevelProp.toString()); + } catch (Exception ex) { + // ignore + } + } + } + return logLevel; + } + + private static List<MediaType> intersectSortMediaTypes(List<MediaType> acceptTypes, + List<MediaType> producesTypes, + final boolean checkDistance) { + List<MediaType> all = intersectMimeTypes(acceptTypes, producesTypes, true, checkDistance); + if (all.size() > 1) { + all.sort(new Comparator<MediaType>() { + + public int compare(MediaType mt1, MediaType mt2) { + int result = compareMediaTypes(mt1, mt2, null); + if (result == 0) { + result = compareQualityAndDistance(mt1, mt2, checkDistance); + } + return result; + } + + }); + } + return all; + } + + private static int compareQualityAndDistance(MediaType mt1, MediaType mt2, boolean checkDistance) { + int result = compareMediaTypesQualityFactors(mt1, mt2, MEDIA_TYPE_Q_PARAM); + if (result == 0) { + result = compareMediaTypesQualityFactors(mt1, mt2, MEDIA_TYPE_QS_PARAM); + } + if (result == 0 && checkDistance) { + Integer dist1 = Integer.valueOf(mt1.getParameters().get(MEDIA_TYPE_DISTANCE_PARAM)); + Integer dist2 = Integer.valueOf(mt2.getParameters().get(MEDIA_TYPE_DISTANCE_PARAM)); + result = dist1.compareTo(dist2); + } + return result; + } + + private static String getCurrentPath(MultivaluedMap<String, String> values) { + String path = values.getFirst(URITemplate.FINAL_MATCH_GROUP); + return path == null ? "/" : path; + } + + public static List<ClassResourceInfo> getRootResources(Message message) { + Service service = message.getExchange().getService(); + return ((JAXRSServiceImpl)service).getClassResourceInfos(); + } + + public static boolean noResourceMethodForOptions(Response exResponse, String httpMethod) { + return exResponse != null && exResponse.getStatus() == 405 + && "OPTIONS".equalsIgnoreCase(httpMethod); + } + + private static Supplier<String> matchMessageLogSupplier(OperationResourceInfo ori, + String path, String httpMethod, MediaType requestType, List<MediaType> acceptContentTypes, + boolean added) { + org.apache.cxf.common.i18n.Message errorMsg = added + ? new org.apache.cxf.common.i18n.Message("OPER_SELECTED_POSSIBLY", + BUNDLE, ori.getMethodToInvoke().getName()) + : new org.apache.cxf.common.i18n.Message("OPER_NO_MATCH", + BUNDLE, + ori.getMethodToInvoke().getName(), + path, + ori.getURITemplate().getValue(), + httpMethod, + ori.getHttpMethod(), + requestType.toString(), + convertTypesToString(ori.getConsumeTypes()), + convertTypesToString(acceptContentTypes), + convertTypesToString(ori.getProduceTypes())); + return () -> errorMsg.toString(); + } + + public static Response createResponse(List<ClassResourceInfo> cris, Message msg, + String responseMessage, int status, boolean addAllow) { + ResponseBuilder rb = toResponseBuilder(status); + if (addAllow) { + Set<String> allowedMethods = new HashSet<>(); + for (ClassResourceInfo cri : cris) { + allowedMethods.addAll(cri.getAllowedMethods()); + } + + for (String m : allowedMethods) { + rb.header("Allow", m); + } + // "OPTIONS" are supported all the time really + if (!allowedMethods.contains("OPTIONS")) { + rb.header("Allow", "OPTIONS"); + } + if (!allowedMethods.contains("HEAD") && allowedMethods.contains("GET")) { + rb.header("Allow", "HEAD"); + } + } + if (msg != null && MessageUtils.getContextualBoolean(msg, REPORT_FAULT_MESSAGE_PROPERTY)) { + rb.type(MediaType.TEXT_PLAIN_TYPE).entity(responseMessage); + } + return rb.build(); + } + + private static boolean matchHttpMethod(String expectedMethod, String httpMethod) { + return expectedMethod.equalsIgnoreCase(httpMethod) + || headMethodPossible(expectedMethod, httpMethod) + || expectedMethod.equals(DefaultMethod.class.getSimpleName()); + } + + public static boolean headMethodPossible(String expectedMethod, String httpMethod) { + return HttpMethod.HEAD.equalsIgnoreCase(httpMethod) && HttpMethod.GET.equals(expectedMethod); + } + + private static String convertTypesToString(List<MediaType> types) { + StringBuilder sb = new StringBuilder(); + for (MediaType type : types) { + sb.append(mediaTypeToString(type)).append(','); + } + return sb.toString(); + } + + public static List<MediaType> getConsumeTypes(Consumes cm) { + return getConsumeTypes(cm, Collections.singletonList(ALL_TYPES)); + } + + public static List<MediaType> getConsumeTypes(Consumes cm, List<MediaType> defaultTypes) { + return cm == null ? defaultTypes + : getMediaTypes(cm.value()); + } + + public static List<MediaType> getProduceTypes(Produces pm) { + return getProduceTypes(pm, Collections.singletonList(ALL_TYPES)); + } + + public static List<MediaType> getProduceTypes(Produces pm, List<MediaType> defaultTypes) { + return pm == null ? defaultTypes + : getMediaTypes(pm.value()); + } + + public static int compareSortedConsumesMediaTypes(List<MediaType> mts1, List<MediaType> mts2, MediaType ct) { + List<MediaType> actualMts1 = getCompatibleMediaTypes(mts1, ct); + List<MediaType> actualMts2 = getCompatibleMediaTypes(mts2, ct); + return compareSortedMediaTypes(actualMts1, actualMts2, null); + } + + public static int compareSortedAcceptMediaTypes(List<MediaType> mts1, List<MediaType> mts2, + List<MediaType> acceptTypes) { + List<MediaType> actualMts1 = intersectSortMediaTypes(mts1, acceptTypes, true); + List<MediaType> actualMts2 = intersectSortMediaTypes(mts2, acceptTypes, true); + int size1 = actualMts1.size(); + int size2 = actualMts2.size(); + for (int i = 0; i < size1 && i < size2; i++) { + int result = compareMediaTypes(actualMts1.get(i), actualMts2.get(i), null); + if (result == 0) { + result = compareQualityAndDistance(actualMts1.get(i), actualMts2.get(i), true); + } + if (result != 0) { + return result; + } + } + return size1 == size2 ? 0 : size1 < size2 ? -1 : 1; + } + + private static List<MediaType> getCompatibleMediaTypes(List<MediaType> mts, MediaType ct) { + List<MediaType> actualMts; + if (mts.size() == 1) { + actualMts = mts; + } else { + actualMts = new LinkedList<>(); + for (MediaType mt : mts) { + if (isMediaTypeCompatible(mt, ct)) { + actualMts.add(mt); + } + } + } + return actualMts; + } + + public static int compareSortedMediaTypes(List<MediaType> mts1, List<MediaType> mts2, String qs) { + int size1 = mts1.size(); + int size2 = mts2.size(); + for (int i = 0; i < size1 && i < size2; i++) { + int result = compareMediaTypes(mts1.get(i), mts2.get(i), qs); + if (result != 0) { + return result; + } + } + return size1 == size2 ? 0 : size1 < size2 ? -1 : 1; + } + + public static int compareMethodParameters(Class<?>[] paraList1, Class<?>[] paraList2) { + int size1 = paraList1.length; + int size2 = paraList2.length; + for (int i = 0; i < size1 && i < size2; i++) { + if (!paraList1[i].equals(paraList2[i])) { + // Handling the case when bridge / synthetic methods may be taken + // into account (f.e. when service implements generic interfaces or + // extends the generic classes). + if (paraList1[i].isAssignableFrom(paraList2[i])) { + return 1; + } else if (paraList2[i].isAssignableFrom(paraList1[i])) { + return -1; + } else { + int result = paraList1[i].getName().compareTo(paraList2[i].getName()); + if (result != 0) { + return result; + } + } + } + } + return size1 == size2 ? 0 : size1 < size2 ? -1 : 1; + } + public static int compareMediaTypes(MediaType mt1, MediaType mt2) { + return compareMediaTypes(mt1, mt2, MEDIA_TYPE_Q_PARAM); + } + public static int compareMediaTypes(MediaType mt1, MediaType mt2, String qs) { + + boolean mt1TypeWildcard = mt1.isWildcardType(); + boolean mt2TypeWildcard = mt2.isWildcardType(); + if (mt1TypeWildcard && !mt2TypeWildcard) { + return 1; + } + if (!mt1TypeWildcard && mt2TypeWildcard) { + return -1; + } + + boolean mt1SubTypeWildcard = mt1.getSubtype().contains(MediaType.MEDIA_TYPE_WILDCARD); + boolean mt2SubTypeWildcard = mt2.getSubtype().contains(MediaType.MEDIA_TYPE_WILDCARD); + if (mt1SubTypeWildcard && !mt2SubTypeWildcard) { + return 1; + } + if (!mt1SubTypeWildcard && mt2SubTypeWildcard) { + return -1; + } + + return qs != null ? compareMediaTypesQualityFactors(mt1, mt2, qs) : 0; + } + + public static int compareMediaTypesQualityFactors(MediaType mt1, MediaType mt2) { + float q1 = getMediaTypeQualityFactor(mt1.getParameters().get(MEDIA_TYPE_Q_PARAM)); + float q2 = getMediaTypeQualityFactor(mt2.getParameters().get(MEDIA_TYPE_Q_PARAM)); + return Float.compare(q1, q2) * -1; + } + + public static int compareMediaTypesQualityFactors(MediaType mt1, MediaType mt2, String qs) { + float q1 = getMediaTypeQualityFactor(mt1.getParameters().get(qs)); + float q2 = getMediaTypeQualityFactor(mt2.getParameters().get(qs)); + return Float.compare(q1, q2) * -1; + } + + public static float getMediaTypeQualityFactor(String q) { + if (q == null) { + return 1; + } + if (q.charAt(0) == '.') { + q = '0' + q; + } + try { + return Float.parseFloat(q); + } catch (NumberFormatException ex) { + // default value will do + } + return 1; + } + + //Message contains following information: PATH, HTTP_REQUEST_METHOD, CONTENT_TYPE, InputStream. + public static List<Object> processParameters(OperationResourceInfo ori, + MultivaluedMap<String, String> values, + Message message) + throws IOException, WebApplicationException { + + Class<?>[] parameterTypes = ori.getInParameterTypes(); + List<Parameter> paramsInfo = ori.getParameters(); + boolean preferModelParams = paramsInfo.size() > parameterTypes.length + && !PropertyUtils.isTrue(message.getContextualProperty("org.apache.cxf.preferMethodParameters")); + + final int parameterTypesLength = preferModelParams ? paramsInfo.size() : parameterTypes.length; + if (parameterTypesLength < 1) { + return Collections.emptyList(); + } + + Type[] genericParameterTypes = ori.getInGenericParameterTypes(); + Annotation[][] anns = ori.getInParameterAnnotations(); + Object[] params = new Object[parameterTypesLength]; + + // Ensure we process all request-body parameters first, then all @*Params, etc. + ParamTuple[] tuple = new ParamTuple[parameterTypesLength]; + for (int i = 0; i < parameterTypesLength; i++) { + tuple[i] = new ParamTuple(); + if (!preferModelParams) { + tuple[i].param = parameterTypes[i]; + tuple[i].genericParam = InjectionUtils.processGenericTypeIfNeeded( + ori.getClassResourceInfo().getServiceClass(), tuple[i].param, genericParameterTypes[i]); + tuple[i].param = InjectionUtils.updateParamClassToTypeIfNeeded(tuple[i].param, + tuple[i].genericParam); + tuple[i].paramAnns = anns == null ? EMPTY_ANNOTATIONS : anns[i]; + } else { + tuple[i].param = paramsInfo.get(i).getJavaType(); + tuple[i].genericParam = tuple[i].param; + tuple[i].paramAnns = EMPTY_ANNOTATIONS; + } + if (paramsInfo.get(i).getType() == ParameterType.REQUEST_BODY) { + params[i] = processRequestBodyParameter(tuple[i].param, + tuple[i].genericParam, + tuple[i].paramAnns, + message, + ori); + } + } + for (int i = 0; i < parameterTypesLength; i++) { + + if (paramsInfo.get(i).getType() != ParameterType.REQUEST_BODY) { + params[i] = processParameter(tuple[i].param, + tuple[i].genericParam, + tuple[i].paramAnns, + paramsInfo.get(i), + values, + message, + ori); + } + } + + return Arrays.asList(params); + } + + private static class ParamTuple { + private Class<?> param; + private Type genericParam; + private Annotation[] paramAnns; + } + + private static Object processRequestBodyParameter(Class<?> parameterClass, + Type parameterType, + Annotation[] parameterAnns, + Message message, + OperationResourceInfo ori) + throws IOException, WebApplicationException { + + if (parameterClass == AsyncResponse.class) { + return new AsyncResponseImpl(message); + } + + String contentType = (String)message.get(Message.CONTENT_TYPE); + + if (contentType == null) { + String defaultCt = (String)message.getContextualProperty(DEFAULT_CONTENT_TYPE); + contentType = defaultCt == null ? MediaType.APPLICATION_OCTET_STREAM : defaultCt; + } + + final MediaType contentTypeMt = toMediaType(contentType); + final MessageContext mc = new MessageContextImpl(message); + + MediaType mt = mc.getHttpHeaders().getMediaType(); + if (mt == null) { + mt = contentTypeMt; + } + + InputStream is; + if (mt.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) { + is = copyAndGetEntityStream(message); + } else { + is = message.getContent(InputStream.class); + } + + if (is == null) { + Reader reader = message.getContent(Reader.class); + if (reader != null) { + is = new ReaderInputStream(reader); + } + } + + return readFromMessageBody(parameterClass, + parameterType, + parameterAnns, + is, + contentTypeMt, + ori, + message); + } + + private static Object processParameter(Class<?> parameterClass, + Type parameterType, + Annotation[] parameterAnns, + Parameter parameter, + MultivaluedMap<String, String> values, + Message message, + OperationResourceInfo ori) + throws IOException, WebApplicationException { + + if (parameter.getType() == ParameterType.REQUEST_BODY) { + return processRequestBodyParameter(parameterClass, parameterType, parameterAnns, message, ori); + } else if (parameter.getType() == ParameterType.CONTEXT) { + return createContextValue(message, parameterType, parameterClass); + } else if (parameter.getType() == ParameterType.BEAN) { + return createBeanParamValue(message, parameterClass, ori); + } else { + + return createHttpParameterValue(parameter, + parameterClass, + parameterType, + parameterAnns, + message, + values, + ori); + } + } + + public static Object createHttpParameterValue(Parameter parameter, + Class<?> parameterClass, + Type genericParam, + Annotation[] paramAnns, + Message message, + MultivaluedMap<String, String> values, + OperationResourceInfo ori) { + + boolean isEncoded = parameter.isEncoded() || ori != null && ori.isEncodedEnabled(); + String defaultValue = parameter.getDefaultValue(); + if (defaultValue == null && ori != null) { + defaultValue = ori.getDefaultParameterValue(); + } + + if (parameter.getType() == ParameterType.PATH) { + return readFromUriParam(message, parameter.getName(), parameterClass, genericParam, + paramAnns, values, defaultValue, !isEncoded); + } + + if (parameter.getType() == ParameterType.QUERY) { + return readQueryString(parameter.getName(), parameterClass, genericParam, + paramAnns, message, defaultValue, !isEncoded); + } + + if (parameter.getType() == ParameterType.MATRIX) { + return processMatrixParam(message, parameter.getName(), parameterClass, genericParam, + paramAnns, defaultValue, !isEncoded); + } + + if (parameter.getType() == ParameterType.FORM) { + return processFormParam(message, parameter.getName(), parameterClass, genericParam, + paramAnns, defaultValue, !isEncoded); + } + + if (parameter.getType() == ParameterType.COOKIE) { + return processCookieParam(message, parameter.getName(), parameterClass, genericParam, + paramAnns, defaultValue); + } + + Object result = null; + if (parameter.getType() == ParameterType.HEADER) { + result = processHeaderParam(message, parameter.getName(), parameterClass, genericParam, + paramAnns, defaultValue); + } + return result; + } + + private static Object processMatrixParam(Message m, String key, + Class<?> pClass, Type genericType, + Annotation[] paramAnns, + String defaultValue, + boolean decode) { + List<PathSegment> segments = JAXRSUtils.getPathSegments( + (String)m.get(Message.REQUEST_URI), decode); + if (!segments.isEmpty()) { + MultivaluedMap<String, String> params = new MetadataMap<>(); + for (PathSegment ps : segments) { + MultivaluedMap<String, String> matrix = ps.getMatrixParameters(); + for (Map.Entry<String, List<String>> entry : matrix.entrySet()) { + for (String value : entry.getValue()) { + params.add(entry.getKey(), value); + } + } + } + + if ("".equals(key)) { + return InjectionUtils.handleBean(pClass, paramAnns, params, ParameterType.MATRIX, m, false); + } + List<String> values = params.get(key); + return InjectionUtils.createParameterObject(values, + pClass, + genericType, + paramAnns, + defaultValue, + false, + ParameterType.MATRIX, + m); + } + + return null; + } + + private static Object processFormParam(Message m, String key, + Class<?> pClass, Type genericType, + Annotation[] paramAnns, + String defaultValue, + boolean decode) { + + MessageContext mc = new MessageContextImpl(m); + MediaType mt = mc.getHttpHeaders().getMediaType(); + + @SuppressWarnings("unchecked") + MultivaluedMap<String, String> params = + (MultivaluedMap<String, String>)m.get(FormUtils.FORM_PARAM_MAP); + + if (params == null) { + params = new MetadataMap<>(); + m.put(FormUtils.FORM_PARAM_MAP, params); + + if (mt == null || mt.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) { + InputStream entityStream = copyAndGetEntityStream(m); + String enc = HttpUtils.getEncoding(mt, StandardCharsets.UTF_8.name()); + String body = FormUtils.readBody(entityStream, enc); + FormUtils.populateMapFromStringOrHttpRequest(params, m, body, enc, decode); + } else { + if ("multipart".equalsIgnoreCase(mt.getType()) + && MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mt)) { + MultipartBody body = AttachmentUtils.getMultipartBody(mc); + FormUtils.populateMapFromMultipart(params, body, m, decode); + } else { + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message("WRONG_FORM_MEDIA_TYPE", + BUNDLE, + mt.toString()); + LOG.warning(errorMsg.toString()); + throw ExceptionUtils.toNotSupportedException(null, null); + } + } + } + + if ("".equals(key)) { + return InjectionUtils.handleBean(pClass, paramAnns, params, ParameterType.FORM, m, false); + } + List<String> results = params.get(key); + + return InjectionUtils.createParameterObject(results, + pClass, + genericType, + paramAnns, + defaultValue, + false, + ParameterType.FORM, + m); + } + + + public static MultivaluedMap<String, String> getMatrixParams(String path, boolean decode) { + int index = path.indexOf(';'); + return index == -1 ? new MetadataMap<String, String>() + : JAXRSUtils.getStructuredParams(path.substring(index + 1), ";", decode, false); + } + + private static Object processHeaderParam(Message m, + String header, + Class<?> pClass, + Type genericType, + Annotation[] paramAnns, + String defaultValue) { + + List<String> values = new HttpHeadersImpl(m).getRequestHeader(header); + if (values != null && values.isEmpty()) { + values = null; + } + return InjectionUtils.createParameterObject(values, + pClass, + genericType, + paramAnns, + defaultValue, + false, + ParameterType.HEADER, + m); + + + } + + private static Object processCookieParam(Message m, String cookieName, + Class<?> pClass, Type genericType, + Annotation[] paramAnns, String defaultValue) { + Cookie c = new HttpHeadersImpl(m).getCookies().get(cookieName); + + if (c == null && defaultValue != null) { + c = Cookie.valueOf(cookieName + '=' + defaultValue); + } + if (c == null) { + return null; + } + + if (pClass.isAssignableFrom(Cookie.class)) { + return c; + } + String value = InjectionUtils.isSupportedCollectionOrArray(pClass) + && InjectionUtils.getActualType(genericType) == Cookie.class + ? c.toString() : c.getValue(); + return InjectionUtils.createParameterObject(Collections.singletonList(value), + pClass, + genericType, + paramAnns, + null, + false, + ParameterType.COOKIE, + m); + } + + public static Object createBeanParamValue(Message m, Class<?> clazz, OperationResourceInfo ori) { + BeanParamInfo bmi = ServerProviderFactory.getInstance(m).getBeanParamInfo(clazz); + if (bmi == null) { + // we could've started introspecting now but the fact no bean info + // is available indicates that the one created at start up has been + // lost and hence it is 500 + LOG.warning("Bean parameter info is not available"); + throw ExceptionUtils.toInternalServerErrorException(null, null); + } + Object instance; + try { + instance = clazz.newInstance(); + } catch (Throwable t) { + throw ExceptionUtils.toInternalServerErrorException(t, null); + } + JAXRSUtils.injectParameters(ori, bmi, instance, m); + + InjectionUtils.injectContexts(instance, bmi, m); + + return instance; + } + + public static Message getContextMessage(Message m) { + + Message contextMessage = m.getExchange() != null ? m.getExchange().getInMessage() : m; + if (contextMessage == null && !PropertyUtils.isTrue(m.get(Message.INBOUND_MESSAGE))) { + contextMessage = m; + } + return contextMessage; + } + + public static <T> T createContextValue(Message m, Type genericType, Class<T> clazz) { + + Message contextMessage = getContextMessage(m); + Object o = null; + if (UriInfo.class.isAssignableFrom(clazz)) { + o = createUriInfo(contextMessage); + } else if (HttpHeaders.class.isAssignableFrom(clazz) + || ProtocolHeaders.class.isAssignableFrom(clazz)) { + o = createHttpHeaders(contextMessage, clazz); + } else if (SecurityContext.class.isAssignableFrom(clazz)) { + SecurityContext customContext = contextMessage.get(SecurityContext.class); + o = customContext == null ? new SecurityContextImpl(contextMessage) : customContext; + } else if (MessageContext.class.isAssignableFrom(clazz)) { + o = new MessageContextImpl(m); + } else if (ResourceInfo.class.isAssignableFrom(clazz)) { + o = new ResourceInfoImpl(contextMessage); + } else if (ResourceContext.class.isAssignableFrom(clazz)) { + o = new ResourceContextImpl(contextMessage, contextMessage.getExchange().get(OperationResourceInfo.class)); + } else if (Request.class.isAssignableFrom(clazz)) { + o = new RequestImpl(contextMessage); + } else if (Providers.class.isAssignableFrom(clazz)) { + o = new ProvidersImpl(contextMessage); + } else if (ContextResolver.class.isAssignableFrom(clazz)) { + o = createContextResolver(genericType, contextMessage); + } else if (Configuration.class.isAssignableFrom(clazz)) { + o = ProviderFactory.getInstance(contextMessage).getConfiguration(contextMessage); + } else if (Application.class.isAssignableFrom(clazz)) { + ProviderInfo<?> providerInfo = + (ProviderInfo<?>)contextMessage.getExchange().getEndpoint().get(Application.class.getName()); + o = providerInfo == null ? null : providerInfo.getProvider(); + } else if (contextMessage != null) { + ContextProvider<?> provider = + ProviderFactory.getInstance(contextMessage).createContextProvider(clazz, contextMessage); + if (provider != null) { + o = provider.createContext(contextMessage); + } + } + if (o == null && contextMessage != null && !MessageUtils.isRequestor(contextMessage)) { + o = HttpUtils.createServletResourceValue(contextMessage, clazz); + } + return clazz.cast(o); + } + + @SuppressWarnings("unchecked") + private static UriInfo createUriInfo(Message m) { + if (MessageUtils.isRequestor(m)) { + m = m.getExchange() != null ? m.getExchange().getOutMessage() : m; + } + MultivaluedMap<String, String> templateParams = + (MultivaluedMap<String, String>)m.get(URITemplate.TEMPLATE_PARAMETERS); + return new UriInfoImpl(m, templateParams); + } + + private static Object createHttpHeaders(Message m, Class<?> ctxClass) { + if (MessageUtils.isRequestor(m)) { + m = m.getExchange() != null ? m.getExchange().getOutMessage() : m; + } + return HttpHeaders.class.isAssignableFrom(ctxClass) ? new HttpHeadersImpl(m) + : new ProtocolHeadersImpl(m); + } + + public static ContextResolver<?> createContextResolver(Type genericType, Message m) { + if (genericType instanceof ParameterizedType) { + return ProviderFactory.getInstance(m).createContextResolver( + ((ParameterizedType)genericType).getActualTypeArguments()[0], m); + } else if (m != null) { + return ProviderFactory.getInstance(m).createContextResolver(genericType, m); + } else { + return null; + } + } + + public static Object createResourceValue(Message m, Type genericType, Class<?> clazz) { + + // lets assume we're aware of servlet types only that can be @Resource-annotated + return createContextValue(m, genericType, clazz); + } + + + //CHECKSTYLE:OFF + private static Object readFromUriParam(Message m, + String parameterName, + Class<?> paramType, + Type genericType, + Annotation[] paramAnns, + MultivaluedMap<String, String> values, + String defaultValue, + boolean decoded) { + //CHECKSTYLE:ON + if ("".equals(parameterName)) { + return InjectionUtils.handleBean(paramType, paramAnns, values, ParameterType.PATH, m, decoded); + } + List<String> results = values.get(parameterName); + return InjectionUtils.createParameterObject(results, + paramType, + genericType, + paramAnns, + defaultValue, + decoded, + ParameterType.PATH, + m); + } + + + + //TODO : multiple query string parsing, do it once + private static Object readQueryString(String queryName, + Class<?> paramType, + Type genericType, + Annotation[] paramAnns, + Message m, + String defaultValue, + boolean decode) { + + MultivaluedMap<String, String> queryMap = new UriInfoImpl(m, null).getQueryParameters(decode); + + if ("".equals(queryName)) { + return InjectionUtils.handleBean(paramType, paramAnns, queryMap, ParameterType.QUERY, m, false); + } + return InjectionUtils.createParameterObject(queryMap.get(queryName), + paramType, + genericType, + paramAnns, + defaultValue, + false, + ParameterType.QUERY, m); + } + + + + /** + * Retrieve map of query parameters from the passed in message + * @return a Map of query parameters. + */ + public static MultivaluedMap<String, String> getStructuredParams(String query, + String sep, + boolean decode, + boolean decodePlus) { + MultivaluedMap<String, String> map = + new MetadataMap<>(new LinkedHashMap<String, List<String>>()); + + getStructuredParams(map, query, sep, decode, decodePlus); + + return map; + } + + public static void getStructuredParams(MultivaluedMap<String, String> queries, + String query, + String sep, + boolean decode, + boolean decodePlus) { + getStructuredParams(queries, query, sep, decode, decodePlus, false); + } + + public static void getStructuredParams(MultivaluedMap<String, String> queries, + String query, + String sep, + boolean decode, + boolean decodePlus, + boolean valueIsCollection) { + if (!StringUtils.isEmpty(query)) { + for (String part : query.split(sep)) { // fastpath expected + int index = part.indexOf('='); + final String name; + String value = null; + if (index == -1) { + name = part; + } else { + name = part.substring(0, index); + value = index < part.length() ? part.substring(index + 1) : ""; + } + if (valueIsCollection) { + if (value != null) { + for (String s : value.split(",")) { + addStructuredPartToMap(queries, sep, name, s, decode, decodePlus); + } + } + } else { + addStructuredPartToMap(queries, sep, name, value, decode, decodePlus); + } + } + } + } + + private static void addStructuredPartToMap(MultivaluedMap<String, String> queries, + String sep, + String name, + String value, + boolean decode, + boolean decodePlus) { + + if (value != null) { + if (decodePlus && value.contains("+")) { + value = value.replace('+', ' '); + } + if (decode) { + value = (";".equals(sep)) + ? HttpUtils.pathDecode(value) : HttpUtils.urlDecode(value); + } + } + + queries.add(HttpUtils.urlDecode(name), value); + } + + private static Object readFromMessageBody(Class<?> targetTypeClass, + Type parameterType, + Annotation[] parameterAnnotations, + InputStream is, + MediaType contentType, + OperationResourceInfo ori, + Message m) throws IOException, WebApplicationException { + + List<MediaType> types = JAXRSUtils.intersectMimeTypes(ori.getConsumeTypes(), contentType); + + final ProviderFactory pf = ServerProviderFactory.getInstance(m); + for (MediaType type : types) { + List<ReaderInterceptor> readers = pf.createMessageBodyReaderInterceptor( + targetTypeClass, + parameterType, + parameterAnnotations, + type, + m, + true, + ori.getNameBindings()); + if (readers != null) { + try { + return readFromMessageBodyReader(readers, + targetTypeClass, + parameterType, + parameterAnnotations, + is, + type, + m); + } catch (IOException e) { + if (e.getClass().getName().equals(NO_CONTENT_EXCEPTION)) { + throw ExceptionUtils.toBadRequestException(e, null); + } + throw e; + } catch (WebApplicationException ex) { + throw ex; + } catch (Exception ex) { + throw new Fault(ex); + } + } + } + + logMessageHandlerProblem("NO_MSG_READER", targetTypeClass, contentType); + throw new WebApplicationException(Response.Status.UNSUPPORTED_MEDIA_TYPE); + } + + @SuppressWarnings("unchecked") + public static Object readFromMessageBodyReader(List<ReaderInterceptor> readers, + Class<?> targetTypeClass, + Type parameterType, + Annotation[] parameterAnnotations, + InputStream is, + MediaType mediaType, + Message m) throws IOException, WebApplicationException { + + // Verbose but avoids an extra context instantiation for the typical path + if (readers.size() > 1) { + ReaderInterceptor first = readers.remove(0); + ReaderInterceptorContext context = new ReaderInterceptorContextImpl(targetTypeClass, + parameterType, + parameterAnnotations, + is, + m, + readers); + + return first.aroundReadFrom(context); + } + MessageBodyReader<?> provider = ((ReaderInterceptorMBR)readers.get(0)).getMBR(); + @SuppressWarnings("rawtypes") + Class cls = targetTypeClass; + return provider.readFrom( + cls, parameterType, parameterAnnotations, mediaType, + new HttpHeadersImpl(m).getRequestHeaders(), is); + } + + + //CHECKSTYLE:OFF + public static void writeMessageBody(List<WriterInterceptor> writers, + Object entity, + Class<?> type, Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap<String, Object> httpHeaders, + Message message) + throws WebApplicationException, IOException { + + OutputStream entityStream = message.getContent(OutputStream.class); + if (entity.getClass().getName().equals( + "org.apache.cxf.jaxrs.reactivestreams.server.StreamingAsyncSubscriber$StreamingResponseImpl")) { + //cache the OutputStream when it's reactive response + entityStream = new CacheAndWriteOutputStream(entityStream); + } + + if (writers.size() > 1) { + WriterInterceptor first = writers.remove(0); + WriterInterceptorContext context = new WriterInterceptorContextImpl(entity, + type, + genericType, + annotations, + entityStream, + message, + writers); + + first.aroundWriteTo(context); + } else { + MessageBodyWriter<Object> writer = ((WriterInterceptorMBW)writers.get(0)).getMBW(); + if (type == byte[].class) { + long size = writer.getSize(entity, type, genericType, annotations, mediaType); + if (size != -1) { + httpHeaders.putSingle(HttpHeaders.CONTENT_LENGTH, Long.toString(size)); + } + } + HttpUtils.convertHeaderValuesToString(httpHeaders, true); + writer.writeTo(entity, type, genericType, annotations, mediaType, + httpHeaders, + entityStream); + } + } + //CHECKSTYLE:ON + + + public static boolean matchConsumeTypes(MediaType requestContentType, + OperationResourceInfo ori) { + + return doMimeTypesIntersect(ori.getConsumeTypes(), requestContentType); + } + + public static boolean matchProduceTypes(MediaType acceptContentType, + OperationResourceInfo ori) { + + return doMimeTypesIntersect(ori.getProduceTypes(), acceptContentType); + } + + public static boolean matchMimeTypes(MediaType requestContentType, + MediaType acceptContentType, + OperationResourceInfo ori) { + + return doMimeTypesIntersect(ori.getConsumeTypes(), requestContentType) + && doMimeTypesIntersect(ori.getProduceTypes(), acceptContentType); + } + + public static List<MediaType> parseMediaTypes(String types) { + List<MediaType> acceptValues = new ArrayList<>(); + + if (types != null) { + int x = 0; + int y = types.indexOf(','); + while (y > 0) { + acceptValues.add(toMediaType(types.substring(x, y).trim())); + x = y + 1; + y = types.indexOf(',', x); + } + String lastMediaType = types.substring(x).trim(); + if (!lastMediaType.isEmpty()) { + acceptValues.add(toMediaType(lastMediaType)); + } + } else { + acceptValues.add(ALL_TYPES); + } + + return acceptValues; + } + + public static boolean doMimeTypesIntersect(List<MediaType> mimeTypesA, MediaType mimeTypeB) { + return doMimeTypesIntersect(mimeTypesA, Collections.singletonList(mimeTypeB)); + } + + public static boolean doMimeTypesIntersect(List<MediaType> requiredMediaTypes, List<MediaType> userMediaTypes) { + final NonAccumulatingIntersector intersector = new NonAccumulatingIntersector(); + intersectMimeTypes(requiredMediaTypes, userMediaTypes, intersector); + return intersector.doIntersect(); + } + + /** + * intersect two mime types + * + * @param requiredMediaTypes + * @param userMediaTypes + * @param addRequiredParamsIfPossible + * @return return a list of intersected mime types + */ + public static List<MediaType> intersectMimeTypes(List<MediaType> requiredMediaTypes, + List<MediaType> userMediaTypes, + boolean addRequiredParamsIfPossible) { + return intersectMimeTypes(requiredMediaTypes, userMediaTypes, addRequiredParamsIfPossible, false); + } + + public static List<MediaType> intersectMimeTypes(List<MediaType> requiredMediaTypes, + List<MediaType> userMediaTypes, + boolean addRequiredParamsIfPossible, + boolean addDistanceParameter) { + final AccumulatingIntersector intersector = new AccumulatingIntersector(addRequiredParamsIfPossible, + addDistanceParameter); + intersectMimeTypes(requiredMediaTypes, userMediaTypes, intersector); + return new ArrayList<>(intersector.getSupportedMimeTypeList()); + } + + private static void intersectMimeTypes(List<MediaType> requiredMediaTypes, List<MediaType> userMediaTypes, + MimeTypesIntersector intersector) { + + for (MediaType requiredType : requiredMediaTypes) { + for (MediaType userType : userMediaTypes) { + boolean isCompatible = isMediaTypeCompatible(requiredType, userType); + if (isCompatible) { + boolean parametersMatched = true; + for (Map.Entry<String, String> entry : userType.getParameters().entrySet()) { + String value = requiredType.getParameters().get(entry.getKey()); + if (value != null && entry.getValue() != null && !(stripDoubleQuotesIfNeeded(value) + .equals(stripDoubleQuotesIfNeeded(entry.getValue())))) { + + if (HTTP_CHARSET_PARAM.equals(entry.getKey()) && value.equalsIgnoreCase(entry.getValue())) { + continue; + } + parametersMatched = false; + break; + } + } + if (!parametersMatched) { + continue; + } + + if (!intersector.intersect(requiredType, userType)) { + return; + } + } + } + } + } + + private static String stripDoubleQuotesIfNeeded(String value) { + if (value != null && value.startsWith("\"") + && value.endsWith("\"") && value.length() > 1) { + value = value.substring(1, value.length() - 1); + } + return value; + } + + private static boolean isMediaTypeCompatible(MediaType requiredType, MediaType userType) { + boolean isCompatible = requiredType.isCompatible(userType); + if (!isCompatible && requiredType.getType().equalsIgnoreCase(userType.getType())) { + isCompatible = compareCompositeSubtypes(requiredType, userType, + PhaseInterceptorChain.getCurrentMessage()); + } + return isCompatible; + } + + static boolean compareCompositeSubtypes(String requiredType, String userType, + Message message) { + return compareCompositeSubtypes(toMediaType(requiredType), toMediaType(userType), message); + } + + private static boolean compareCompositeSubtypes(MediaType requiredType, MediaType userType, + Message message) { + boolean isCompatible = false; + // check if we have composite subtypes + String subType1 = requiredType.getSubtype(); + String subType2 = userType.getSubtype(); + + String subTypeAfterPlus1 = splitMediaSubType(subType1, true); + String subTypeAfterPlus2 = splitMediaSubType(subType2, true); + if (message != null && MessageUtils.getContextualBoolean(message, PARTIAL_HIERARCHICAL_MEDIA_SUBTYPE_CHECK)) { + if (subTypeAfterPlus1 != null || subTypeAfterPlus2 != null) { + boolean nullPossible = subTypeAfterPlus1 == null || subTypeAfterPlus2 == null; + isCompatible = subTypeAfterPlus1 == null && subTypeAfterPlus2.equals(subType1) + || subTypeAfterPlus2 == null && subTypeAfterPlus1.equals(subType2); + if (!isCompatible && !nullPossible) { + isCompatible = subTypeAfterPlus1.equalsIgnoreCase(subTypeAfterPlus2) + && (subType1.charAt(0) == '*' || subType2.charAt(0) == '*'); + } + + if (!isCompatible) { + String subTypeBeforePlus1 = splitMediaSubType(subType1, false); + String subTypeBeforePlus2 = splitMediaSubType(subType2, false); + nullPossible = subTypeBeforePlus1 == null || subTypeBeforePlus2 == null; + isCompatible = subTypeBeforePlus1 == null && subTypeBeforePlus2 != null + && subTypeBeforePlus2.equals(subType1) + || subTypeBeforePlus2 == null && subTypeBeforePlus1 != null + && subTypeBeforePlus1.equals(subType2); + if (!isCompatible && !nullPossible) { + isCompatible = subTypeBeforePlus1.equalsIgnoreCase(subTypeBeforePlus2) + && (subType1.charAt(subType1.length() - 1) == '*' + || subType2.charAt(subType2.length() - 1) == '*'); + } + } + } + } else { + if (subTypeAfterPlus1 != null && subTypeAfterPlus2 != null) { + + isCompatible = subTypeAfterPlus1.equalsIgnoreCase(subTypeAfterPlus2) + && (subType1.charAt(0) == '*' || subType2.charAt(0) == '*'); + + if (!isCompatible) { + String subTypeBeforePlus1 = splitMediaSubType(subType1, false); + String subTypeBeforePlus2 = splitMediaSubType(subType2, false); + + isCompatible = subTypeBeforePlus1 != null && subTypeBeforePlus1.equalsIgnoreCase(subTypeBeforePlus2) + && (subType1.charAt(subType1.length() - 1) == '*' + || subType2.charAt(subType2.length() - 1) == '*'); + } + } + } + return isCompatible; + } + + private static String splitMediaSubType(String type, boolean after) { + int index = type.indexOf('+'); + return index == -1 ? null : after ? type.substring(index + 1) : type.substring(0, index); + } + + public static List<MediaType> intersectMimeTypes(List<MediaType> mimeTypesA, + MediaType mimeTypeB) { + return intersectMimeTypes(mimeTypesA, + Collections.singletonList(mimeTypeB), false); + } + + public static List<MediaType> intersectMimeTypes(String mimeTypesA, + String mimeTypesB) { + return intersectMimeTypes(parseMediaTypes(mimeTypesA), + parseMediaTypes(mimeTypesB), + false); + } + + public static List<MediaType> sortMediaTypes(String mediaTypes, String qs) { + return sortMediaTypes(JAXRSUtils.parseMediaTypes(mediaTypes), qs); + } + public static List<MediaType> sortMediaTypes(List<MediaType> types, final String qs) { + if (types.size() > 1) { + Collections.sort(types, new Comparator<MediaType>() { + + public int compare(MediaType mt1, MediaType mt2) { + return JAXRSUtils.compareMediaTypes(mt1, mt2, qs); + } + + }); + } + return types; + } + + + public static <T extends Throwable> Response convertFaultToResponse(T ex, Message currentMessage) { + return ExceptionUtils.convertFaultToResponse(ex, currentMessage); + } + + public static void setMessageContentType(Message message, Response response) { + if (response != null) { + Object ct = response.getMetadata().getFirst(HttpHeaders.CONTENT_TYPE); + if (ct != null) { + Exchange ex = message.getExchange(); + if (ex.getInMessage() == message) { + ex.put(Message.CONTENT_TYPE, ct.toString()); + } else { + message.put(Message.CONTENT_TYPE, ct.toString()); + } + } + } + + } + + public static QName getClassQName(Class<?> type) { + String nsURI = PackageUtils.getNamespace(PackageUtils.getPackageName(type)); + if (nsURI.endsWith("/")) { + nsURI = nsURI.substring(0, nsURI.length() - 1); + } + return new QName(nsURI, type.getSimpleName(), "ns1"); + } + + public static QName convertStringToQName(String name) { + return DOMUtils.convertStringToQName(name, ""); + } + + public static boolean runContainerRequestFilters(ServerProviderFactory pf, + Message m, + boolean preMatch, + Set<String> names) throws IOException { + List<ProviderInfo<ContainerRequestFilter>> containerFilters = preMatch + ? pf.getPreMatchContainerRequestFilters() : pf.getPostMatchContainerRequestFilters(names); + if (!containerFilters.isEmpty()) { + ContainerRequestContext context = new ContainerRequestContextImpl(m, preMatch, false); + for (ProviderInfo<ContainerRequestFilter> filter : containerFilters) { + InjectionUtils.injectContexts(filter.getProvider(), filter, m); + filter.getProvider().filter(context); + Response response = m.getExchange().get(Response.class); + if (response != null) { + setMessageContentType(m, response); + return true; + } + } + } + return false; + } + + public static void runContainerResponseFilters(ServerProviderFactory pf, + ResponseImpl r, + Message m, + OperationResourceInfo ori, + Method invoked) throws IOException, Throwable { + List<ProviderInfo<ContainerResponseFilter>> containerFilters = + pf.getContainerResponseFilters(ori == null ? null : ori.getNameBindings()); + if (!containerFilters.isEmpty()) { + ContainerRequestContext requestContext = + new ContainerRequestContextImpl(m.getExchange().getInMessage(), + false, + true); + ContainerResponseContext responseContext = + new ContainerResponseContextImpl(r, m, + ori == null ? null : ori.getClassResourceInfo().getServiceClass(), invoked); + for (ProviderInfo<ContainerResponseFilter> filter : containerFilters) { + InjectionUtils.injectContexts(filter.getProvider(), filter, m); + filter.getProvider().filter(requestContext, responseContext); + } + } + } + + public static String mediaTypeToString(MediaType mt, String... ignoreParams) { + List<String> list = ignoreParams == null || ignoreParams.length == 0 ? null + : Arrays.asList(ignoreParams); + + return MediaTypeHeaderProvider.typeToString(mt, list); + } + + public static MediaType toMediaType(String value) { + if (value == null) { + return ALL_TYPES; + } + return MediaTypeHeaderProvider.valueOf(value); + } + + public static Response toResponse(int status) { + return toResponseBuilder(status).build(); + } + + public static Response toResponse(Response.Status status) { + return toResponse(status.getStatusCode()); + } + + public static ResponseBuilder toResponseBuilder(int status) { + return new ResponseBuilderImpl().status(status); + } + + public static ResponseBuilder toResponseBuilder(Response.Status status) { + return toResponseBuilder(status.getStatusCode()); + } + + public static ResponseBuilder fromResponse(Response response) { + return fromResponse(response, true); + } + + public static ResponseBuilder fromResponse(Response response, boolean copyEntity) { + ResponseBuilder rb = toResponseBuilder(response.getStatus()); + if (copyEntity) { + rb.entity(response.getEntity()); + } + for (Map.Entry<String, List<Object>> entry : response.getMetadata().entrySet()) { + List<Object> values = entry.getValue(); + for (Object value : values) { + rb.header(entry.getKey(), value); + } + } + return rb; + } + + public static Response copyResponseIfNeeded(Response response) { + if (!(response instanceof ResponseImpl)) { + Response r = fromResponse(response).build(); + Field[] declaredFields = ReflectionUtil.getDeclaredFields(response.getClass()); + for (Field f : declaredFields) { + Class<?> declClass = f.getType(); + if (declClass == Annotation[].class) { + try { + Annotation[] fieldAnnotations = + ReflectionUtil.accessDeclaredField(f, response, Annotation[].class); + ((ResponseImpl)r).setEntityAnnotations(fieldAnnotations); + } catch (Throwable ex) { + LOG.warning("Custom annotations if any can not be copied"); + } + break; + } + } + return r; + } + return response; + } + + public static Message getCurrentMessage() { + return PhaseInterceptorChain.getCurrentMessage(); + } + + public static ClassResourceInfo getRootResource(Message m) { + return (ClassResourceInfo)m.getExchange().get(JAXRSUtils.ROOT_RESOURCE_CLASS); + } + + public static void pushOntoStack(OperationResourceInfo ori, + MultivaluedMap<String, String> params, + Message msg) { + OperationResourceInfoStack stack = msg.get(OperationResourceInfoStack.class); + if (stack == null) { + stack = new OperationResourceInfoStack(); + msg.put(OperationResourceInfoStack.class, stack); + } + + + List<String> values = null; + if (params.size() <= 1) { + values = Collections.emptyList(); + } else { + values = new ArrayList<>(params.size() - 1); + addTemplateVarValues(values, params, ori.getClassResourceInfo().getURITemplate()); + addTemplateVarValues(values, params, ori.getURITemplate()); + } + Class<?> realClass = ori.getClassResourceInfo().getServiceClass(); + stack.push(new MethodInvocationInfo(ori, realClass, values)); + } + + private static void addTemplateVarValues(List<String> values, + MultivaluedMap<String, String> params, + URITemplate template) { + if (template != null) { + for (String var : template.getVariables()) { + List<String> paramValues = params.get(var); + if (paramValues != null) { + values.addAll(paramValues); + } + } + } + } + + public static String logMessageHandlerProblem(String name, Class<?> cls, MediaType ct) { + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message(name, BUNDLE, cls.getName(), mediaTypeToString(ct)); + String errorMessage = errorMsg.toString(); + LOG.severe(errorMessage); + return errorMessage; + } + + /** + * Get path URI template, combining base path, class & method & subresource templates + * @param message message instance + * @param cri class resource info + * @param ori operation resource info + * @param subOri operation subresource info + * @return the URI template for the method in question + */ + public static String getUriTemplate(Message message, ClassResourceInfo cri, OperationResourceInfo ori, + OperationResourceInfo subOri) { + final String template = getUriTemplate(message, cri, ori); + final String methodPathTemplate = getUriTemplate(subOri); + return combineUriTemplates(template, methodPathTemplate); + } + + /** + * Get path URI template, combining base path, class & method templates + * @param message message instance + * @param cri class resource info + * @param ori operation resource info + * @return the URI template for the method in question + */ + public static String getUriTemplate(Message message, ClassResourceInfo cri, OperationResourceInfo ori) { + final String basePath = (String)message.get(Message.BASE_PATH); + final String classPathTemplate = getUriTemplate(cri); + final String methodPathTemplate = getUriTemplate(ori); + + // The application path (@ApplicationPath) is incorporated into Message.BASE_PATH, + // since it is part of the address. + String template = basePath; + if (StringUtils.isEmpty(template)) { + template = "/"; + } else if (!template.startsWith("/")) { + template = "/" + template; + } + + template = combineUriTemplates(template, classPathTemplate); + return combineUriTemplates(template, methodPathTemplate); + } + + /** + * Gets the URI template of the operation from its resource info + * to assemble final URI template + * @param ori operation resource info + * @return URI template + */ + private static String getUriTemplate(OperationResourceInfo ori) { + final URITemplate template = ori.getURITemplate(); + if (template != null) { + return template.getValue(); + } else { + return null; + } + } + + /** + * Goes over sub-resource class resource templates (through parent chain) if necessary + * to assemble final URI template + * @param cri root or subresource class resource info + * @return URI template chain + */ + private static String getUriTemplate(ClassResourceInfo cri) { + final URITemplate template = cri.getURITemplate(); + if (template != null) { + return template.getValue(); + } else if (cri.getParent() != null) { /* probably subresource */ + return getUriTemplate(cri.getParent()); + } else { + return null; /* should not happen */ + } + } + + /** + * Combines two URI templates together + * @param parent parent URI template + * @param child child URI template + * @return the URI template combined from the parent and child + */ + private static String combineUriTemplates(final String parent, final String child) { + if (StringUtils.isEmpty(child)) { + return parent; + } + + // The way URI templates are normalized in org.apache.cxf.jaxrs.model.URITemplate: + // - empty or null become "/" + // - "/" is added at the start if not present + if ("/".equals(parent)) { + return child; + } else if ("/".equals(child)) { + return parent; + } else if (parent.endsWith("/")) { + // Remove only last slash + return parent.replaceAll("/$", "") + child; + } else { + return parent + child; + } + } + + // copy the input stream so that it is not inadvertently closed + private static InputStream copyAndGetEntityStream(Message m) { + LoadingByteArrayOutputStream baos = new LoadingByteArrayOutputStream(); + try (InputStream in = m.getContent(InputStream.class)) { + IOUtils.copy(in, baos); + } catch (IOException e) { + throw ExceptionUtils.toInternalServerErrorException(e, null); + } + m.setContent(InputStream.class, baos.createInputStream()); + return baos.createInputStream(); + } +}
