/*
* (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.opencmis.impl;
import static org.junit.Assert.assertNotNull;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import org.apache.catalina.Wrapper;
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.deploy.FilterMap;
import org.apache.catalina.startup.Tomcat;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.bindings.CmisBindingFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
import org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet;
import org.apache.chemistry.opencmis.server.shared.BasicAuthCallContextHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.coyote.ProtocolHandler;
import org.apache.tomcat.util.net.JIoEndpoint;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.resource.Resource;
import org.mortbay.thread.QueuedThreadPool;
import org.nuxeo.ecm.core.opencmis.tests.StatusLoggingDefaultHttpInvoker;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.ecm.platform.web.common.requestcontroller.filter.NuxeoRequestControllerFilter;
import org.nuxeo.ecm.platform.web.common.requestcontroller.service.FilterConfigDescriptor;
import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerManager;
import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerService;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import com.google.inject.Binder;
/**
* Feature that starts an embedded server configured for CMIS.
* <p>
* This is abstract, so subclasses can specify if AtomPub, Browser Bindings or Web Services are used
*/
@Deploy({ "org.nuxeo.ecm.platform.web.common" // for request controller
})
public abstract class CmisFeatureSessionHttp extends CmisFeatureSession {
private static final Log log = LogFactory.getLog(CmisFeatureSessionHttp.class);
public static final String BASE_RESOURCE = "jetty-test";
public static final String HOST = "localhost";
public static final int PORT = 17488;
public static final boolean USE_TOMCAT = true;
// Jetty server
public Server server;
// Tomcat server
public Tomcat tomcat;
public URI serverURI;
public Session session;
@Override
public void beforeRun(FeaturesRunner runner) throws Exception {
String repositoryName = runner.getFeature(CoreFeature.class).getRepositoryName();
setUpServer();
setUpCmisSession(repositoryName);
}
@Override
public void configure(FeaturesRunner runner, Binder binder) {
super.configure(runner, binder);
binder.bind(Session.class).toInstance(session);
}
@Override
public void afterRun(FeaturesRunner runner) throws Exception {
try {
tearDownCmisSession();
} finally {
tearDownServer();
}
}
@Override
public Session setUpCmisSession(String repositoryName) {
SessionFactory sf = SessionFactoryImpl.newInstance();
Map<String, String> params = new HashMap<String, String>();
params.put(SessionParameter.AUTHENTICATION_PROVIDER_CLASS, CmisBindingFactory.STANDARD_AUTHENTICATION_PROVIDER);
params.put(SessionParameter.CACHE_SIZE_REPOSITORIES, "10");
params.put(SessionParameter.CACHE_SIZE_TYPES, "100");
params.put(SessionParameter.CACHE_SIZE_OBJECTS, "100");
params.put(SessionParameter.REPOSITORY_ID, repositoryName);
params.put(SessionParameter.USER, USERNAME);
params.put(SessionParameter.PASSWORD, PASSWORD);
params.put(SessionParameter.HTTP_INVOKER_CLASS, StatusLoggingDefaultHttpInvoker.class.getName());
addParams(params);
session = sf.createSession(params);
return session;
}
@Override
public void tearDownCmisSession() {
if (session != null) {
session.clear();
session = null;
}
}
/** Adds protocol-specific parameters. */
protected abstract void addParams(Map<String, String> params);
protected abstract EventListener[] getEventListeners();
protected abstract Servlet getServlet();
protected List<FilterAndName> getFilters() {
return Arrays.asList( //
new FilterAndName(new NuxeoRequestControllerFilter(), "NuxeoRequestController"), //
new FilterAndName(new TrustingNuxeoAuthFilter(), "NuxeoAuthenticationFilter"));
}
public static class FilterAndName {
public Filter filter;
public String name;
public FilterAndName(Filter filter, String name) {
this.filter = filter;
this.name = name;
}
}
protected void setUpServer() throws Exception {
RequestControllerService ctrl = (RequestControllerService) Framework.getService(RequestControllerManager.class);
// use transactional config
FilterConfigDescriptor conf = new FilterConfigDescriptor("cmis-test", ".*", true, true, false, false, false,
null);
ctrl.registerFilterConfig(conf);
if (USE_TOMCAT) {
setUpTomcat();
} else {
setUpJetty();
}
}
protected void tearDownServer() throws Exception {
if (USE_TOMCAT) {
tearDownTomcat();
} else {
tearDownJetty();
}
}
protected void setUpJetty() throws Exception {
server = new Server();
Connector connector = new SocketConnector();
connector.setHost(HOST);
connector.setPort(PORT);
connector.setMaxIdleTime(60 * 1000); // 60 seconds
server.addConnector(connector);
Context context = new Context(server, "/", Context.SESSIONS);
context.setBaseResource(Resource.newClassPathResource("/" + BASE_RESOURCE));
context.setEventListeners(getEventListeners());
ServletHolder holder = new ServletHolder(getServlet());
holder.setInitParameter(CmisAtomPubServlet.PARAM_CALL_CONTEXT_HANDLER,
BasicAuthCallContextHandler.class.getName());
context.addServlet(holder, "/*");
// TODO filter
serverURI = new URI("http://" + HOST + ':' + PORT + '/');
server.start();
}
protected void tearDownJetty() throws Exception {
((QueuedThreadPool) server.getThreadPool()).setMaxStopTimeMs(1000);
server.stop();
killRemainingServerThreads();
server.join();
server = null;
}
@SuppressWarnings("deprecation")
protected void killRemainingServerThreads() throws Exception {
QueuedThreadPool tp = (QueuedThreadPool) server.getThreadPool();
int n = tp.getThreads();
if (n == 0) {
return;
}
log.error(n + " Jetty threads failed to stop, killing them");
Object threadsLock = getFieldValue(tp, "_threadsLock");
@SuppressWarnings("unchecked")
Set<Thread> threadSet = (Set<Thread>) getFieldValue(tp, "_threads");
List<Thread> threads;
synchronized (threadsLock) {
threads = new ArrayList<Thread>(threadSet);
}
for (Thread t : threads) {
t.stop(); // try to stop thread brutally
}
}
// org.apache.chemistry.opencmis.server.shared.HttpUtils.splitPath returns [""] instead of []
// if there's not at least a non-empty servlet or context, so provide one.
public static final String CONTEXT = "context";
protected void setUpTomcat() throws Exception {
tomcat = new Tomcat();
tomcat.setBaseDir("."); // for tmp dir
tomcat.setHostname(HOST);
tomcat.setPort(PORT);
ProtocolHandler p = tomcat.getConnector().getProtocolHandler();
JIoEndpoint endpoint = (JIoEndpoint) getFieldValue(p, "endpoint");
// ServerSocketFactory factory = new
// ReuseAddrServerSocketFactory(endpoint);
// endpoint.setServerSocketFactory(factory);
// endpoint.getSocketProperties().setSoReuseAddress(true);
endpoint.setMaxKeepAliveRequests(1); // vital for clean shutdown
URL url = Thread.currentThread().getContextClassLoader().getResource(BASE_RESOURCE);
assertNotNull(url);
File docBase = new File(url.getPath());
org.apache.catalina.Context context = tomcat.addContext("/" + CONTEXT, docBase.getAbsolutePath());
String SERVLET_NAME = "testServlet";
Wrapper servlet = tomcat.addServlet("/" + CONTEXT, SERVLET_NAME, getServlet());
servlet.addInitParameter(CmisAtomPubServlet.PARAM_CALL_CONTEXT_HANDLER,
BasicAuthCallContextHandler.class.getName());
servlet.addInitParameter(CmisAtomPubServlet.PARAM_CMIS_VERSION, CmisVersion.CMIS_1_1.value());
context.addServletMapping("/*", SERVLET_NAME);
context.setApplicationLifecycleListeners(getEventListeners());
for (FilterAndName f : getFilters()) {
addFilter(context, SERVLET_NAME, f.name, f.filter);
}
serverURI = new URI("http://" + HOST + ':' + PORT + '/' + CONTEXT);
tomcat.start();
}
protected void addFilter(org.apache.catalina.Context context, String servletName, String filterName, Filter filter) {
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
context.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addServletName(servletName);
context.addFilterMap(filterMap);
}
protected void tearDownTomcat() throws Exception {
if (tomcat != null) {
tomcat.stop();
tomcat.destroy();
Thread.sleep(100);
tomcat = null;
}
}
// /** Creates SO_REUSEADDR server sockets. */
// protected static class ReuseAddrServerSocketFactory extends
// DefaultServerSocketFactory {
// public ReuseAddrServerSocketFactory(AbstractEndpoint endpoint) {
// super(endpoint);
// }
//
// @Override
// public ServerSocket createSocket(int port) throws IOException {
// return createSocket(port, 50, null);
// }
//
// @Override
// public ServerSocket createSocket(int port, int backlog)
// throws IOException {
// return createSocket(port, backlog, null);
// }
//
// @Override
// public ServerSocket createSocket(int port, int backlog,
// InetAddress ifAddress) throws IOException {
// ServerSocket serverSocket = new ServerSocket();
// serverSocket.setReuseAddress(true); // SO_REUSEADDR
// serverSocket.bind(new InetSocketAddress(ifAddress, port), backlog);
// return serverSocket;
// }
// }
protected static Object getFieldValue(Object object, String name) throws Exception {
Class<? extends Object> klass = object.getClass();
Field f = null;
while (f == null && klass != Object.class) {
try {
f = klass.getDeclaredField(name);
} catch (NoSuchFieldException e) {
klass = klass.getSuperclass();
}
}
f.setAccessible(true);
return f.get(object);
}
}