/*
* Copyright 2006-2008 Web Cohesion
*
* Licensed 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.codehaus.enunciate.modules.jersey;
import freemarker.template.TemplateException;
import org.apache.commons.digester.RuleSet;
import org.codehaus.enunciate.EnunciateException;
import org.codehaus.enunciate.apt.EnunciateClasspathListener;
import org.codehaus.enunciate.apt.EnunciateFreemarkerModel;
import org.codehaus.enunciate.contract.jaxrs.ResourceMethod;
import org.codehaus.enunciate.contract.jaxrs.RootResource;
import org.codehaus.enunciate.contract.validation.ValidationException;
import org.codehaus.enunciate.contract.validation.Validator;
import org.codehaus.enunciate.main.Enunciate;
import org.codehaus.enunciate.main.webapp.BaseWebAppFragment;
import org.codehaus.enunciate.main.webapp.WebAppComponent;
import org.codehaus.enunciate.modules.FreemarkerDeploymentModule;
import org.codehaus.enunciate.modules.SpecProviderModule;
import org.codehaus.enunciate.modules.jersey.config.JerseyRuleSet;
import org.codehaus.enunciate.template.freemarker.ClassForNameMethod;
import javax.ws.rs.core.MediaType;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.*;
/**
* <h1>Jersey Module</h1>
*
* <p>The Jersey module generates and compiles the support files and classes necessary to support a REST application according to
* <a href="https://jsr311.dev.java.net/">JSR-311</a>, using <a href="https://jersey.dev.java.net/">Jersey</a>.</p>
*
* <ul>
* <li><a href="#app">Jersey Application</a></li>
* <li><a href="#steps">steps</a></li>
* <li><a href="#config">configuration</a></li>
* <li><a href="#artifacts">artifacts</a></li>
* </ul>
*
* <h1><a name="app">Jersey Application</a></h1>
*
* <p>We direct you do the documentation for <a href="https://jsr311.dev.java.net/">JAX-RS</a> and <a href="https://jersey.dev.java.net/">Jersey</a> to
* learn how to build a REST application using these technologies. However, it is important to note a few idiosyncrasies of the Enunciate-supported
* Jersey application.</p>
*
* <h3>REST subcontext</h3>
*
* <p>Because the Jersey application is presumably deployed along with other Enunciate-supported applications (JAX-WS for SOAP, API documentation, etc.),
* it will, by default, be mounted at a specific subcontext as defined in the Enunciate configuration (attribute "defaultRestSubcontext" of the
* "enunciate/services/rest" element). This means that a JAX-RS resource applied at path "mypath" will actually be mounted at "rest/mypath", assuming
* that "rest" is the subcontext (which it is by default).</p>
*
* <p>While is it recommended that the subcontext be preserved, you can disable it in the <a href="#config">configuration</a> for this module. Note, however,
* that this increases the chance of the paths of your REST resources conflicting with the paths of your documentation, SOAP endpoints, etc. Enunciate
* provides an additional check to see if a REST resource is too greedy because it has a <a href="https://jsr311.dev.java.net/nonav/javadoc/javax/ws/rs/Path.html">path
* parameter</a> in the first path segment. This can also be disabled in configuration, but doing so will effectively disable the Enunciate-generated
* documentation and other web service endpoints.</p>
*
* <h3>Content Negotiation</h3>
*
* <p>Enuncite provides a special content negotiation (conneg) to Jersey such that that each resource is mounted from the REST subcontext (see above) but
* ALSO from a subcontext that conforms to the id of each content type that the resource supports. So, if the content type id of the "application/xml"
* content type is "xml" then the resource at path "mypath" will be mounted at both "/rest/mypath" and "/xml/mypath". You can disable this path-based content
* negotiation feature by setting <tt>usePathBasedConneg="false"</tt>.</p>
*
* <p>The content types for each JAX-RS resource are declared by the @Produces annotation. The content type ids are customized with the
* "enunciate/services/rest/content-types" element in the Enunciate configuration. Enunciate supplies providers for the "application/xml" and "application/json"
* content types by default.</p>
*
* <h1><a name="steps">Steps</a></h1>
*
* <h3>generate</h3>
*
* <p>The generate step of the Jersey module generates the configuration files for a servlet-based Jersey application.</p>
*
* <h1><a name="config">Configuration</a></h1>
*
* <p>The Jersey module supports the following attributes:</p>
*
* <ul>
* <li>The "useSubcontext" attribute is used to enable/disable mounting the JAX-RS resources at the rest subcontext. Default: "true".</li>
* <li>The "usePathBasedConneg" attribute is used to enable/disable path-based conneg (see above). Default: "true".</a></li>
* <li>The "useWildcardServletMapping" attribute is used to tell Enunciate to use a wildcard to map to the jersey servlet. By default, Enunciate
* attempts to map each endpoint to a specific servlet mapping. Default: "false".</a></li>
* <li>The "disableWildcardServletError" attribute is used to enable/disable the Enunciate "wildcard" resource check. Default: "false".</a></li>
* <li>The "resourceProviderFactory" attribute is used to specify the fully-qualified classname of an instance of
* com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory that jersey will use. The default is the spring-based factory or the
* jersey default instance if spring isn't enabled.</a></li>
* <li>The "defaultNamespace" attribute is used to specify the default XML namespace. This namespace will have no prefix during XML serialization.</li>
* <li>The "loadOnStartup" attribute is used to specify the order in which the servlet is loaded on startup by the web application. By default, no order is specified.</li>
* </ul>
*
* <p>The Jersey module also supports an arbitrary number of "init-param" child elements that can be used to specify the init parameters (e.g.
* container request filters, etc.) of the Jersey servlet. The "init-param" element supports a "name" attribute and a "value" attribute.</p>
*
* <h1><a name="artifacts">Artifacts</a></h1>
*
* <p>The Jersey deployment module exports no artifacts.</p>
*
* @author Ryan Heaton
* @docFileName module_jersey.html
*/
public class JerseyDeploymentModule extends FreemarkerDeploymentModule implements EnunciateClasspathListener, SpecProviderModule {
private boolean jacksonAvailable = false;
private boolean useSubcontext = true;
private boolean usePathBasedConneg = true;
private boolean disableWildcardServletError = false;
private boolean useWildcardServletMapping = false;
private String resourceProviderFactory = null;
private String applicationClass = null;
private String defaultNamespace = null;
private String loadOnStartup = null;
private final Map<String, String> servletInitParams = new HashMap<String, String>();
/**
* @return "jersey"
*/
@Override
public String getName() {
return "jersey";
}
/**
* The root resources template URL.
*
* @return The root resources template URL.
*/
public URL getRootResourceListTemplateURL() {
return JerseyDeploymentModule.class.getResource("jaxrs-root-resources.list.fmt");
}
/**
* The providers template URL.
*
* @return The providers template URL.
*/
public URL getProvidersListTemplateURL() {
return JerseyDeploymentModule.class.getResource("jaxrs-providers.list.fmt");
}
/**
* The jaxb types template URL.
*
* @return The jaxb types template URL.
*/
public URL getJaxbTypesTemplateURL() {
return JerseyDeploymentModule.class.getResource("jaxrs-jaxb-types.list.fmt");
}
/**
* @return A new {@link JerseyValidator}.
*/
@Override
public Validator getValidator() {
return new JerseyValidator(isUseSubcontext() || !isDisableWildcardServletError());
}
@Override
public void init(Enunciate enunciate) throws EnunciateException {
super.init(enunciate);
if (!isDisabled()) {
enunciate.getConfig().addCustomResourceParameterAnnotation("com.sun.jersey.multipart.FormDataParam"); //support for multipart parameters
enunciate.getConfig().addSystemResourceParameterAnnotation("com.sun.jersey.api.core.InjectParam"); //support for inject param.
}
}
// Inherited.
@Override
public void initModel(EnunciateFreemarkerModel model) {
super.initModel(model);
if (!isDisabled()) {
Map<String, String> contentTypes2Ids = model.getContentTypesToIds();
if (getEnunciate().isModuleEnabled("amf")) { //if the amf module is enabled, we'll add amf rest endpoints.
contentTypes2Ids.put("application/x-amf", "amf");
}
else {
debug("AMF module has been disabled, so it's assumed the REST endpoints won't be available in AMF format.");
}
if (jacksonAvailable) {
contentTypes2Ids.put("application/json", "json"); //if we can load jackson, we've got json.
}
else {
debug("Couldn't find Jackson on the classpath, so it's assumed the REST endpoints aren't available in JSON format.");
}
for (RootResource resource : model.getRootResources()) {
for (ResourceMethod resourceMethod : resource.getResourceMethods(true)) {
Map<String, Set<String>> subcontextsByContentType = new HashMap<String, Set<String>>();
String subcontext = isUseSubcontext() ? getRestSubcontext() : "";
debug("Resource method %s of resource %s to be made accessible at subcontext \"%s\".",
resourceMethod.getSimpleName(), resourceMethod.getParent().getQualifiedName(), subcontext);
subcontextsByContentType.put(null, new TreeSet<String>(Arrays.asList(subcontext)));
resourceMethod.putMetaData("defaultSubcontext", subcontext);
if (isUsePathBasedConneg()) {
for (String producesMime : resourceMethod.getProducesMime()) {
MediaType producesType = MediaType.valueOf(producesMime);
for (Map.Entry<String, String> contentTypeToId : contentTypes2Ids.entrySet()) {
MediaType type = MediaType.valueOf(contentTypeToId.getKey());
if (producesType.isCompatible(type)) {
String id = '/' + contentTypeToId.getValue();
String fullpath = resourceMethod.getFullpath();
if (fullpath.startsWith(id) || fullpath.startsWith(contentTypeToId.getValue())) {
throw new ValidationException(resourceMethod.getPosition(), String.format("The path of this resource starts with \"%s\" and you've got path-based conneg enabled. So Enunciate can't tell whether a request for \"%s\" is a request for this resource or a request for the \"%s\" representation of resource \"%s\". You're going to have to either adjust the path of the resource or disable path-based conneg in the enunciate config (e.g. usePathBasedConneg=\"false\").", id, fullpath, id, fullpath.substring(fullpath.indexOf(contentTypeToId.getValue()) + contentTypeToId.getValue().length())));
}
debug("Resource method %s of resource %s to be made accessible at subcontext \"%s\" because it produces %s/%s.",
resourceMethod.getSimpleName(), resourceMethod.getParent().getQualifiedName(), id, producesType.getType(), producesType.getSubtype());
String contentTypeValue = String.format("%s/%s", type.getType(), type.getSubtype());
Set<String> subcontextList = subcontextsByContentType.get(contentTypeValue);
if (subcontextList == null) {
subcontextList = new TreeSet<String>();
subcontextsByContentType.put(contentTypeValue, subcontextList);
}
subcontextList.add(id);
}
}
}
}
resourceMethod.putMetaData("subcontexts", subcontextsByContentType);
}
}
}
}
// Inherited.
public void onClassesFound(Set<String> classes) {
jacksonAvailable |= classes.contains("org.codehaus.jackson.jaxrs.JacksonJsonProvider");
}
public void doFreemarkerGenerate() throws EnunciateException, IOException, TemplateException {
if (!isUpToDate()) {
EnunciateFreemarkerModel model = getModel();
model.put("forName", new ClassForNameMethod());
processTemplate(getRootResourceListTemplateURL(), model);
processTemplate(getProvidersListTemplateURL(), model);
processTemplate(getJaxbTypesTemplateURL(), model);
Map<String, String> conentTypesToIds = model.getContentTypesToIds();
Properties mappings = new Properties();
for (Map.Entry<String, String> contentTypeToId : conentTypesToIds.entrySet()) {
mappings.put(contentTypeToId.getValue(), contentTypeToId.getKey());
}
File file = new File(getGenerateDir(), "media-type-mappings.properties");
FileOutputStream out = new FileOutputStream(file);
mappings.store(out, "JAX-RS media type mappings.");
out.flush();
out.close();
Map<String, String> ns2prefixes = model.getNamespacesToPrefixes();
mappings = new Properties();
for (Map.Entry<String, String> ns2prefix : ns2prefixes.entrySet()) {
mappings.put(ns2prefix.getKey() == null ? "" : ns2prefix.getKey(), ns2prefix.getValue());
}
if (this.defaultNamespace != null) {
mappings.put("--DEFAULT_NAMESPACE_ALIAS--", this.defaultNamespace);
}
file = new File(getGenerateDir(), "ns2prefix.properties");
out = new FileOutputStream(file);
mappings.store(out, "Namespace to prefix mappings.");
out.flush();
out.close();
}
else {
info("Skipping generation of JAX-RS support files because everything appears up-to-date.");
}
}
@Override
protected void doBuild() throws EnunciateException, IOException {
super.doBuild();
File webappDir = getBuildDir();
webappDir.mkdirs();
File webinf = new File(webappDir, "WEB-INF");
File webinfClasses = new File(webinf, "classes");
getEnunciate().copyFile(new File(getGenerateDir(), "jaxrs-providers.list"), new File(webinfClasses, "jaxrs-providers.list"));
getEnunciate().copyFile(new File(getGenerateDir(), "jaxrs-root-resources.list"), new File(webinfClasses, "jaxrs-root-resources.list"));
getEnunciate().copyFile(new File(getGenerateDir(), "jaxrs-jaxb-types.list"), new File(webinfClasses, "jaxrs-jaxb-types.list"));
getEnunciate().copyFile(new File(getGenerateDir(), "media-type-mappings.properties"), new File(webinfClasses, "media-type-mappings.properties"));
getEnunciate().copyFile(new File(getGenerateDir(), "ns2prefix.properties"), new File(webinfClasses, "ns2prefix.properties"));
BaseWebAppFragment webappFragment = new BaseWebAppFragment(getName());
webappFragment.setBaseDir(webappDir);
WebAppComponent servletComponent = new WebAppComponent();
servletComponent.setName("jersey");
servletComponent.setClassname(EnunciateJerseyServletContainer.class.getName());
TreeMap<String, String> initParams = new TreeMap<String, String>();
initParams.putAll(getServletInitParams());
if (!isUsePathBasedConneg()) {
initParams.put(JerseyAdaptedHttpServletRequest.FEATURE_PATH_BASED_CONNEG, Boolean.FALSE.toString());
}
if (isUseSubcontext()) {
initParams.put(JerseyAdaptedHttpServletRequest.PROPERTY_SERVLET_PATH, getRestSubcontext());
}
if (getResourceProviderFactory() != null) {
initParams.put(JerseyAdaptedHttpServletRequest.PROPERTY_RESOURCE_PROVIDER_FACTORY, getResourceProviderFactory());
}
if (getApplicationClass() != null) {
initParams.put("javax.ws.rs.Application", getApplicationClass());
}
if (getLoadOnStartup() != null) {
servletComponent.setLoadOnStartup(getLoadOnStartup());
}
servletComponent.setInitParams(initParams);
TreeSet<String> urlMappings = new TreeSet<String>();
for (RootResource rootResource : getModel().getRootResources()) {
for (ResourceMethod resourceMethod : rootResource.getResourceMethods(true)) {
String resourceMethodPattern = resourceMethod.getServletPattern();
for (Set<String> subcontextList : ((Map<String, Set<String>>) resourceMethod.getMetaData().get("subcontexts")).values()) {
for (String subcontext : subcontextList) {
String servletPattern;
if ("".equals(subcontext)) {
servletPattern = isUseWildcardServletMapping() ? "/*" : resourceMethodPattern;
}
else {
servletPattern = isUseWildcardServletMapping() ? subcontext + "/*" : subcontext + resourceMethodPattern;
}
if (urlMappings.add(servletPattern)) {
debug("Resource method %s of resource %s to be made accessible by servlet pattern %s.",
resourceMethod.getSimpleName(), resourceMethod.getParent().getQualifiedName(), servletPattern);
}
}
}
}
}
if (urlMappings.contains("/*")) {
urlMappings.clear();
urlMappings.add("/*");
}
else {
Iterator<String> iterator = urlMappings.iterator();
while (iterator.hasNext()) {
String mapping = iterator.next();
if (!mapping.endsWith("/*") && urlMappings.contains(mapping + "/*")) {
iterator.remove();
}
}
}
servletComponent.setUrlMappings(urlMappings);
webappFragment.setServlets(Arrays.asList(servletComponent));
getEnunciate().addWebAppFragment(webappFragment);
}
protected String getRestSubcontext() {
String restSubcontext = getEnunciate().getConfig().getDefaultRestSubcontext();
//todo: override default rest subcontext?
return restSubcontext;
}
@Override
public RuleSet getConfigurationRules() {
return new JerseyRuleSet();
}
/**
* Whether the generated sources are up-to-date.
*
* @return Whether the generated sources are up-to-date.
*/
protected boolean isUpToDate() {
return enunciate.isUpToDateWithSources(getGenerateDir());
}
// Inherited.
public boolean isJaxwsProvider() {
return false;
}
// Inherited.
public boolean isJaxrsProvider() {
return true;
}
/**
* Whether to use the REST subcontext.
*
* @return Whether to use the REST subcontext.
*/
public boolean isUseSubcontext() {
return useSubcontext;
}
/**
* Whether to use the REST subcontext.
*
* @param useSubcontext Whether to use the REST subcontext.
*/
public void setUseSubcontext(boolean useSubcontext) {
this.useSubcontext = useSubcontext;
}
/**
* Whether to use path-based conneg.
*
* @return Whether to use path-based conneg.
*/
public boolean isUsePathBasedConneg() {
return usePathBasedConneg;
}
/**
* Whether to use path-based conneg.
*
* @param usePathBasedConneg Whether to use path-based conneg.
*/
public void setUsePathBasedConneg(boolean usePathBasedConneg) {
this.usePathBasedConneg = usePathBasedConneg;
}
/**
* The fully-qualified classname of an instance of com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory that jersey will use.
*
* @return The fully-qualified classname of an instance of com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory that jersey will use.
*/
public String getResourceProviderFactory() {
return resourceProviderFactory;
}
/**
* The fully-qualified classname of an instance of com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory that jersey will use.
*
* @param resourceProviderFactory The fully-qualified classname of an instance of com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory that jersey will use.
*/
public void setResourceProviderFactory(String resourceProviderFactory) {
this.resourceProviderFactory = resourceProviderFactory;
}
/**
* The fully-qualified classname of an instance of the implementation of javax.ws.rs.core.Application that jersey will use.
*
* @return The fully-qualified classname of an instance of the implementation of javax.ws.rs.core.Application that jersey will use.
*/
public String getApplicationClass() {
return applicationClass;
}
/**
* The fully-qualified classname of an instance of the implementation of javax.ws.rs.core.Application that jersey will use.'
*
* @param applicationClass The fully-qualified classname of an instance of the implementation of javax.ws.rs.core.Application that jersey will use.
*/
public void setApplicationClass(String applicationClass) {
this.applicationClass = applicationClass;
}
/**
* The order in which the servlet is loaded on startup by the web application.
*
* @return The order in which the servlet is loaded on startup by the web application.
*/
public String getLoadOnStartup() {
return loadOnStartup;
}
/**
* The order in which the servlet is loaded on startup by the web application.
*
* @param loadOnStartup The order in which the servlet is loaded on startup by the web application.
*/
public void setLoadOnStartup(String loadOnStartup) {
this.loadOnStartup = loadOnStartup;
}
/**
* Whether to disable the greedy servlet pattern error.
*
* @return Whether to disable the greedy servlet pattern error.
*/
public boolean isDisableWildcardServletError() {
return disableWildcardServletError;
}
/**
* Whether to disable the wildcard servlet pattern error.
*
* @param disableWildcardServletError Whether to disable the wildcard servlet pattern error.
*/
public void setDisableWildcardServletError(boolean disableWildcardServletError) {
this.disableWildcardServletError = disableWildcardServletError;
}
/**
* Whether to use the wildcard servlet mapping.
*
* @return Whether to use the wildcard servlet mapping.
*/
public boolean isUseWildcardServletMapping() {
return useWildcardServletMapping;
}
/**
* Whether to use the wildcard servlet mapping.
*
* @param useWildcardServletMapping Whether to use the wildcard servlet mapping.
*/
public void setUseWildcardServletMapping(boolean useWildcardServletMapping) {
this.useWildcardServletMapping = useWildcardServletMapping;
}
/**
* The default namespace. This namespace will have no prefix associated with it during XML serialization.
*
* @return The default namespace.
*/
public String getDefaultNamespace() {
return defaultNamespace;
}
/**
* The default namespace.
*
* @param defaultNamespace The default namespace.
*/
public void setDefaultNamespace(String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
}
/**
* Get the servlet init params.
*
* @return The servlet init params.
*/
public Map<String, String> getServletInitParams() {
return servletInitParams;
}
/**
* Add a servlet init param.
*
* @param name The name of the init param.
* @param value The value of the init param.
*/
public void addServletInitParam(String name, String value) {
this.servletInitParams.put(name, value);
}
// Inherited.
@Override
public boolean isDisabled() {
if (super.isDisabled()) {
return true;
}
else if (getModelInternal() != null && getModelInternal().getRootResources().isEmpty()) {
debug("Jersey module is disabled because there are no root resources.");
return true;
}
else if (getModelInternal() != null && getModelInternal().getEnunciateConfig() != null && getModelInternal().getEnunciateConfig().getWebAppConfig() != null && getModelInternal().getEnunciateConfig().getWebAppConfig().isDisabled()) {
debug("Module '%s' is disabled because the web application processing has been disabled.", getName());
return true;
}
return false;
}
}