/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.jaxrs.internal.wrappers.provider; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Providers; import org.restlet.Context; import org.restlet.data.MediaType; import org.restlet.ext.jaxrs.InstantiateException; import org.restlet.ext.jaxrs.ObjectFactory; import org.restlet.ext.jaxrs.internal.core.ThreadLocalizedContext; import org.restlet.ext.jaxrs.internal.exceptions.IllegalConstrParamTypeException; import org.restlet.ext.jaxrs.internal.exceptions.IllegalParamTypeException; import org.restlet.ext.jaxrs.internal.exceptions.IllegalTypeException; import org.restlet.ext.jaxrs.internal.exceptions.ImplementationException; import org.restlet.ext.jaxrs.internal.exceptions.InjectException; import org.restlet.ext.jaxrs.internal.exceptions.MissingAnnotationException; import org.restlet.ext.jaxrs.internal.exceptions.MissingConstructorException; import org.restlet.ext.jaxrs.internal.exceptions.ProviderNotInitializableException; import org.restlet.ext.jaxrs.internal.util.Converter; /** * Contains the entity providers and has some methods to pick the wished out. * * @author Stephan Koops */ public class JaxRsProviders implements javax.ws.rs.ext.Providers, MessageBodyReaderSet { private static final Logger localLogger = Context.getCurrentLogger(); /** * Returns the generic class of the given {@link ContextResolver} class. */ private static Class<?> getCtxResGenClass(Class<?> crClaz) { Type[] crIfTypes = crClaz.getGenericInterfaces(); for (Type crIfType : crIfTypes) { if (!(crIfType instanceof ParameterizedType)) continue; Type t = ((ParameterizedType) crIfType).getActualTypeArguments()[0]; if (!(t instanceof Class<?>)) continue; return (Class<?>) t; } return null; } private final Set<ProviderWrapper> all; /** * This {@link Set} contains all available * {@link javax.ws.rs.ext.ContextResolver}s.<br> * This field is final, because it is shared with other objects. */ private final Collection<ProviderWrapper> contextResolvers; private final Map<Class<? extends Throwable>, ProviderWrapper> excMappers; private final ExtensionBackwardMapping extensionBackwardMapping; private final Logger logger; private final List<ProviderWrapper> messageBodyReaderWrappers; private final List<ProviderWrapper> messageBodyWriterWrappers; private volatile ObjectFactory objectFactory; private final ThreadLocalizedContext tlContext; /** * Creates a new JaxRsProviders. * * @param objectFactory * @param tlContext * @param extensionBackwardMapping * @param logger */ public JaxRsProviders(ObjectFactory objectFactory, ThreadLocalizedContext tlContext, ExtensionBackwardMapping extensionBackwardMapping, Logger logger) { this.all = new CopyOnWriteArraySet<ProviderWrapper>(); this.messageBodyReaderWrappers = new CopyOnWriteArrayList<ProviderWrapper>(); this.messageBodyWriterWrappers = new CopyOnWriteArrayList<ProviderWrapper>(); this.contextResolvers = new CopyOnWriteArraySet<ProviderWrapper>(); this.excMappers = new ConcurrentHashMap<Class<? extends Throwable>, ProviderWrapper>(); this.objectFactory = objectFactory; this.tlContext = tlContext; this.extensionBackwardMapping = extensionBackwardMapping; this.logger = logger; } /** * Adds the given provider to this JaxRsProviders. If the Provider is not an * entity provider, it doesn't matter. * * @param provider * @param defaultProvider */ private void add(ProviderWrapper provider, boolean defaultProvider) { if (provider.isWriter()) { if (defaultProvider) this.messageBodyWriterWrappers.add(provider); else this.messageBodyWriterWrappers.add(0, provider); } if (provider.isReader()) { if (defaultProvider) this.messageBodyReaderWrappers.add(provider); else this.messageBodyReaderWrappers.add(0, provider); } if (provider.isContextResolver()) this.contextResolvers.add(provider); if (provider.isExceptionMapper()) this.addExcMapper(provider); this.all.add(provider); } /** * @param jaxRsProviderClass * @return true if the provider was added, or false if not */ public boolean addClass(Class<?> jaxRsProviderClass) { ProviderWrapper provider; try { provider = new PerRequestProviderWrapper(jaxRsProviderClass, objectFactory, tlContext, this, extensionBackwardMapping, this.logger); } catch (IllegalParamTypeException e) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ": Could not instantiate class " + jaxRsProviderClass.getName(); logger.log(Level.WARNING, msg, e); return false; } catch (InstantiateException e) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ": Could not instantiate class " + jaxRsProviderClass.getName(); logger.log(Level.WARNING, msg, e); return false; } catch (MissingAnnotationException e) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ": Could not instantiate class " + jaxRsProviderClass.getName() + ", because " + e.getMessage(); logger.log(Level.WARNING, msg); return false; } catch (InvocationTargetException ite) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ", because an exception occurred while instantiating"; logger.log(Level.WARNING, msg, ite); return false; } catch (IllegalArgumentException iae) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ", because it could not be instantiated"; logger.log(Level.WARNING, msg, iae); return false; } catch (MissingConstructorException mce) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ", because no valid constructor was found: " + mce.getMessage(); logger.warning(msg); return false; } catch (IllegalConstrParamTypeException e) { String msg = "Ignore provider " + jaxRsProviderClass.getName() + ", because no valid constructor was found: " + e.getMessage(); logger.warning(msg); return false; } this.add(provider, false); return true; } /** * Adds the given {@link ExceptionMapper} to this ExceptionMappers. * * @param excMapper * @throws NullPointerException * if null is given */ @SuppressWarnings("unchecked") private void addExcMapper(ProviderWrapper excMapperWrapper) { Class<? extends Throwable> excClass = (Class<? extends Throwable>) excMapperWrapper .getExcMapperType(); excMappers.put(excClass, excMapperWrapper); } /** * @param jaxRsProviderObject * @param defaultProvider * @return true if the object was added, false if not. * @throws WebApplicationException */ public boolean addSingleton(Object jaxRsProviderObject, boolean defaultProvider) throws WebApplicationException { ProviderWrapper provider; try { provider = new SingletonProvider(jaxRsProviderObject, this.logger); } catch (IllegalArgumentException iae) { String msg = "Ignore provider " + jaxRsProviderObject.getClass().getName() + ", because it could not be instantiated"; logger.log(Level.WARNING, msg, iae); return false; } this.add(provider, defaultProvider); return true; } /** * converts the cause of the given InvocationTargetException to a * {@link Response}, if an {@link ExceptionMapper} could be found.<br> * Otherwise this method returns an Response with an internal server error. * * @param cause * the thrown exception (was wrapped by an * {@link InvocationTargetException}) * @return the created Response * @throws NullPointerException * if <code>null</code> is given * @see ExceptionMapper#toResponse(Object) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public Response convert(Throwable cause) { ExceptionMapper mapper = getExceptionMapper(cause.getClass()); if (mapper == null) { if (cause instanceof RuntimeException) throw (RuntimeException) cause; if (cause instanceof Error) throw (Error) cause; String entity = "No ExceptionMapper was found, but must be found"; return Response.serverError().entity(entity) .type(javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE).build(); } Response response = mapper.toResponse(cause); if (response == null) { String message = "The ExceptionMapper returned null"; localLogger.log(Level.WARNING, message); return Response.serverError().entity(message).build(); } return response; } /** * Returns the {@link MessageBodyReader}, that best matches the given * criteria. * * @param paramType * @param genericType * @param annotations * @param mediaType * The {@link MediaType}, that should be supported. * @return the {@link MessageBodyReader}, that best matches the given * criteria, or null if no matching MessageBodyReader could be * found. * @see MessageBodyReaderSet#getBestReader(Class, Type, Annotation[], * MediaType) */ public MessageBodyReader getBestReader(Class<?> paramType, Type genericType, Annotation[] annotations, MediaType mediaType) { // NICE optimization: may be cached for speed. for (ProviderWrapper mbrw : this.messageBodyReaderWrappers) { if (mbrw.supportsRead(mediaType)) { MessageBodyReader mbr; try { mbr = mbrw.getInitializedReader(); } catch (ProviderNotInitializableException e) { continue; } catch (WebApplicationException e) { continue; } if (mbr.isReadable(paramType, genericType, annotations, Converter.toJaxRsMediaType(mediaType))) return mbr; } } return null; } /** * Get a context resolver for a particular type of context and media type. * The set of resolvers is first filtered by comparing the supplied value of * {@code mediaType} with the value of each resolver's * {@link javax.ws.rs.Produces}, ensuring the generic type of the context * resolver is assignable to the supplied value of {@code contextType}, and * eliminating those that do not match. If only one resolver matches the * criteria then it is returned. If more than one resolver matches then the * list of matching resolvers is ordered with those with the best matching * values of {@link javax.ws.rs.Produces} (x/y > x/* > */*) sorted * first. A proxy is returned that delegates calls to * {@link ContextResolver#getContext(java.lang.Class)} to each matching * context resolver in order and returns the first non-null value it obtains * or null if all matching context resolvers return null. * * @param contextType * the class of context desired * @param mediaType * the media type of data for which a context is required. * @return a matching context resolver instance or null if no matching * context providers are found. * @see Providers#getContextResolver(Class, javax.ws.rs.core.MediaType) */ @SuppressWarnings("unchecked") public <T> javax.ws.rs.ext.ContextResolver<T> getContextResolver( Class<T> contextType, javax.ws.rs.core.MediaType mediaType) { // TODO refactor JaxRsProviders.getContextResolver() // LATER test JaxRsProviders.getContextResolver for (ProviderWrapper crWrapper : this.contextResolvers) { final javax.ws.rs.ext.ContextResolver<?> cr; try { cr = crWrapper.getInitializedCtxResolver().getContextResolver(); } catch (ProviderNotInitializableException e1) { continue; } catch (WebApplicationException e1) { continue; } if (cr == null) { // TODO this is a little bit hacky. continue; } final Class<?> crClaz = cr.getClass(); final Class<?> genClass = JaxRsProviders.getCtxResGenClass(crClaz); if (genClass == null || !genClass.equals(contextType)) { continue; } if (!crWrapper.supportsWrite(mediaType)) { continue; } try { Method getContext = crClaz.getMethod("getContext", Class.class); if (getContext.getReturnType().equals(contextType)) { return (javax.ws.rs.ext.ContextResolver<T>) cr; } } catch (SecurityException e) { throw new RuntimeException( "sorry, the method getContext(Class) of ContextResolver " + crClaz + " is not accessible"); } catch (NoSuchMethodException e) { throw new RuntimeException( "The ContextResolver " + crClaz + " is not valid, because it has no method getContext(Class)"); } } return null; } /** * @param causeClass * @return the ExceptionMapper for the given Throwable class, or null, if * none was found. */ @SuppressWarnings("unchecked") public <T extends Throwable> ExceptionMapper<T> getExceptionMapper( Class<T> causeClass) { if (causeClass == null) throw new ImplementationException( "The call of an exception mapper with null is not allowed"); ProviderWrapper mapperWrapper; for (;;) { mapperWrapper = this.excMappers.get(causeClass); if (mapperWrapper != null) { try { return (ExceptionMapper<T>) mapperWrapper .getInitializedExcMapper(); } catch (ProviderNotInitializableException e) { localLogger .info("The exception mapper for " + causeClass + " could not be initialized, so it can#t be used. Will look for the next exception mapper in hierarchy."); // look for next } catch (WebApplicationException e) { localLogger .log(Level.INFO, "The exception mapper for " + causeClass + " could not be initialized, so it can#t be used. Will look for the next exception mapper in hierarchy.", e); // look for next } } Class<?> superclass = causeClass.getSuperclass(); if (superclass == null || superclass.equals(Object.class)) return null; causeClass = (Class<T>) superclass; } // disabled caching, because adding of new ExceptionMappers could // cause trouble. // this.excMappers.put(superclass, mapper); } /** * @see javax.ws.rs.ext.Providers#getMessageBodyReader(Class, Type, * Annotation[], javax.ws.rs.core.MediaType) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public <T> javax.ws.rs.ext.MessageBodyReader<T> getMessageBodyReader( Class<T> type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType) { MediaType restletMediaType = Converter.toRestletMediaType(mediaType); final MessageBodyReader mbr; mbr = getBestReader(type, genericType, annotations, restletMediaType); return (javax.ws.rs.ext.MessageBodyReader) mbr.getJaxRsReader(); } /** * @see javax.ws.rs.ext.Providers#getMessageBodyWriter(Class, Type, * Annotation[], javax.ws.rs.core.MediaType) */ @SuppressWarnings({ "unchecked" }) public <T> javax.ws.rs.ext.MessageBodyWriter<T> getMessageBodyWriter( Class<T> type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType) { MediaType restletMediaType = Converter.toRestletMediaType(mediaType); for (ProviderWrapper mbww : this.messageBodyWriterWrappers) { if (mbww.supportsWrite(restletMediaType)) { MessageBodyWriter mbw; try { mbw = mbww.getInitializedWriter(); } catch (ProviderNotInitializableException e) { continue; } catch (WebApplicationException e) { continue; } if (mbw.isWriteable(type, genericType, annotations, mediaType)) return (javax.ws.rs.ext.MessageBodyWriter<T>) mbw .getJaxRsWriter(); } } return null; } /** * Init all providers. If an error for one provider occurs, this provider is * ignored and the next provider initialized. * * @param tlContext * @param extensionBackwardMapping */ public void initAll() { for (final ProviderWrapper provider : new ArrayList<ProviderWrapper>( this.all)) { try { provider.initAtAppStartUp(tlContext, this, extensionBackwardMapping); } catch (InjectException e) { localLogger.log(Level.WARNING, "The provider " + provider.getClassName() + " could not be used", e); this.remove(provider); } catch (IllegalTypeException e) { localLogger.log(Level.WARNING, "The provider " + provider.getClassName() + " could not be used", e); this.remove(provider); } catch (InvocationTargetException e) { localLogger.log(Level.WARNING, "The provider " + provider.getClassName() + " could not be used", e.getCause()); this.remove(provider); } catch (SecurityException e) { localLogger.log(Level.WARNING, "The provider " + provider.getClassName() + " could not be used", e.getCause()); this.remove(provider); } } } /** * @param provider */ private void remove(ProviderWrapper provider) { this.all.remove(provider); this.contextResolvers.remove(provider); this.messageBodyReaderWrappers.remove(provider); this.messageBodyWriterWrappers.remove(provider); Iterator<Map.Entry<Class<? extends Throwable>, ProviderWrapper>> excMapperEntryIter = this.excMappers .entrySet().iterator(); while (excMapperEntryIter.hasNext()) { final Map.Entry<Class<? extends Throwable>, ProviderWrapper> excMapperEntry; excMapperEntry = excMapperEntryIter.next(); ProviderWrapper providerWrapper = excMapperEntry.getValue(); if (providerWrapper.equals(provider)) excMapperEntryIter.remove(); } } /** * Sets the ObjectFactory * * @param objectFactory */ public void setObjectFactory(ObjectFactory objectFactory) { this.objectFactory = objectFactory; } /** * Returns a Collection of {@link MessageBodyWriter}s, which generic type * supports the given entityClass. * * @param entityClass * @param genericType * may be null * @param annotations * @param mediaType * @return a sub set of message body writers * @see javax.ws.rs.ext.MessageBodyWriter#isWriteable(Class, Type, * Annotation[]) */ public MessageBodyWriterSubSet writerSubSet(Class<?> entityClass, Type genericType) { final List<MessageBodyWriter> mbws = new ArrayList<MessageBodyWriter>(); for (ProviderWrapper mbww : this.messageBodyWriterWrappers) { MessageBodyWriter mbw; try { mbw = mbww.getInitializedWriter(); } catch (ProviderNotInitializableException e) { continue; } if (mbw.supportsWrite(entityClass, genericType)) { mbws.add(mbw); } } // NICE optimization: may be cached for speed. return new MessageBodyWriterSubSet(mbws, entityClass, genericType); } }