Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.opencastproject.kernel.rest.RestPublisher Maven / Gradle / Ivy
/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.kernel.rest;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.rest.SharedHttpContext;
import org.opencastproject.rest.StaticResource;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.NotFoundException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.cxf.Bus;
import org.apache.cxf.binding.BindingFactoryManager;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSBindingFactory;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.ext.ResourceComparator;
import org.apache.cxf.jaxrs.impl.UriInfoImpl;
import org.apache.cxf.jaxrs.lifecycle.ResourceProvider;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.model.OperationResourceInfo;
import org.apache.cxf.jaxrs.provider.json.JSONProvider;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.servlet.CXFNonSpringServlet;
import org.apache.http.HttpStatus;
import org.codehaus.jettison.mapped.Configuration;
import org.codehaus.jettison.mapped.MappedNamespaceConvention;
import org.codehaus.jettison.mapped.MappedXMLStreamWriter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
/**
* Listens for JAX-RS annotated services and publishes them to the global URL space using a single shared HttpContext.
*/
public class RestPublisher implements RestConstants {
/** The logger **/
protected static final Logger logger = LoggerFactory.getLogger(RestPublisher.class);
/** The rest publisher looks for any non-servlet with the 'opencast.service.path' property */
public static final String JAX_RS_SERVICE_FILTER = "(&(!(objectClass=javax.servlet.Servlet))(" + SERVICE_PATH_PROPERTY
+ "=*))";
/** A map that sets default xml namespaces in {@link XMLStreamWriter}s */
protected static final ConcurrentHashMap NAMESPACE_MAP;
protected List providers = null;
static {
NAMESPACE_MAP = new ConcurrentHashMap<>();
NAMESPACE_MAP.put("http://www.w3.org/2001/XMLSchema-instance", "");
}
/** The rest publisher's OSGI declarative services component context */
protected ComponentContext componentContext;
/** A service tracker that monitors JAX-RS annotated services, (un)publishing servlets as they (dis)appear */
protected ServiceTracker jaxRsTracker = null;
/**
* A bundle tracker that registers StaticResource servlets for bundles with the right headers.
*/
protected BundleTracker bundleTracker = null;
/** The base URL for this server */
protected String baseServerUri;
/** Holds references to servlets that this class publishes, so they can be unpublished later */
protected Map> servletRegistrationMap;
/** The JAX-RS Server */
private Server server;
/** The map of JAX-RS resource providers */
private Map, ResourceProvider> resourceProviders = new ConcurrentHashMap<>();
/** A token to store in the miss cache */
private Object nullToken = new Object();
private final CacheLoader, Object> servicePathLoader = new CacheLoader, Object>() {
@Override
public Object load(Class> clazz) {
ServiceReference> ref = componentContext.getBundleContext().getServiceReference(clazz.getName());
if (ref == null) {
logger.warn("No service reference found for class {}", clazz.getName());
return nullToken;
}
String servicePath = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
return StringUtils.isBlank(servicePath) ? nullToken : servicePath;
}
};
private final LoadingCache, Object> servicePathCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES).build(servicePathLoader);
/** Activates this rest publisher */
@SuppressWarnings("unchecked")
protected void activate(ComponentContext componentContext) {
logger.debug("activate()");
baseServerUri = componentContext.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
this.componentContext = componentContext;
servletRegistrationMap = new ConcurrentHashMap<>();
providers = new ArrayList<>();
JSONProvider jsonProvider = new OpencastJSONProvider();
jsonProvider.setIgnoreNamespaces(true);
jsonProvider.setNamespaceMap(NAMESPACE_MAP);
providers.add(jsonProvider);
providers.add(new ExceptionMapper() {
@Override
public Response toResponse(NotFoundException e) {
return Response
.status(HttpStatus.SC_NOT_FOUND)
.entity("The resource you requested does not exist.")
.type(MediaType.TEXT_PLAIN)
.build();
}
});
providers.add(new ExceptionMapper() {
@Override
public Response toResponse(UnauthorizedException e) {
return Response
.status(HttpStatus.SC_UNAUTHORIZED)
.entity("unauthorized")
.type(MediaType.TEXT_PLAIN)
.build();
}
});
try {
jaxRsTracker = new JaxRsServiceTracker();
bundleTracker = new StaticResourceBundleTracker(componentContext.getBundleContext());
} catch (InvalidSyntaxException e) {
throw new IllegalStateException(e);
}
jaxRsTracker.open();
bundleTracker.open();
}
/**
* Deactivates the rest publisher
*/
protected void deactivate() {
logger.debug("deactivate()");
jaxRsTracker.close();
bundleTracker.close();
}
protected class OsgiCxfEndpointComparator implements ResourceComparator {
@Override
public int compare(OperationResourceInfo oper1, OperationResourceInfo oper2, Message message) {
return compareByServiceClass(oper1.getClassResourceInfo().getServiceClass(),
oper2.getClassResourceInfo().getServiceClass(), message);
}
@Override
public int compare(ClassResourceInfo cri1, ClassResourceInfo cri2, Message message) {
return compareByServiceClass(cri1.getServiceClass(), cri2.getServiceClass(), message);
}
private int compareByServiceClass(Class> clazz1, Class> clazz2, Message message) {
if (clazz1.equals(clazz2))
return 0;
UriInfoImpl uriInfo = new UriInfoImpl(message);
String path = uriInfo.getBaseUri().getPath();
path = StringUtils.removeEnd(path, "/");
if (StringUtils.isBlank(path))
return 0;
Object servicePath1 = servicePathCache.getUnchecked(clazz1);
Object servicePath2 = servicePathCache.getUnchecked(clazz2);
if (servicePath1 != nullToken && path.equals(servicePath1)) {
return -1;
} else if (servicePath2 != nullToken && path.equals(servicePath2)) {
return 1;
} else if (servicePath1 != nullToken && servicePath2 != nullToken) {
return servicePath1.toString().compareTo(servicePath2.toString());
} else {
return 0;
}
}
}
/**
* Creates a REST endpoint for the JAX-RS annotated service.
*
* @param ref
* the osgi service reference
* @param service
* The service itself
*/
protected void createEndpoint(ServiceReference> ref, Object service) {
String serviceType = (String) ref.getProperty(SERVICE_TYPE_PROPERTY);
String servicePath = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
boolean servicePublishFlag = ref.getProperty(SERVICE_PUBLISH_PROPERTY) == null
|| Boolean.parseBoolean((String) ref.getProperty(SERVICE_PUBLISH_PROPERTY));
boolean jobProducer = Boolean.parseBoolean((String) ref.getProperty(SERVICE_JOBPRODUCER_PROPERTY));
ServiceRegistration> reg = servletRegistrationMap.get(servicePath);
if (reg != null) {
logger.debug("Rest endpoint {} is still registred, skip registering again", servicePath);
return;
}
RestServlet cxf = new RestServlet();
try {
Dictionary props = new Hashtable<>();
props.put(SharedHttpContext.ALIAS, servicePath);
props.put(SharedHttpContext.SERVLET_NAME, service.toString());
props.put(SharedHttpContext.CONTEXT_ID, RestConstants.HTTP_CONTEXT_ID);
props.put(SharedHttpContext.SHARED, "true");
props.put(SERVICE_TYPE_PROPERTY, serviceType);
props.put(SERVICE_PATH_PROPERTY, servicePath);
props.put(SERVICE_PUBLISH_PROPERTY, servicePublishFlag);
props.put(SERVICE_JOBPRODUCER_PROPERTY, jobProducer);
reg = componentContext.getBundleContext().registerService(Servlet.class.getName(), cxf, props);
} catch (Exception e) {
logger.info("Problem registering REST endpoint {} : {}", servicePath, e.getMessage());
return;
}
servletRegistrationMap.put(servicePath, reg);
// Wait for the servlet to be initialized as long as one minute. Since the servlet is published via the whiteboard,
// this may happen asynchronously. However, after 30 seconds we expect the HTTP service and the whiteboard
// implementation to be loaded and active.
int count = 0;
while (!cxf.isInitialized() && count < 300) {
logger.debug("Waiting for the servlet at '{}' to be initialized", servicePath);
try {
Thread.sleep(100);
count++;
} catch (InterruptedException e) {
logger.warn("Interrupt while waiting for RestServlet initialization");
break;
}
}
// Was initialization successful
if (!cxf.isInitialized()) {
logger.error("Whiteboard implemenation failed to pick up REST endpoint declaration {}", serviceType);
return;
}
resourceProviders.put(ref, new SingletonResourceProvider(service));
// Set up cxf
Bus bus = cxf.getBus();
JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
sf.setBus(bus);
sf.setProviders(providers);
// Set the service class
sf.setResourceProviders(new ArrayList<>(resourceProviders.values()));
sf.setResourceComparator(new OsgiCxfEndpointComparator());
sf.setAddress("/");
BindingFactoryManager manager = sf.getBus().getExtension(BindingFactoryManager.class);
JAXRSBindingFactory factory = new JAXRSBindingFactory();
factory.setBus(sf.getBus());
manager.registerBindingFactory(JAXRSBindingFactory.JAXRS_BINDING_ID, factory);
if (server != null) {
logger.debug("Destroying JAX-RS server");
server.stop();
server.destroy();
}
server = sf.create();
logger.info("Registered REST endpoint at " + servicePath);
if (service instanceof RestEndpoint) {
((RestEndpoint) service).endpointPublished();
}
}
/**
* Removes an endpoint
*
* @param alias
* The URL space to reclaim
* @param reference
* The service reference
*/
protected void destroyEndpoint(String alias, ServiceReference> reference) {
ServiceRegistration> reg = servletRegistrationMap.remove(alias);
resourceProviders.remove(reference);
if (reg != null) {
reg.unregister();
}
}
/**
* Extends the CXF JSONProvider for the grand purpose of removing '@' symbols from json and padded jsonp.
*/
protected static class OpencastJSONProvider extends JSONProvider {
private static final Charset UTF8 = Charset.forName("utf-8");
/**
* {@inheritDoc}
*/
@Override
protected XMLStreamWriter createWriter(Object actualObject, Class> actualClass, Type genericType, String enc,
OutputStream os, boolean isCollection) throws Exception {
Configuration c = new Configuration(NAMESPACE_MAP);
c.setSupressAtAttributes(true);
MappedNamespaceConvention convention = new MappedNamespaceConvention(c);
return new MappedXMLStreamWriter(convention, new OutputStreamWriter(os, UTF8)) {
@Override
public void writeStartElement(String prefix, String local, String uri) throws XMLStreamException {
super.writeStartElement("", local, "");
}
@Override
public void writeStartElement(String uri, String local) throws XMLStreamException {
super.writeStartElement("", local, "");
}
@Override
public void setPrefix(String pfx, String uri) throws XMLStreamException {
}
@Override
public void setDefaultNamespace(String uri) throws XMLStreamException {
}
};
}
}
/**
* A custom ServiceTracker that published JAX-RS annotated services with the
* {@link RestPublisher#SERVICE_PATH_PROPERTY} property set to some non-null value.
*/
public class JaxRsServiceTracker extends ServiceTracker {
JaxRsServiceTracker() throws InvalidSyntaxException {
super(componentContext.getBundleContext(),
componentContext.getBundleContext().createFilter(JAX_RS_SERVICE_FILTER), null);
}
@Override
public void removedService(ServiceReference reference, Object service) {
String servicePath = (String) reference.getProperty(SERVICE_PATH_PROPERTY);
destroyEndpoint(servicePath, reference);
super.removedService(reference, service);
}
@Override
public Object addingService(ServiceReference reference) {
Object service = componentContext.getBundleContext().getService(reference);
if (service == null) {
logger.info("JAX-RS service {} has not been instantiated yet, or has already been unregistered. Skipping "
+ "endpoint creation.", reference);
} else {
Path pathAnnotation = service.getClass().getAnnotation(Path.class);
if (pathAnnotation == null) {
logger.warn(
"{} was registered with '{}={}', but the service is not annotated with the JAX-RS "
+ "@Path annotation",
service, SERVICE_PATH_PROPERTY, reference.getProperty(SERVICE_PATH_PROPERTY));
} else {
createEndpoint(reference, service);
}
}
return super.addingService(reference);
}
}
/**
* A classloader that delegates to an OSGI bundle for loading resources.
*/
class StaticResourceClassLoader extends ClassLoader {
private Bundle bundle = null;
StaticResourceClassLoader(Bundle bundle) {
super();
this.bundle = bundle;
}
@Override
public URL getResource(String name) {
URL url = bundle.getResource(name);
logger.debug("{} found resource {} from name {}", this, url, name);
return url;
}
}
/**
* Tracks bundles containing static resources to be exposed via HTTP URLs.
*/
class StaticResourceBundleTracker extends BundleTracker {
private HashMap servlets = new HashMap<>();
/**
* Creates a new StaticResourceBundleTracker.
*
* @param context
* the bundle context
*/
StaticResourceBundleTracker(BundleContext context) {
super(context, Bundle.ACTIVE, null);
}
/**
* {@inheritDoc}
*
* @see org.osgi.util.tracker.BundleTracker#addingBundle(org.osgi.framework.Bundle, org.osgi.framework.BundleEvent)
*/
@Override
public Object addingBundle(Bundle bundle, BundleEvent event) {
String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
String welcomeFile = bundle.getHeaders().get(RestConstants.HTTP_WELCOME);
if (classpath != null && alias != null) {
Dictionary props = new Hashtable<>();
props.put(SharedHttpContext.ALIAS, alias);
props.put(SharedHttpContext.CONTEXT_ID, RestConstants.HTTP_CONTEXT_ID);
props.put(SharedHttpContext.SHARED, "true");
StaticResource servlet = new StaticResource(new StaticResourceClassLoader(bundle), classpath, alias,
welcomeFile);
// We use the newly added bundle's context to register this service, so when that bundle shuts down, it brings
// down this servlet with it
logger.debug("Registering servlet with alias {}", alias);
ServiceRegistration serviceRegistration = componentContext.getBundleContext()
.registerService(Servlet.class.getName(), servlet, props);
servlets.put(bundle, serviceRegistration);
}
return super.addingBundle(bundle, event);
}
@Override
public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
if (classpath != null && alias != null) {
ServiceRegistration serviceRegistration = servlets.get(bundle);
if (serviceRegistration != null) {
serviceRegistration.unregister();
servlets.remove(bundle);
}
}
super.removedBundle(bundle, event, object);
}
}
/**
* An HttpServlet that uses a JAX-RS service to handle requests.
*/
public class RestServlet extends CXFNonSpringServlet {
/** Serialization UID */
private static final long serialVersionUID = -8963338160276371426L;
/** Whether this servlet has been initialized by the http service */
private boolean initialized = false;
/**
* Whether the http service has initialized this servlet.
*
* @return the initialization state
*/
public boolean isInitialized() {
return initialized;
}
/**
* Default constructor needed by Jetty
*/
public RestServlet() {
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
initialized = true;
}
@Override
protected void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException {
if (request.getRequestURI().endsWith("/docs")) {
try {
response.sendRedirect("/docs.html?path=" + request.getServletPath());
} catch (IOException e) {
logger.error("Unable to redirect to rest docs: {}", ExceptionUtils.getStackTrace(e));
}
} else {
super.handleRequest(request, response);
}
}
}
}