diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/ResponseImpl.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/ResponseImpl.java index b7ba0818608..e240f50e296 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/ResponseImpl.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/ResponseImpl.java @@ -405,7 +405,7 @@ public T readEntity(GenericType genType) throws ProcessingException, Ille @Override public T readEntity(Class cls, Annotation[] anns) throws ProcessingException, IllegalStateException { - return doReadEntity(cls, cls, anns, true); + return doReadEntity(cls, cls, anns); } @Override @@ -413,20 +413,15 @@ public T readEntity(Class cls, Annotation[] anns) throws ProcessingExcept public T readEntity(GenericType genType, Annotation[] anns) throws ProcessingException, IllegalStateException { return doReadEntity((Class) genType.getRawType(), - genType.getType(), anns, true); + genType.getType(), anns); } public T doReadEntity(Class cls, Type t, Annotation[] anns) throws ProcessingException, IllegalStateException { - return doReadEntity(cls, t, anns, false); - } - - public T doReadEntity(Class cls, Type t, Annotation[] anns, boolean closeAfterRead) - throws ProcessingException, IllegalStateException { checkEntityIsClosed(); //according to javadoc, should close when is not buffered. - boolean shouldClose = !this.entityBufferred; + boolean shouldClose = !entityBufferred && !JAXRSUtils.isStreamingOutType(cls); if (lastEntity != null && cls.isAssignableFrom(lastEntity.getClass()) && !(lastEntity instanceof InputStream)) { @@ -478,7 +473,7 @@ public T doReadEntity(Class cls, Type t, Annotation[] anns, boolean close T tCastLastEntity = castLastEntity(); shouldClose = shouldClose && !(tCastLastEntity instanceof AutoCloseable) && !(tCastLastEntity instanceof Source); - if (closeAfterRead && shouldClose) { + if (shouldClose) { close(); } return tCastLastEntity; @@ -489,7 +484,7 @@ public T doReadEntity(Class cls, Type t, Annotation[] anns, boolean close autoClose(cls, true); reportMessageHandlerProblem("MSG_READER_PROBLEM", cls, mediaType, ex); } else { - if (closeAfterRead && shouldClose) { + if (shouldClose) { close(); } return null; diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/AbstractJAXBProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/AbstractJAXBProvider.java index f2bee17b4d7..b94760ab33f 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/AbstractJAXBProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/AbstractJAXBProvider.java @@ -42,6 +42,7 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.ext.ContextResolver; @@ -68,6 +69,8 @@ import org.xml.sax.helpers.DefaultHandler; +import com.ctc.wstx.exc.WstxEOFException; + import org.apache.cxf.annotations.SchemaValidation; import org.apache.cxf.common.jaxb.JAXBUtils; import org.apache.cxf.common.util.PackageUtils; @@ -719,7 +722,12 @@ protected static StringBuilder handleExceptionStart(Exception e) { return sb; } - protected static void handleExceptionEnd(Throwable t, String message, boolean read) { + protected static void handleExceptionEnd(Throwable t, String message, boolean read) throws NoContentException { + if (t instanceof WstxEOFException && t.getMessage().startsWith("Unexpected EOF in prolog")){ + String noContent = new org.apache.cxf.common.i18n.Message("EMPTY_BODY", BUNDLE).toString(); + LOG.warning(noContent); + throw new NoContentException(noContent); + } Response.Status status = read ? Response.Status.BAD_REQUEST : Response.Status.INTERNAL_SERVER_ERROR; Response r = JAXRSUtils.toResponseBuilder(status) @@ -728,7 +736,7 @@ protected static void handleExceptionEnd(Throwable t, String message, boolean re : ExceptionUtils.toInternalServerErrorException(t, r); } - protected void handleJAXBException(JAXBException e, boolean read) { + protected void handleJAXBException(JAXBException e, boolean read) throws NoContentException { StringBuilder sb = handleExceptionStart(e); Throwable linked = e.getLinkedException(); if (linked != null && linked.getMessage() != null) { @@ -753,7 +761,7 @@ protected void handleJAXBException(JAXBException e, boolean read) { handleExceptionEnd(t, message, read); } - protected void handleXMLStreamException(XMLStreamException e, boolean read) { + protected void handleXMLStreamException(XMLStreamException e, boolean read) throws NoContentException { StringBuilder sb = handleExceptionStart(e); handleExceptionEnd(e, sb.toString(), read); } diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparator.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparator.java new file mode 100644 index 00000000000..505acfa7961 --- /dev/null +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparator.java @@ -0,0 +1,206 @@ +/** + * 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.provider; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Comparator; + +import org.apache.cxf.jaxrs.utils.GenericsUtils; + +public class GenericArgumentComparator implements Comparator> { + + private final Class genericInterface; + + public GenericArgumentComparator(final Class genericInterface) { + if (genericInterface == null) { + throw new IllegalArgumentException("Generic Interface cannot be null"); + } + if (genericInterface.getTypeParameters().length == 0) { + throw new IllegalArgumentException("Interface has no generic type parameters: " + genericInterface); + } + if (genericInterface.getTypeParameters().length > 1) { + throw new IllegalArgumentException("Interface must have only 1 generic type parameter: " + + genericInterface); + } + this.genericInterface = genericInterface; + } + + /** + * This comparator sorts the most specific type to the top of the list. + * + * Effectively, this sorts classes in descending order with java.lang.Object + * always as the last element if present. + */ + @Override + public int compare(final Class a, final Class b) { + /* + * To keep things from being too abstract and confusing, this javadoc refers + * MessageBodyReader as the value of genericInterface. + * + * It could be any similar interface with just one generic type parameter, + * such as MessageBodyWriter, ContextResolver, Consumer, etc. + */ + + /* + * Get the actual type each class specified as its MessageBodyReader generic + * parameter. An array of one arg will be returned or null if the class does + * not implement MessageBodyReader. + */ + final Type[] aTypes = GenericsUtils.getTypeArgumentsFor(genericInterface, a); + final Type[] bTypes = GenericsUtils.getTypeArgumentsFor(genericInterface, b); + + /* + * If either class does not implement the MessageBodyReader interface and + * therefore returned a null Types array, that class should have the lower + * priority. + */ + if (aTypes == bTypes) { + return 0; + } + if (aTypes == null) { + return 1; + } + if (bTypes == null) { + return -1; + } + + /* + * We only support interfaces like MessageBodyReader that have + * just one type parameter. Neither class returned a null Type + * array so we know each properly implements the interface and + * therefore we don't need to check array lengths. + */ + final Type aType = aTypes[0]; + final Type bType = bTypes[0]; + + return compare(aType, bType); + } + + public int compare(final Type aType, final Type bType) { + if (aType == bType) { + return 0; + } + + /* + * At this point we're now dealing with actual the value each + * class specified for their MessageBodyReader implementations. + * + * Types like String, Boolean and URI will appear as a Class. + * Types like JAXBElement which themselves have a parameter will + * appear as a ParameterizedType. + * + * Let's first evaluate them as basic classes. Only if they're + * the same class do we need to look at their parameters. + */ + final Class aClass = asClass(aType); + final Class bClass = asClass(bType); + + /* + * If they aren't the same class we only need to look at the + * classes themselves and can ignore any parameters they have + */ + if (!aClass.equals(bClass)) { + /* + * For those who can't remember this cryptic method: + * + * Red.class.isAssignableFrom(Color.class) == false + * Color.class.isAssignableFrom(Red.class) == true + */ + + // bClass is a more generic version of aClass + if (bClass.isAssignableFrom(aClass)) { + return -1; + } + + // aClass is a more generic version of bClass + if (aClass.isAssignableFrom(bClass)) { + return 1; + } + + // These classes are unrelated + return 0; + } + + /* + * They are the same class, so let's look at their parameters + * and try to sort based on those. + */ + final Type aParam = getFirstParameterOrObject(aType); + final Type bParam = getFirstParameterOrObject(bType); + + return compare(aParam, bParam); + } + + private Type getFirstParameterOrObject(final Type type) { + if (!(type instanceof ParameterizedType)) { + return Object.class; + } + + final ParameterizedType parameterizedType = (ParameterizedType) type; + final Type[] types = parameterizedType.getActualTypeArguments(); + + if (types.length == 0) { + return Object.class; + } + + /* + * This parameterized type may have more than one + * generic argument (like Map or Function do). If + * so, too bad, we're ignoring it out of laziness. + * + * Feel free to come here and implement what makes + * sense to you if you need this feature. Maybe + * you have a Map and Map + * situation you want to support. + */ + return types[0]; + } + + private Class asClass(final Type aType) { + if (aType instanceof Class) { + return (Class) aType; + } + + if (aType instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) aType; + return asClass(parameterizedType.getRawType()); + } + + if (aType instanceof TypeVariable) { + final TypeVariable typeVariable = (TypeVariable) aType; + final Type[] bounds = typeVariable.getBounds(); + + if (bounds == null || bounds.length == 0) { + return Object.class; + } else { + return asClass(bounds[0]); + } + } + + if (aType instanceof WildcardType) { + // todo + return Object.class; + } + + return Object.class; + } +} diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java index c8a0affd8b5..afbf571e004 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java @@ -39,6 +39,7 @@ import java.util.TreeMap; import java.util.logging.Logger; +import javax.annotation.Priority; import javax.ws.rs.Produces; import javax.ws.rs.core.Application; import javax.ws.rs.core.Configuration; @@ -77,6 +78,8 @@ import org.apache.cxf.message.Message; import org.apache.cxf.message.MessageUtils; +import static javax.ws.rs.Priorities.USER; + public abstract class ProviderFactory { public static final String DEFAULT_FILTER_NAME_BINDING = "org.apache.cxf.filter.binding"; public static final String PROVIDER_SELECTION_PROPERTY_CHANGED = "provider.selection.property.changed"; @@ -662,6 +665,7 @@ protected void setCommonProviders(List> theProvid sortReaders(); sortWriters(); sortContextResolvers(); + sortParamConverters(); mapInterceptorFilters(readerInterceptors, readInts, ReaderInterceptor.class, true); mapInterceptorFilters(writerInterceptors, writeInts, WriterInterceptor.class, true); @@ -783,7 +787,9 @@ private void sortContextResolvers() { contextResolvers.sort(new ContextResolverComparator()); } - + private void sortParamConverters() { + paramConverters.sort(new ParamConverterComparator()); + } @@ -853,9 +859,12 @@ public void setUserProviders(List userProviders) { setProviders(true, false, userProviders.toArray()); } - private static class MessageBodyReaderComparator + static class MessageBodyReaderComparator implements Comparator>> { + private final GenericArgumentComparator classComparator = + new GenericArgumentComparator(MessageBodyReader.class); + public int compare(ProviderInfo> p1, ProviderInfo> p2) { MessageBodyReader e1 = p1.getProvider(); @@ -870,7 +879,10 @@ public int compare(ProviderInfo> p1, if (result != 0) { return result; } - result = compareClasses(e1, e2); + + final Class class1 = ClassHelper.getRealClass(e1); + final Class class2 = ClassHelper.getRealClass(e2); + result = classComparator.compare(class1, class2); if (result != 0) { return result; } @@ -878,19 +890,30 @@ public int compare(ProviderInfo> p1, if (result != 0) { return result; } - return comparePriorityStatus(p1.getProvider().getClass(), p2.getProvider().getClass()); + + result = comparePriorityStatus(p1.getProvider().getClass(), p2.getProvider().getClass()); + if (result != 0) { + return result; + } + + return p1.getProvider().getClass().getName().compareTo(p2.getProvider().getClass().getName()); } } - private static class MessageBodyWriterComparator + static class MessageBodyWriterComparator implements Comparator>> { + private final GenericArgumentComparator classComparator = + new GenericArgumentComparator(MessageBodyWriter.class); + public int compare(ProviderInfo> p1, ProviderInfo> p2) { MessageBodyWriter e1 = p1.getProvider(); MessageBodyWriter e2 = p2.getProvider(); - int result = compareClasses(e1, e2); + final Class class1 = ClassHelper.getRealClass(e1); + final Class class2 = ClassHelper.getRealClass(e2); + int result = classComparator.compare(class1, class2); if (result != 0) { return result; } @@ -909,7 +932,12 @@ public int compare(ProviderInfo> p1, return result; } - return comparePriorityStatus(p1.getProvider().getClass(), p2.getProvider().getClass()); + result = comparePriorityStatus(p1.getProvider().getClass(), p2.getProvider().getClass()); + if (result != 0) { + return result; + } + + return p1.getProvider().getClass().getName().compareTo(p2.getProvider().getClass().getName()); } } @@ -1495,4 +1523,56 @@ public void setProviderComparator(Comparator providerComparator) { writerInterceptors = sortedWriterInterceptors; } + protected static class ParamConverterComparator implements Comparator> { + + @Override + public int compare(final ProviderInfo a, + final ProviderInfo b) { + + /* + * Primary sort. Also takes care of sorting custom + * converters from system converters due to priority + * defaults + */ + int result = sortByPriority(a, b); + + /* + * Secondary sort as this list *will* change order + * once in a while between jvm restarts, which can + * have frustrating consequences for users who are + * expecting no change in behavior as they aren't + * changing their code. + */ + if (result == 0) { + result = sortByClassName(a, b); + } + + return result; + } + + public int sortByPriority(final ProviderInfo a, + final ProviderInfo b) { + final int aPriority = getPriority(a); + final int bPriority = getPriority(b); + + // Sort ascending as the priority with the lowest number wins + return Integer.compare(aPriority, bPriority); + } + + public int sortByClassName(final ProviderInfo a, + final ProviderInfo b) { + + // Sort ascending as the priority with the lowest number wins + return a.getProvider().getClass().getName().compareTo(b.getProvider().getClass().getName()); + } + + private int getPriority(final ProviderInfo providerInfo) { + final Priority priority = providerInfo.getProvider().getClass().getAnnotation(Priority.class); + if (priority!=null) { + return priority.value(); + } + return providerInfo.isCustom() ? USER : USER + 1000; + } + } + } diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/GenericsUtils.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/GenericsUtils.java new file mode 100644 index 00000000000..ad7cca85502 --- /dev/null +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/GenericsUtils.java @@ -0,0 +1,224 @@ +/** + * 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.lang.reflect.GenericDeclaration; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +public final class GenericsUtils { + + private GenericsUtils() { + // no-op + } + + /** + * Get the generic parameter for a specific interface we implement. The generic types + * of other interfaces the specified class may implement will be ignored and not reported. + * + * If the interface has multiple generic parameters then multiple types will be returned. + * If the interface has no generic parameters, then a zero-length array is returned. + * If the class does not implement this interface, null will be returned. + * + * @param intrface The interface that has generic parameters + * @param clazz The class implementing the interface and specifying the generic type + * @return the parameter types for this interface or null if the class does not implement the interface + */ + public static Type[] getTypeArgumentsFor(final Class intrface, final Class clazz) { + if (!intrface.isAssignableFrom(clazz)) { + return null; + } + + // Is this one of our immediate interfaces or super classes? + final Optional directTypes = genericTypes(clazz) + .filter(type -> type instanceof ParameterizedType) + .map(ParameterizedType.class::cast) + .filter(parameterizedType -> intrface.equals(parameterizedType.getRawType())) + .map(ParameterizedType::getActualTypeArguments) + .findFirst(); + + if (directTypes.isPresent()) { + return directTypes.get(); + } + + // Look at our parent and interface parents for the type + final Type[] types = declaredTypes(clazz) + .filter(Objects::nonNull) + .map(aClass -> getTypeArgumentsFor(intrface, aClass)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (types == null) { + // Our parent does not implement this interface. We are + // assignable to this interface, so it must be coming from + // a place we aren't yet looking. Feature gap. + return null; + } + + // The types we got back may in fact have variables in them, + // in which case we need resolve them. + for (int i = 0; i < types.length; i++) { + types[i] = resolveTypeVariable(types[i], clazz); + types[i] = resolveParameterizedTypes(types[i], clazz); + } + + return types; + } + + private static Type resolveParameterizedTypes(final Type parameterized, final Class clazz) { + // If this isn't actually a variable, return what they passed us + // as there is nothing to resolve + if (!(parameterized instanceof ParameterizedType)) { + return parameterized; + } + + final ParameterizedType parameterizedType = (ParameterizedType) parameterized; + + final Type[] types = parameterizedType.getActualTypeArguments(); + boolean modified = false; + // The types we got back may in fact have variables in them, + // in which case we need resolve them. + for (int i = 0; i < types.length; i++) { + final Type original = types[i]; + types[i] = resolveTypeVariable(types[i], clazz); + types[i] = resolveParameterizedTypes(types[i], clazz); + if (!original.equals(types[i])) { + modified = true; + } + } + + // We didn't have any work to do + if (!modified) { + return parameterized; + } + + return new ResolvedParameterizedType(parameterizedType, types); + } + + private static class ResolvedParameterizedType implements ParameterizedType { + private final ParameterizedType parameterizedType; + private final Type[] actualTypesResolved; + + ResolvedParameterizedType(final ParameterizedType parameterizedType, final Type[] actualTypes) { + this.parameterizedType = parameterizedType; + this.actualTypesResolved = actualTypes; + } + + @Override + public Type[] getActualTypeArguments() { + return actualTypesResolved; + } + + @Override + public Type getRawType() { + return parameterizedType.getRawType(); + } + + @Override + public Type getOwnerType() { + return parameterizedType.getOwnerType(); + } + + @Override + public String getTypeName() { + return parameterizedType.getTypeName(); + } + } + + private static Type resolveTypeVariable(final Type variable, final Class clazz) { + // If this isn't actually a variable, return what they passed us + // as there is nothing to resolve + if (!(variable instanceof TypeVariable)) { + return variable; + } + + final TypeVariable typeVariable = (TypeVariable) variable; + + // Where was this type variable declared? + final GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + + // At the moment we only support type variables on class definitions + // so if it isn't of type Class, return the unresolved variable + if (!(genericDeclaration instanceof Class)) { + return variable; + } + final Class declaringClass = (Class) genericDeclaration; + + // Get the position of the variable in the generic signature + // where variable names are specified + final int typePosition = positionOf(variable, declaringClass.getTypeParameters()); + + // We cannot seem to find our type variable in the list of parameters? + // This shouldn't happen, but it did. Return the unresolved variable + if (typePosition == -1) { + return variable; + } + + // Get the actual type arguments passed from the place where the declaringClass + // was used by clazz in either an 'extends' or 'implements' context + final Type[] actualTypes = genericTypes(clazz) + .filter(type -> type instanceof ParameterizedType) + .map(ParameterizedType.class::cast) + .filter(parameterizedType -> declaringClass.equals(parameterizedType.getRawType())) + .map(ParameterizedType::getActualTypeArguments) + .findFirst().orElse(null); + + // We cannot seem to find where the types are specified. We have a + // feature gap in our code. Return the unresolved variable + if (actualTypes == null) { + return variable; + } + + // We found where the actual types were supplied, but somehow the + // array lengths don't line up? This shouldn't happen, but did. + // Return the unresolved variable + if (actualTypes.length != declaringClass.getTypeParameters().length) { + return variable; + } + + final Type resolvedType = actualTypes[typePosition]; + + return resolvedType; + } + + private static Stream genericTypes(Class clazz) { + return Stream.concat(Stream.of(clazz.getGenericSuperclass()), Stream.of(clazz.getGenericInterfaces())); + } + + private static Stream> declaredTypes(Class clazz) { + return Stream.concat(Stream.of(clazz.getSuperclass()), Stream.of(clazz.getInterfaces())); + } + + private static int positionOf(final Type variable, final TypeVariable>[] typeParameters) { + for (int i = 0; i < typeParameters.length; i++) { + final TypeVariable> typeParameter = typeParameters[i]; + if (variable.equals(typeParameter)) { + return i; + } + } + return -1; + } + +} diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java index 55430931f71..72380f197e5 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java @@ -537,15 +537,19 @@ public static T handleParameter(String value, } private static RuntimeException createParamConversionException(ParameterType pType, Exception ex) { - // - // For path, query & matrix parameters this is 404, - // for others 400... - // - if (pType == ParameterType.PATH || pType == ParameterType.QUERY - || pType == ParameterType.MATRIX) { - return ExceptionUtils.toNotFoundException(ex, null); - } - return ExceptionUtils.toBadRequestException(ex, null); + /* + * Loosely related to the following section of the Jakarta REST specification: + * + * At least one of the acceptable response entity body media types is a supported output data + * format (see Section 3.5). If no methods support one of the acceptable response entity body + * media types an implementation MUST generate a NotAcceptableException (406 status) + * and no entity. + * + * Tested by: + * com.sun.ts.tests.jaxrs.ee.rs.ext.paramconverter.JAXRSClient + * atomicIntegerIsLazyDeployableAndThrowsErrorTest_from_standalone + */ + return ExceptionUtils.toNotAcceptableException(ex, null); } public static Optional> getParamConverter(Class pClass, diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java index f264b481b6a..3f0073a4d0c 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java @@ -47,6 +47,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import javax.activation.DataSource; import javax.ws.rs.ClientErrorException; import javax.ws.rs.Consumes; import javax.ws.rs.HttpMethod; @@ -65,6 +66,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; @@ -164,11 +166,11 @@ public final class JAXRSUtils { 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 NO_CONTENT_EXCEPTION = NoContentException.class.getName(); private static final String HTTP_CHARSET_PARAM = "charset"; private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; private static final Set> STREAMING_OUT_TYPES = new HashSet<>( - Arrays.asList(InputStream.class, Reader.class, StreamingOutput.class)); + Arrays.asList(InputStream.class, Reader.class, StreamingOutput.class, DataSource.class)); private JAXRSUtils() { } @@ -394,6 +396,8 @@ public static OperationResourceInfo findTargetMethod( int methodMatched = 0; int consumeMatched = 0; + boolean resourceMethodsAdded = false; + boolean generateOptionsResponse = false; List finalPathSubresources = null; for (Map.Entry> rEntry : matchedResources.entrySet()) { ClassResourceInfo resource = rEntry.getKey(); @@ -433,18 +437,21 @@ public static OperationResourceInfo findTargetMethod( if (matchProduceTypes(acceptType, ori)) { candidateList.put(ori, map); added = true; + resourceMethodsAdded = true; break; } } } //CHECKSTYLE:ON + } else if ("OPTIONS".equalsIgnoreCase(httpMethod)) { + generateOptionsResponse = true; } } } LOG.fine(matchMessageLogSupplier(ori, path, httpMethod, requestType, acceptContentTypes, added)); } } - if (finalPathSubresources != null && pathMatched > 0 + if (finalPathSubresources != null && (resourceMethodsAdded || generateOptionsResponse) && !MessageUtils.getContextualBoolean(message, KEEP_SUBRESOURCE_CANDIDATES, false)) { for (OperationResourceInfo key : finalPathSubresources) { candidateList.remove(key); @@ -1184,7 +1191,10 @@ public static T createContextValue(Message m, Type genericType, Class cla } 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)); + OperationResourceInfo operationResourceInfo = contextMessage.getExchange().get(OperationResourceInfo.class); + if (operationResourceInfo != null) { + o = new ResourceContextImpl(contextMessage, operationResourceInfo); + } } else if (Request.class.isAssignableFrom(clazz)) { o = new RequestImpl(contextMessage); } else if (Providers.class.isAssignableFrom(clazz)) { diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparatorTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparatorTest.java new file mode 100644 index 00000000000..420acd23d13 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/GenericArgumentComparatorTest.java @@ -0,0 +1,298 @@ +/** + * 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.provider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; + +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class GenericArgumentComparatorTest { + + /** + * The type parameters of each relate and have a clear inheritance order + */ + @Test + public void simpleCase() { + class Shape { + } + class Circle extends Shape { + } + abstract class ShapeReader implements MessageBodyReader { + } + abstract class CircleReader implements MessageBodyReader { + } + abstract class ObjectReader implements MessageBodyReader { + } + + final List> classes = classes(ShapeReader.class, CircleReader.class, ObjectReader.class); + + assertOrder(MessageBodyReader.class, classes, "CircleReader\n" + + "ShapeReader\n" + + "ObjectReader"); + } + + /** + * The parent implements the interface and the subclass defines the type + */ + @Test + public void subclassDefinedParameter() { + class Shape { + } + class Circle extends Shape { + } + abstract class Resolver implements ContextResolver { + } + abstract class ShapeResolver extends Resolver { + } + abstract class CircleResolver extends Resolver { + } + abstract class ObjectResolver extends Resolver { + } + + final List> classes = classes(ShapeResolver.class, CircleResolver.class, ObjectResolver.class); + + assertOrder(ContextResolver.class, classes, "CircleResolver\n" + + "ShapeResolver\n" + + "ObjectResolver"); + } + + + /** + * The type parameters of one hasn't actually been defined + */ + @Test + public void testUnspecifiedType() { + class Shape { + } + class Circle extends Shape { + } + abstract class ShapeReader implements MessageBodyReader { + } + abstract class CircleReader implements MessageBodyReader { + } + abstract class UnknownReader implements MessageBodyReader { + } + + final List> classes = classes(ShapeReader.class, CircleReader.class, UnknownReader.class); + + assertOrder(MessageBodyReader.class, classes, "CircleReader\n" + + "ShapeReader\n" + + "UnknownReader"); + } + + /** + * The type parameters have a variable with bounds + */ + @Test + public void boundedTypeVariable() { + class Shape { + } + class Circle extends Shape { + } + abstract class ShapeReader implements MessageBodyReader { + } + abstract class CircleReader implements MessageBodyReader { + } + abstract class ObjectReader implements MessageBodyReader { + } + + final List> classes = classes(ShapeReader.class, CircleReader.class, ObjectReader.class); + + assertOrder(MessageBodyReader.class, classes, "CircleReader\n" + + "ShapeReader\n" + + "ObjectReader"); + } + + /** + * The type parameters of each have no relationship + */ + @Test + public void noInheritance() { + class Red { + } + class Green { + } + class Blue { + } + + abstract class RedReader implements MessageBodyReader { + } + abstract class GreenReader implements MessageBodyReader { + } + abstract class BlueReader implements MessageBodyReader { + } + + final List> classListA = asList(RedReader.class, GreenReader.class, BlueReader.class); + assertOrder(MessageBodyReader.class, classListA, "RedReader\n" + + "GreenReader\n" + + "BlueReader"); + + final List> classListB = asList(GreenReader.class, RedReader.class, GreenReader.class, + BlueReader.class); + assertOrder(MessageBodyReader.class, classListB, "GreenReader\n" + + "RedReader\n" + + "GreenReader\n" + + "BlueReader"); + } + + /** + * One item is not a MessageBodyReader and should sort to the bottom + */ + @Test + public void oneTypeIsNotMessageBodyReader() { + + class Shape { + } + abstract class ShapeReader implements MessageBodyReader { + } + abstract class CircleReader { + } + abstract class ObjectReader implements MessageBodyReader { + } + + final List> classes = classes(ShapeReader.class, CircleReader.class, ObjectReader.class); + + assertOrder(MessageBodyReader.class, classes, "ShapeReader\n" + + "ObjectReader\n" + + "CircleReader"); + } + + /** + * All items are equal in the first type parameter and can only be distinguished + * by their nested parameter types + */ + @Test + public void nestedTypeArgument() throws Exception { + class Shape { + } + class Circle extends Shape { + } + abstract class ShapeReader implements MessageBodyReader> { + } + abstract class CircleReader implements MessageBodyReader> { + } + abstract class ObjectReader implements MessageBodyReader> { + } + + final List> classes = classes(ShapeReader.class, CircleReader.class, ObjectReader.class); + + assertOrder(MessageBodyReader.class, classes, "CircleReader\n" + + "ShapeReader\n" + + "ObjectReader"); + } + + /** + * All items are equal in the first type parameter and can only be distinguished + * by their nested parameter types. Also the type is defined by the subclasses. + */ + @Test + public void nestedTypeArgumentSubclass() throws Exception { + class Shape { + } + class Circle extends Shape { + } + + abstract class Writer implements MessageBodyWriter> { + } + abstract class ShapeWriter extends Writer { + } + abstract class CircleWriter extends Writer { + } + abstract class ObjectWriter extends Writer { + } + + final List> classes = classes(ShapeWriter.class, CircleWriter.class, ObjectWriter.class); + + assertOrder(MessageBodyWriter.class, classes, "CircleWriter\n" + + "ShapeWriter\n" + + "ObjectWriter"); + } + + /** + * Don't just grab the first generics we see, they must map directly or + * indirectly back to the interface we are interested in + */ + @Test + public void unrelatedGenericsAreIgnored() throws Exception { + class Color { + } + class Red extends Color { + } + class Crimson extends Red { + } + + class Shape { + } + class Circle extends Shape { + } + + abstract class Writer implements MessageBodyWriter> { + } + abstract class ShapeWriter extends Writer implements ContextResolver { + } + abstract class CircleWriter extends Writer { + } + abstract class ObjectWriter extends Writer { + } + + abstract class SpecialCircleWriter extends CircleWriter implements ContextResolver { + } + abstract class SpecialShapeWriter extends ShapeWriter { + } + abstract class SpecialObjectWriter extends ObjectWriter implements ContextResolver { + } + + final List> classes = classes(SpecialObjectWriter.class, SpecialCircleWriter.class, + SpecialShapeWriter.class); + + assertOrder(MessageBodyWriter.class, classes, "SpecialCircleWriter\n" + + "SpecialShapeWriter\n" + + "SpecialObjectWriter"); + } + + public static List> classes(final Class... classes) { + final List> list = new ArrayList>(asList(classes)); + Collections.shuffle(list); + return list; + } + + public static void assertOrder(final Class genericInterface, + final List> classes, + final String expected) { + final GenericArgumentComparator comparator = new GenericArgumentComparator(genericInterface); + classes.sort(comparator); + final String order = classes.stream() + .map(Class::getSimpleName) + .reduce((s, s2) -> s + "\n" + s2) + .get(); + + assertEquals(expected, order); + } + +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyReaderComparatorTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyReaderComparatorTest.java new file mode 100644 index 00000000000..2dd2e118088 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyReaderComparatorTest.java @@ -0,0 +1,198 @@ +/** + * 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.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; + +import org.apache.cxf.jaxrs.model.ProviderInfo; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MessageBodyReaderComparatorTest { + + /** + * These three providers are identical from the JAX-RS perspective. + * + * However, we want to ensure the user always gets a consistent experience + * and which provider is invoked first to be completely stable and deterministic. + * + * As often providers are discovered on the classpath via the @Provider annotation + * and classpath order is not deterministic, we provide a fallback stable sort so + * users don't experience "random failures" when doing things like restarting their + * jvm, upgrading their server, and non-coding activities that can change classpath + * order. + */ + @Test + public void sortIsStable() throws Exception { + + class Red extends Reader { + } + class Green extends Reader { + } + class Blue extends Reader { + } + + final List>> providers = Providers.readers() + .system(new Red()) + .system(new Green()) + .system(new Blue()) + .get(); + + + assertOrder(providers, "Blue\n" + + "Green\n" + + "Red"); + } + + + @Test + public void mostSpecificMediaTypeWins() { + + @Consumes("*/*") + class StarStar extends Reader { + } + @Consumes("text/plain") + class TextPlain extends Reader { + } + @Consumes("text/*") + class TextStar extends Reader { + } + + final List>> providers = Providers.readers() + .system(new StarStar()) + .system(new TextPlain()) + .system(new TextStar()) + .get(); + + + assertOrder(providers, "TextPlain\n" + + "TextStar\n" + + "StarStar"); + } + + // These Generics should be ignored + interface Alpha { + } + + // These Generics should be ignored + interface Beta { + } + + @Test + public void mostSpecificClassTypeWins() { + + class Shape { + } + class Square extends Shape { + } + + class ShapeReader extends Reader implements Alpha { + } + class SquareReader extends Reader implements Beta { + } + class ObjectReader extends Reader implements Alpha { + } + + final List>> providers = Providers.readers() + .system(new ShapeReader()) + .system(new SquareReader()) + .system(new ObjectReader()) + .get(); + + + assertOrder(providers, "SquareReader\n" + + "ShapeReader\n" + + "ObjectReader"); + } + + + public static void assertOrder(final List>> actual, + final String expected) { + final ProviderFactory.MessageBodyReaderComparator comparator = + new ProviderFactory.MessageBodyReaderComparator(); + + actual.sort(comparator); + final String order = actual.stream() + .map(ProviderInfo::getProvider) + .map(Object::getClass) + .map(Class::getSimpleName) + .reduce((s, s2) -> s + "\n" + s2) + .get(); + + assertEquals(expected, order); + } + + private static class Providers { + private final List> providers = new ArrayList<>(); + + public static Providers> readers() { + return new Providers<>(); + } + + public static Providers> writers() { + return new Providers<>(); + } + + public Providers system(final T provider) { + providers.add(new ProviderInfo<>(provider, null, false)); + return this; + } + + public Providers custom(final T provider) { + providers.add(new ProviderInfo<>(provider, null, true)); + return this; + } + + public List> get() { + Collections.shuffle(providers); + return providers; + } + } + + + public static class Reader implements MessageBodyReader { + @Override + public boolean isReadable(final Class aClass, final Type type, final Annotation[] annotations, + final MediaType mediaType) { + throw new UnsupportedOperationException(); + } + + @Override + public T readFrom(final Class aClass, final Type type, final Annotation[] annotations, + final MediaType mediaType, final MultivaluedMap multivaluedMap, + final InputStream inputStream) throws IOException, WebApplicationException { + throw new UnsupportedOperationException(); + } + } +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyWriterComparatorTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyWriterComparatorTest.java new file mode 100644 index 00000000000..377604cda07 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MessageBodyWriterComparatorTest.java @@ -0,0 +1,193 @@ +/** + * 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.provider; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; + +import org.apache.cxf.jaxrs.model.ProviderInfo; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MessageBodyWriterComparatorTest { + + /** + * These three providers are identical from the JAX-RS perspective. + * + * However, we want to ensure the user always gets a consistent experience + * and which provider is invoked first to be completely stable and deterministic. + * + * As often providers are discovered on the classpath via the @Provider annotation + * and classpath order is not deterministic, we provide a fallback stable sort so + * users don't experience "random failures" when doing things like restarting their + * jvm, upgrading their server, and non-coding activities that can change classpath + * order. + */ + @Test + public void sortIsStable() throws Exception { + + class Red extends Writer { + } + class Green extends Writer { + } + class Blue extends Writer { + } + + final List>> providers = Providers.writers() + .system(new Red()) + .system(new Green()) + .system(new Blue()) + .get(); + + + assertOrder(providers, "Blue\n" + + "Green\n" + + "Red"); + } + + + @Test + public void mostSpecificMediaTypeWins() { + + @Produces("*/*") + class StarStar extends Writer { + } + @Produces("text/plain") + class TextPlain extends Writer { + } + @Produces("text/*") + class TextStar extends Writer { + } + + final List>> providers = Providers.writers() + .system(new StarStar()) + .system(new TextPlain()) + .system(new TextStar()) + .get(); + + + assertOrder(providers, "TextPlain\n" + + "TextStar\n" + + "StarStar"); + } + + // These Generics should be ignored + interface Alpha { + } + + // These Generics should be ignored + interface Beta { + } + + @Test + public void mostSpecificClassTypeWins() { + + class Shape { + } + class Square extends Shape { + } + + class ShapeWriter extends Writer implements Alpha { + } + class SquareWriter extends Writer implements Beta { + } + class ObjectWriter extends Writer implements Alpha { + } + + final List>> providers = Providers.writers() + .system(new ShapeWriter()) + .system(new SquareWriter()) + .system(new ObjectWriter()) + .get(); + + + assertOrder(providers, "SquareWriter\n" + + "ShapeWriter\n" + + "ObjectWriter"); + } + + + public static void assertOrder(final List>> actual, + final String expected) { + final ProviderFactory.MessageBodyWriterComparator comparator = + new ProviderFactory.MessageBodyWriterComparator(); + + actual.sort(comparator); + final String order = actual.stream() + .map(ProviderInfo::getProvider) + .map(Object::getClass) + .map(Class::getSimpleName) + .reduce((s, s2) -> s + "\n" + s2) + .get(); + + assertEquals(expected, order); + } + + private static class Providers { + private final List> providers = new ArrayList<>(); + + public static Providers> writers() { + return new Providers<>(); + } + + public Providers system(final T provider) { + providers.add(new ProviderInfo<>(provider, null, false)); + return this; + } + + public Providers custom(final T provider) { + providers.add(new ProviderInfo<>(provider, null, true)); + return this; + } + + public List> get() { + Collections.shuffle(providers); + return providers; + } + } + + + public static class Writer implements MessageBodyWriter { + @Override + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + throw new UnsupportedOperationException(); + } + + @Override + public void writeTo(T t, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap multivaluedMap, OutputStream outputStream) + throws IOException, WebApplicationException { + throw new UnsupportedOperationException(); + } + } +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ParamConverterComparatorTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ParamConverterComparatorTest.java new file mode 100644 index 00000000000..7cc27e00c81 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ParamConverterComparatorTest.java @@ -0,0 +1,201 @@ +/** + * 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.provider; + +import org.apache.cxf.jaxrs.model.ProviderInfo; +import org.junit.Test; + +import javax.annotation.Priority; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ParamConverterComparatorTest { + + /** + * The @Priority annotation can be used to give precedence + * to one converter over another. Lowest number wins. + */ + @Test + public void priorityAnnotation() { + + @Priority(37) + class Red extends Provider { + } + + @Priority(11) + class Green extends Provider { + } + + @Priority(71) + class Blue extends Provider { + } + + final List> infos = Providers.converters() + .custom(new Blue()) + .custom(new Green()) + .custom(new Red()) + .get(); + + assertOrder(infos, "Green\n" + + "Red\n" + + "Blue"); + + } + + /** + * Any converters built-in by default should have a lower + * priority than user supplied converters. User supplied + * converters should be favored. + */ + @Test + public void customOverSystem() { + class Red extends Provider { + } + + class Green extends Provider { + } + + class Blue extends Provider { + } + + final List> infos = Providers.converters() + .custom(new Blue()) + .system(new Green()) + .custom(new Red()) + .get(); + + assertOrder(infos, "Blue\n" + + "Red\n" + + "Green"); + } + + /** + * Ensure the 'custom' status we keep internally does not + * take precedence over the @Priority annotation. + */ + @Test + public void priorityAnnotationBeatsCustomBoolean() { + + @Priority(37) + class Red extends Provider { + } + + @Priority(11) + class Green extends Provider { + } + + @Priority(71) + class Blue extends Provider { + } + + final List> infos = Providers.converters() + .custom(new Blue()) + .custom(new Green()) + .system(new Red()) + .get(); + + assertOrder(infos, "Green\n" + + "Red\n" + + "Blue"); + } + + /** + * We do a fallback sort on class name as when there is nothing + * else to influence order, we essentially get a JVM influenced + * list that *will* change order once in a while between jvm + * restarts. This can have frustrating consequences for users + * who are expecting no change in behavior as they aren't + * changing their code. + */ + @Test + public void fallBackClassNameSort() { + class Red extends Provider { + } + + class Green extends Provider { + } + + class Blue extends Provider { + } + + final List> infos = Providers.converters() + .custom(new Blue()) + .custom(new Green()) + .custom(new Red()) + .get(); + + assertOrder(infos, "Blue\n" + + "Green\n" + + "Red"); + } + + private static class Provider implements ParamConverterProvider { + @Override + public ParamConverter getConverter(final Class aClass, final Type type, final Annotation[] annotations) { + return null; + } + } + + private static class Providers { + private final List> providers = new ArrayList<>(); + + public static Providers converters() { + return new Providers<>(); + } + + public Providers system(final T provider) { + providers.add(new ProviderInfo<>(provider, null, false)); + return this; + } + + public Providers custom(final T provider) { + providers.add(new ProviderInfo<>(provider, null, true)); + return this; + } + + public List> get() { + Collections.shuffle(providers); + return providers; + } + } + + public static void assertOrder(final List> actual, + final String expected) { + final ProviderFactory.ParamConverterComparator comparator = + new ProviderFactory.ParamConverterComparator(); + + actual.sort(comparator); + final String order = actual.stream() + .map(ProviderInfo::getProvider) + .map(Object::getClass) + .map(Class::getSimpleName) + .reduce((s, s2) -> s + "\n" + s2) + .get(); + + assertEquals(expected, order); + } + +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/GenericsUtilsTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/GenericsUtilsTest.java new file mode 100644 index 00000000000..ee3b94b7b37 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/GenericsUtilsTest.java @@ -0,0 +1,350 @@ +/** + * 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.File; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URL; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + + +public class GenericsUtilsTest { + + @Test + public void testGetInterfaceParameter() throws Exception { + + class URIConsumer implements Consumer { + @Override + public void accept(URI uri) { + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, URIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + @Test + public void parametersSpecifiedByParent() throws Exception { + + class URIConsumer implements Consumer { + @Override + public void accept(URI uri) { + } + } + + class SpecializedConsumer extends URIConsumer { + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, + SpecializedConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + /** + * Scenario: our parent class implemented the generic interface and did + * not specify the actual type either. The actual type is declared + * by the subclass. + */ + @Test + public void parametersDeferredByParent() { + + class URIConsumer implements Consumer { + @Override + public void accept(T uri) { + } + } + + class SpecializedConsumer extends URIConsumer { + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, + SpecializedConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + /** + * Scenario: our parent class implemented the generic interface and did + * not specify the actual type either. The actual type is declared + * by the subclass. + */ + @Test + public void parametersDeferredByParentOfParent() { + + class URIConsumer implements Consumer { + @Override + public void accept(T uri) { + } + } + + class SpecializedConsumer extends URIConsumer { + } + + class VerySpecializedConsumer extends SpecializedConsumer { + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, + VerySpecializedConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + /** + * The interface we are after is coming to us from another + * interface we implement. Let's ensure we can resolve it. + */ + @Test + public void interfaceInheritance() { + + class URIConsumer implements ImprovedConsumer { + @Override + public void accept(URI uri) { + + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, URIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + interface ImprovedConsumer extends Consumer { + } + + + /** + * Our parent has a type variable that maps to + * a type variable of one of its interfaces that + * itself maps to an interface + */ + @Test + public void interfaceInheritanceVariable() { + + class URIConsumer implements ImprovedConsumer { + @Override + public void accept(R uri) { + + } + } + + class SpecializedConsumer extends URIConsumer { + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, + SpecializedConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + /** + * Our parent has a type variable that maps to + * a type variable of one of its interfaces that + * itself maps to an interface + */ + @Test + public void unrelatedGenericInterfacesAreIgnored() { + + class URIConsumer implements Consumer, Function { + @Override + public void accept(URI uri) { + } + + @Override + public File apply(URL url) { + return null; + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, URIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URI.class, interfaceTypes[0]); + } + + @Test + public void multipleParametersAreSupported() { + + class URIConsumer implements Consumer, Function { + @Override + public void accept(URI uri) { + } + + @Override + public File apply(URL url) { + return null; + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Function.class, URIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(2, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URL.class, interfaceTypes[0]); + assertEquals(File.class, interfaceTypes[1]); + } + + /** + * URIConsumer defines only one of the two type parameters, the other + * is left a type variable to be defined by the subclass. + * + * Verify we can handle resolving the variables + */ + @Test + public void mixOfDirectlyAndIndirectlyDefinedParameters() { + + class URIConsumer implements Consumer, Function { + @Override + public void accept(URI uri) { + } + + @Override + public File apply(I url) { + return null; + } + } + + class SpecializedURIConsumer extends URIConsumer { + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Function.class, + SpecializedURIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(2, interfaceTypes.length); + + // The type we're expecting is URI + assertEquals(URL.class, interfaceTypes[0]); + assertEquals(File.class, interfaceTypes[1]); + } + + @Test + public void typeParameterValueHasTypeParameter() { + + class FunctionConsumer implements Consumer> { + @Override + public void accept(Function urlFileFunction) { + + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, FunctionConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // The type we're expecting is URI + assertTrue(interfaceTypes[0] instanceof ParameterizedType); + + final ParameterizedType functionType = (ParameterizedType) interfaceTypes[0]; + + assertEquals(Function.class, functionType.getRawType()); + assertEquals(URL.class, functionType.getActualTypeArguments()[0]); + assertEquals(File.class, functionType.getActualTypeArguments()[1]); + } + + @Test + public void canResolveTypesOfParameterizedType() { + + class FunctionConsumer implements Consumer> { + @Override + public void accept(Function urlFileFunction) { + + } + } + + class SpecializedFunctionConsumer extends FunctionConsumer { + @Override + public void accept(Function urlFileFunction) { + + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Consumer.class, + SpecializedFunctionConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertEquals(1, interfaceTypes.length); + + // Function is a parameterized type + assertTrue(interfaceTypes[0] instanceof ParameterizedType); + final ParameterizedType functionType = (ParameterizedType) interfaceTypes[0]; + assertEquals(Function.class, functionType.getRawType()); + assertEquals(URL.class, functionType.getActualTypeArguments()[0]); + assertEquals(File.class, functionType.getActualTypeArguments()[1]); + } + + /** + * If the specified class does not implement the interface, null will + * be returned + */ + @Test + public void interfaceNotImplemented() throws Exception { + + class URIConsumer implements Consumer { + @Override + public void accept(URI uri) { + } + } + + final Type[] interfaceTypes = GenericsUtils.getTypeArgumentsFor(Function.class, URIConsumer.class); + + // Consumer has only one parameter, so we are expecting one type + assertNull(interfaceTypes); + } +}