/*
* JBoss, Home of Professional Open Source
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.seam.resteasy;
import static org.jboss.seam.annotations.Install.BUILT_IN;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.ext.Providers;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.core.SynchronousDispatcher;
import org.jboss.resteasy.core.ThreadLocalResteasyProviderFactory;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResourceFactory;
import org.jboss.resteasy.spi.StringConverter;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.Seam;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.JndiName;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Init;
import org.jboss.seam.deployment.AnnotationDeploymentHandler;
import org.jboss.seam.deployment.DeploymentStrategy;
import org.jboss.seam.log.Log;
import org.jboss.seam.util.EJB;
import org.jboss.seam.util.Reflections;
/**
* Detects (through scanning and configuration) JAX-RS resources and providers, then
* registers them with RESTEasy.
* <p>
* This class is a factory for <tt>org.jboss.seam.resteasy.dispatcher</tt> and it has been
* designed for extension. Alternatively, you can ignore what this class is doing and provide a
* different <tt>org.jboss.seam.resteasy.dispatcher</tt> yourself without extending this class.
* </p>
* <p>
* The main methods of this class are <tt>registerProviders()</tt> and <tt>registerResources()</tt>.
* These methods call out to the individual fine-grained registration procedures, which you can override
* if a different registration strategy for a particular type/component is desired.
* </p>
*
* @author Christian Bauer
*/
@Name("org.jboss.seam.resteasy.bootstrap")
@Scope(ScopeType.APPLICATION)
@Startup
@AutoCreate
@Install(precedence = BUILT_IN, classDependencies = "org.jboss.resteasy.spi.ResteasyProviderFactory")
public class ResteasyBootstrap
{
@Logger
Log log;
@In
protected Application application;
// The job of this class is to initialize and configure the RESTEasy Dispatcher instance
protected Dispatcher dispatcher;
@Factory("org.jboss.seam.resteasy.dispatcher")
public Dispatcher getDispatcher()
{
return dispatcher;
}
@Create
public void init()
{
log.info("bootstrapping JAX-RS application");
// Custom ResteasyProviderFactory that understands Seam component lookup at runtime
SeamResteasyProviderFactory providerFactory = createProviderFactory();
dispatcher = createDispatcher(providerFactory);
initDispatcher();
// Always use the "deployment sensitive" factory - that means it is handled through ThreadLocal, not static
// TODO: How does that actually work? It's never used because the dispatcher is created with the original one
SeamResteasyProviderFactory.setInstance(new ThreadLocalResteasyProviderFactory(providerFactory));
// Put Providers, Registry and Dispatcher into RESTEasy context.
dispatcher.getDefaultContextObjects().put(Providers.class, providerFactory);
dispatcher.getDefaultContextObjects().put(Registry.class, dispatcher.getRegistry());
dispatcher.getDefaultContextObjects().put(Dispatcher.class, dispatcher);
Map contextDataMap = SeamResteasyProviderFactory.getContextDataMap();
contextDataMap.putAll(dispatcher.getDefaultContextObjects());
// Seam can scan the classes for us, we just have to list them in META-INF/seam-deployment.properties
DeploymentStrategy deployment = (DeploymentStrategy) Component.getInstance("deploymentStrategy");
AnnotationDeploymentHandler handler =
(AnnotationDeploymentHandler) deployment.getDeploymentHandlers().get(AnnotationDeploymentHandler.NAME);
Collection<Class<?>> providers = findProviders(handler);
Collection<Class<?>> resources = findResources(handler);
Collection<Component> seamComponents = findSeamComponents();
registerProviders(seamComponents, providers);
registerResources(seamComponents, resources);
}
protected SeamResteasyProviderFactory createProviderFactory()
{
return new SeamResteasyProviderFactory();
}
protected Dispatcher createDispatcher(SeamResteasyProviderFactory providerFactory)
{
return new SynchronousDispatcher(providerFactory);
}
protected void initDispatcher()
{
getDispatcher().setLanguageMappings(application.getLanguageMappings());
getDispatcher().setMediaTypeMappings(application.getMediaTypeMappings());
}
protected Collection<Class<?>> findProviders(AnnotationDeploymentHandler handler)
{
return findTypes(
handler,
application.isScanProviders(),
javax.ws.rs.ext.Provider.class.getName(),
application.getProviderClassNames()
);
}
protected Collection<Class<?>> findResources(AnnotationDeploymentHandler handler)
{
return findTypes(
handler,
application.isScanResources(),
javax.ws.rs.Path.class.getName(),
application.getResourceClassNames()
);
}
protected Collection<Class<?>> findTypes(AnnotationDeploymentHandler handler, boolean scanClasspathForAnnotations,
String annotationFQName, Collection<String> includeTypeNames)
{
Collection<Class<?>> types = new HashSet();
if (scanClasspathForAnnotations)
{
Collection<Class<?>> annotatedTypes = handler.getClassMap().get(annotationFQName);
if (annotatedTypes != null) types.addAll(annotatedTypes);
}
try
{
for (String s : new HashSet<String>(includeTypeNames))
{
types.add(Reflections.classForName(s));
}
}
catch (ClassNotFoundException ex)
{
log.error("error loading JAX-RS type: " + ex.getMessage(), ex);
}
return types;
}
protected Collection<Component> findSeamComponents()
{
// Iterate through all variables in the application context that end with ".component"
log.debug("discovering all Seam components");
Collection<Component> seamComponents = new HashSet();
String[] applicationContextNames = Contexts.getApplicationContext().getNames();
for (String applicationContextName : applicationContextNames)
{
if (applicationContextName.endsWith(".component"))
{
Component seamComponent =
(Component) Component.getInstance(applicationContextName, ScopeType.APPLICATION);
seamComponents.add(seamComponent);
}
}
return seamComponents;
}
protected void registerProviders(Collection<Component> seamComponents, Collection<Class<?>> providerClasses)
{
// RESTEasy built-in providers first
if (application.isUseBuiltinProviders())
{
log.info("registering built-in RESTEasy providers");
RegisterBuiltin.register(getDispatcher().getProviderFactory());
}
Set<Class> handledProviders = new HashSet(); // Stuff we don't want to examine twice
/* TODO: Retracted due to missing RESTEasy SPI for external provider metadata, see https://jira.jboss.org/jira/browse/JBSEAM-4247
for (Component seamComponent : seamComponents)
{
// The component can have one (not many) @Provider annotated business interface
Class providerInterface = getAnnotatedInterface(javax.ws.rs.ext.Provider.class, seamComponent);
// How we register it depends on the component type
switch (seamComponent.getType())
{
// TODO: We don't support EJB Seam components as providers
case JAVA_BEAN:
// We are only interested in components that have a @Provider annotation on iface or bean
if (providerInterface == null
&& !seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.ext.Provider.class))
{
break;
}
// They also have to be in the right scope, otherwise we can't handle their lifecylce (yet)
switch (seamComponent.getScope())
{
case APPLICATION:
// StringConverter is a special case
if (StringConverter.class.isAssignableFrom(seamComponent.getBeanClass()))
{
getDispatcher().getProviderFactory().addStringConverter(
(StringConverter) Component.getInstance(seamComponent.getName())
);
}
else
{
registerSeamComponentProvider(seamComponent);
}
break;
default:
throw new RuntimeException(
"Provider Seam component '" + seamComponent.getName() + "' must be scoped " +
"APPLICATION"
);
}
break;
}
// We simply add everything we have seen so far... it's not really necessary but it doesn't hurt (does it?)
handledProviders.add(seamComponent.getBeanClass());
handledProviders.addAll(seamComponent.getBusinessInterfaces());
}
*/
for (Class<?> providerClass : providerClasses)
{
// An @Provider annotated type may:
// - have been already handled as a Seam component in the previous routine
if (handledProviders.contains(providerClass)) continue;
// - be a RESTEasy built-in provider
if (providerClass.getName().startsWith("org.jboss.resteasy.plugins.providers")) continue;
// - be an interface, which we don't care about if we don't have an implementation
if (providerClass.isInterface()) continue;
// - be just plain RESTEasy, no Seam component lookup or lifecycle
if (StringConverter.class.isAssignableFrom(providerClass))
{
log.debug("registering provider as RESTEasy StringConverter: {0}", providerClass);
getDispatcher().getProviderFactory().addStringConverter((Class<? extends StringConverter>) providerClass);
}
else
{
log.debug("registering provider as plain JAX-RS type: {0}", providerClass);
getDispatcher().getProviderFactory().registerProvider(providerClass);
}
}
}
protected void registerResources(Collection<Component> seamComponents, Collection<Class<?>> resourceClasses)
{
Set<Class> handledResources = new HashSet(); // Stuff we don't want to examine twice
// These classes themselves should not be registered at all
// Configured ResourceHome and ResourceQuery components will be registered later
handledResources.add(ResourceHome.class);
handledResources.add(ResourceQuery.class);
for (Component seamComponent : seamComponents)
{
// A bean class of type (not subtypes) ResourceHome or ResourceQuery annotated with @Path, then
// it's a Seam component resource we need to register with getPath() on the instance, it has been
// configured in components.xml
if (seamComponent.getBeanClass().equals(ResourceHome.class) ||
seamComponent.getBeanClass().equals(ResourceQuery.class))
{
registerHomeQueryResources(seamComponent);
continue;
}
// The component can have one (not many) @Path annotated business interface
Class resourceInterface = getAnnotatedInterface(javax.ws.rs.Path.class, seamComponent);
// How we register it depends on the component type
switch (seamComponent.getType())
{
case STATELESS_SESSION_BEAN:
// EJB seam component resources must be @Path annotated on one of their business interfaces
if (resourceInterface != null)
{
// TODO: Do we have to consider the scope? It should be stateless, right?
registerInterfaceSeamComponentResource(seamComponent, resourceInterface);
}
break;
case STATEFUL_SESSION_BEAN:
// EJB seam component resources must be @Path annotated on one of their business interfaces
if (resourceInterface != null)
{
log.error("Not implemented: Stateful EJB Seam component resource: " + seamComponent);
// TODO: registerStatefulEJBSeamComponentResource(seamComponent);
}
break;
case JAVA_BEAN:
// We are only interested in components that have a @Path annotation on iface or bean
if (resourceInterface == null
&& !seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.Path.class))
{
break;
}
// They also have to be in the right scope, otherwise we can't handle their lifecylce (yet)
switch (seamComponent.getScope())
{
case EVENT:
case APPLICATION:
case STATELESS:
case SESSION:
if (resourceInterface != null)
{
registerInterfaceSeamComponentResource(seamComponent, resourceInterface);
}
else if (seamComponent.getBeanClass().isAnnotationPresent(javax.ws.rs.Path.class))
{
registerSeamComponentResource(seamComponent);
}
break;
default:
throw new RuntimeException(
"Resource Seam component '" + seamComponent.getName() + "' must be scoped either " +
"EVENT, APPLICATION, STATELESS, or SESSION"
);
}
break;
}
// We simply add everything we have seen so far... it's not really necessary but it doesn't hurt (does it?)
handledResources.add(seamComponent.getBeanClass());
handledResources.addAll(seamComponent.getBusinessInterfaces());
}
for (Class<?> resourceClass : resourceClasses)
{
// An @Path annotated type may:
// - have been already handled as a Seam component in the previous routine
if (handledResources.contains(resourceClass)) continue;
// - be an interface, which we don't care about if we don't have an implementation
if (resourceClass.isInterface()) continue;
// - be a @Stateless EJB implementation class that was listed in components.xml
if (resourceClass.isAnnotationPresent(EJB.STATELESS))
{
registerStatelessEJBResource(resourceClass);
}
// - be a @Stateful EJB implementation class that was listed in components.xml
else if (resourceClass.isAnnotationPresent(EJB.STATEFUL))
{
throw new RuntimeException(
"Only stateless EJBs can be JAX-RS resources, remove from configuration: " + resourceClass.getName()
);
}
else
{
// - just be a regular JAX-RS lifecycle instance that can created/destroyed by RESTEasy
registerPlainResource(resourceClass);
}
}
}
protected void registerHomeQueryResources(Component seamComponent)
{
// We can always instantiate this safely here because it can't have dependencies!
AbstractResource instance = (AbstractResource) seamComponent.newInstance();
String path = instance.getPath();
if (path != null)
{
if (!path.startsWith("/")) {
path = "/" + path;
}
log.debug(
"registering resource, configured ResourceHome/Query on path {1}, as Seam component: {0}",
seamComponent.getName(),
path
);
ResourceFactory factory = new SeamResteasyResourceFactory(
seamComponent.getBeanClass(),
seamComponent,
getDispatcher().getProviderFactory()
);
getDispatcher().getRegistry().addResourceFactory(factory, path);
}
else
{
log.error("Unable to register {0} resource on null path, check components.xml", seamComponent.getName());
}
}
protected void registerSeamComponentResource(Component seamComponent)
{
log.debug("registering resource as Seam component: {0}", seamComponent.getName());
ResourceFactory factory = new SeamResteasyResourceFactory(
seamComponent.getBeanClass(),
seamComponent,
getDispatcher().getProviderFactory()
);
getDispatcher().getRegistry().addResourceFactory(factory);
}
protected void registerInterfaceSeamComponentResource(Component seamComponent, Class resourceInterface)
{
log.debug(
"registering resource, annotated interface {1}, as Seam component: {0}",
seamComponent.getName(),
resourceInterface.getName()
);
ResourceFactory factory = new SeamResteasyResourceFactory(
resourceInterface,
seamComponent,
getDispatcher().getProviderFactory()
);
getDispatcher().getRegistry().addResourceFactory(factory);
}
protected void registerStatelessEJBResource(Class ejbImplementationClass)
{
String jndiName = getJndiName(ejbImplementationClass);
log.debug(
"registering resource, stateless EJB implementation {1}, as RESTEasy JNDI resource name: {0}",
jndiName,
ejbImplementationClass.getName()
);
getDispatcher().getRegistry().addJndiResource(jndiName);
}
protected void registerPlainResource(Class plainResourceClass)
{
log.debug("registering resource, event-scoped JAX-RS lifecycle: {0}", plainResourceClass.getName());
getDispatcher().getRegistry().addResourceFactory(new POJOResourceFactory(plainResourceClass));
}
protected void registerSeamComponentProvider(Component seamComponent)
{
log.debug("registering provider as Seam component: {0}", seamComponent.getName());
getDispatcher().getProviderFactory().registerProviderInstance(
Component.getInstance(seamComponent.getName())
);
}
protected Class getAnnotatedInterface(Class<? extends Annotation> annotation, Component seamComponent)
{
Class resourceInterface = null;
for (Class anInterface : seamComponent.getBusinessInterfaces())
{
if (anInterface.isAnnotationPresent(annotation))
{
if (resourceInterface != null)
{
throw new IllegalStateException("Only one business interface can be annotated " + annotation + ": " + seamComponent);
}
resourceInterface = anInterface;
}
}
return resourceInterface;
}
protected String getJndiName(Class<?> beanClass)
{
if (beanClass.isAnnotationPresent(JndiName.class))
{
return beanClass.getAnnotation(JndiName.class).value();
}
else
{
String jndiPattern = Init.instance().getJndiPattern();
if (jndiPattern == null)
{
throw new IllegalArgumentException(
"You must specify org.jboss.seam.core.init.jndiPattern or use @JndiName: " + beanClass.getName()
);
}
return jndiPattern.replace("#{ejbName}", Seam.getEjbName(beanClass));
}
}
}