/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.openejb.arquillian.openejb;
import org.apache.openejb.AppContext;
import org.apache.openejb.BeanContext;
import org.apache.openejb.ModuleTestContext;
import org.apache.openejb.OpenEJB;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.OpenEjbContainer;
import org.apache.openejb.arquillian.common.ArquillianUtil;
import org.apache.openejb.arquillian.openejb.server.ServiceManagers;
import org.apache.openejb.assembler.classic.AppInfo;
import org.apache.openejb.assembler.classic.Assembler;
import org.apache.openejb.assembler.classic.ClassListInfo;
import org.apache.openejb.assembler.classic.OpenEjbConfigurationFactory;
import org.apache.openejb.assembler.classic.ServletInfo;
import org.apache.openejb.assembler.classic.WebAppBuilder;
import org.apache.openejb.assembler.classic.WebAppInfo;
import org.apache.openejb.config.AppModule;
import org.apache.openejb.config.ConfigurationFactory;
import org.apache.openejb.config.DeploymentFilterable;
import org.apache.openejb.config.WebModule;
import org.apache.openejb.core.LocalInitialContext;
import org.apache.openejb.core.LocalInitialContextFactory;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.server.httpd.session.SessionManager;
import org.apache.openejb.web.LightweightWebAppBuilder;
import org.apache.webbeans.web.lifecycle.test.MockHttpSession;
import org.apache.webbeans.web.lifecycle.test.MockServletContext;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.container.spi.client.container.DeploymentException;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription;
import org.jboss.arquillian.container.spi.client.protocol.metadata.HTTPContext;
import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
import org.jboss.arquillian.container.spi.client.protocol.metadata.Servlet;
import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.InstanceProducer;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.test.spi.TestClass;
import org.jboss.arquillian.test.spi.annotation.SuiteScoped;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.descriptor.api.Descriptor;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import static org.apache.openejb.cdi.ScopeHelper.startContexts;
import static org.apache.openejb.cdi.ScopeHelper.stopContexts;
public class OpenEJBDeployableContainer implements DeployableContainer<OpenEJBConfiguration> {
private static final Properties PROPERTIES = new Properties();
static {
// global properties
PROPERTIES.setProperty(Context.INITIAL_CONTEXT_FACTORY, LocalInitialContextFactory.class.getName());
PROPERTIES.setProperty(LocalInitialContext.ON_CLOSE, LocalInitialContext.Close.DESTROY.name());
PROPERTIES.setProperty(DeploymentFilterable.DEPLOYMENTS_CLASSPATH_PROPERTY, "false");
try {
OpenEJBDeployableContainer.class.getClassLoader().loadClass("org.apache.openejb.server.ServiceManager");
PROPERTIES.setProperty(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE, "true");
} catch (final Exception e) {
// ignored
}
}
private static final ConcurrentMap<String, DeploymentInfo> DEPLOYMENT_INFO = new ConcurrentHashMap<String, DeploymentInfo>();
public static final AppContext NO_APP_CTX = new AppContext(null, SystemInstance.get(), null, null, null, false);
// config
private Properties properties;
// system
private Assembler assembler;
private InitialContext initialContext;
private ConfigurationFactory configurationFactory;
private Collection<Archive<?>> containerArchives;
// suite
@Inject
@DeploymentScoped
private InstanceProducer<AppInfo> appInfoProducer;
@Inject
@DeploymentScoped
private InstanceProducer<AppContext> appContextProducer;
@Inject
@SuiteScoped
private InstanceProducer<Context> contextProducer;
@Inject
@DeploymentScoped
private InstanceProducer<ServletContext> servletContextProducer;
@Inject
@DeploymentScoped
private InstanceProducer<HttpSession> sessionProducer;
@Inject
@DeploymentScoped
private InstanceProducer<Closeables> closeablesProducer;
@Inject
@SuiteScoped
private InstanceProducer<ClassLoader> classLoader;
@Inject
private Instance<Closeables> closeables;
@Inject
private Instance<ServletContext> servletContext;
@Inject
private Instance<HttpSession> session;
@Inject
private Instance<AppInfo> info;
@Inject
private Instance<AppContext> appContext;
@Inject
private Instance<TestClass> testClass;
private OpenEJBConfiguration configuration;
@Override
public Class<OpenEJBConfiguration> getConfigurationClass() {
return OpenEJBConfiguration.class;
}
@Override
public void setup(final OpenEJBConfiguration openEJBConfiguration) {
properties = new Properties();
configuration = openEJBConfiguration;
final ByteArrayInputStream bais = new ByteArrayInputStream(openEJBConfiguration.getProperties().getBytes());
try {
properties.load(bais);
} catch (final IOException e) {
throw new OpenEJBRuntimeException(e);
} finally {
IO.close(bais);
}
for (final Map.Entry<Object, Object> defaultKey : PROPERTIES.entrySet()) {
final String key = defaultKey.getKey().toString();
if (!properties.containsKey(key)) {
properties.setProperty(key, defaultKey.getValue().toString());
}
}
ArquillianUtil.preLoadClassesAsynchronously(openEJBConfiguration.getPreloadClasses());
}
@Override
public void start() throws LifecycleException {
try {
initialContext = new InitialContext(properties);
} catch (final NamingException e) {
throw new LifecycleException("can't start the OpenEJB container", e);
}
assembler = SystemInstance.get().getComponent(Assembler.class);
configurationFactory = (ConfigurationFactory) SystemInstance.get().getComponent(OpenEjbConfigurationFactory.class);
if ("true".equalsIgnoreCase(PROPERTIES.getProperty(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE))
&& SystemInstance.get().getComponent(WebAppBuilder.class) == null) {
SystemInstance.get().setComponent(WebAppBuilder.class, new LightweightWebAppBuilder());
}
contextProducer.set(initialContext);
containerArchives = ArquillianUtil.toDeploy(properties);
final Closeables globalScopeCloseables = new Closeables();
SystemInstance.get().setComponent(Closeables.class, globalScopeCloseables);
for (final Archive<?> archive : containerArchives) {
try {
quickDeploy(archive, testClass.get(), globalScopeCloseables);
} catch (final DeploymentException e) {
Logger.getLogger(OpenEJBDeployableContainer.class.getName()).log(Level.SEVERE, e.getMessage(), e);
}
}
}
@Override
public ProtocolMetaData deploy(final Archive<?> archive) throws DeploymentException {
final DeploymentInfo info;
try {
final Closeables cl = new Closeables();
closeablesProducer.set(cl);
info = quickDeploy(archive, testClass.get(), cl);
// try to switch module context jndi to let test use java:module naming
// we could put the managed bean in the war but then test class should respect all the
// container rules (CDI) which is not the case with this solution
if (archive.getName().endsWith(".war")) {
final List<BeanContext> beanContexts = info.appCtx.getBeanContexts();
if (beanContexts.size() > 1) {
final Iterator<BeanContext> it = beanContexts.iterator();
while (it.hasNext()) {
final BeanContext next = it.next();
if (ModuleTestContext.class.isInstance(next.getModuleContext()) && BeanContext.Comp.class != next.getBeanClass()) {
for (final BeanContext b : beanContexts) {
if (b.getModuleContext() != next.getModuleContext()) {
ModuleTestContext.class.cast(next.getModuleContext())
.setModuleJndiContextOverride(b.getModuleContext().getModuleJndiContext());
break;
}
}
break;
}
}
}
}
servletContextProducer.set(info.appServletContext);
sessionProducer.set(info.appSession);
appInfoProducer.set(info.appInfo);
appContextProducer.set(info.appCtx);
final ClassLoader loader = info.appCtx.getWebContexts().isEmpty() ? info.appCtx.getClassLoader() : info.appCtx.getWebContexts().iterator().next().getClassLoader();
classLoader.set(loader == null ? info.appCtx.getClassLoader() : loader);
} catch (final Exception e) {
throw new DeploymentException("can't deploy " + archive.getName(), e);
}
// if service manager is started allow @ArquillianResource URL injection
if (PROPERTIES.containsKey(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE)) {
final ProtocolMetaData metaData = ServiceManagers.protocolMetaData(appInfoProducer.get());
HTTPContext http = null;
for (final WebAppInfo webapp : info.appInfo.webApps) {
for (final ServletInfo servletInfo : webapp.servlets) {
if (http == null) {
http = HTTPContext.class.cast(metaData.getContexts().iterator().next());
http.add(new Servlet(servletInfo.servletName, webapp.contextRoot));
}
}
for (final ClassListInfo classListInfo : webapp.webAnnotatedClasses) {
for (final String path : classListInfo.list) {
if (!path.contains("!")) {
continue;
}
if (http == null) {
http = HTTPContext.class.cast(metaData.getContexts().iterator().next());
}
http.add(new Servlet(path.substring(path.lastIndexOf('!') + 2).replace(".class", "").replace("/", "."), webapp.contextRoot));
}
}
}
if (metaData != null) {
return metaData;
}
}
return new ProtocolMetaData();
}
private DeploymentInfo quickDeploy(final Archive<?> archive, final TestClass testClass, final Closeables cls) throws DeploymentException {
final String name = archive.getName();
DeploymentInfo info = DEPLOYMENT_INFO.get(name);
if (info == null) {
try {
final AppModule module = OpenEJBArchiveProcessor.createModule(archive, testClass, cls);
final AppInfo appInfo = configurationFactory.configureApplication(module);
final WebAppBuilder webAppBuilder = SystemInstance.get().getComponent(WebAppBuilder.class);
final boolean isEmbeddedWebAppBuilder = webAppBuilder != null && LightweightWebAppBuilder.class.isInstance(webAppBuilder);
if (isEmbeddedWebAppBuilder) {
// for now we keep the same classloader, open to discussion if we should recreate it, not sure it does worth it
final LightweightWebAppBuilder lightweightWebAppBuilder = LightweightWebAppBuilder.class.cast(webAppBuilder);
for (final WebModule w : module.getWebModules()) {
final String moduleId = w.getModuleId();
lightweightWebAppBuilder.setClassLoader(moduleId, w.getClassLoader());
cls.add(new Closeable() {
@Override
public void close() throws IOException {
lightweightWebAppBuilder.removeClassLoader(moduleId);
}
});
}
}
final AppContext appCtx = assembler.createApplication(appInfo, module.getClassLoader());
if (isEmbeddedWebAppBuilder && PROPERTIES.containsKey(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE) && !appCtx.getWebContexts().isEmpty()) {
cls.add(new Closeable() {
@Override
public void close() throws IOException {
try {
final SessionManager sessionManager = SystemInstance.get().getComponent(SessionManager.class);
if (sessionManager != null) {
for (final WebContext web : appCtx.getWebContexts()) {
sessionManager.destroy(web);
}
}
} catch (final Throwable e) {
// no-op
}
}
});
}
final ServletContext appServletContext = new MockServletContext();
final HttpSession appSession = new MockHttpSession();
if (configuration.isStartDefaultScopes() && appCtx.getWebBeansContext() != null) {
startContexts(appCtx.getWebBeansContext().getContextsService(), appServletContext, appSession);
}
info = new DeploymentInfo(appServletContext, appSession, appInfo, appCtx);
if (configuration.isSingleDeploymentByArchiveName(name)) {
DEPLOYMENT_INFO.putIfAbsent(name, info);
}
} catch (final Exception e) {
throw new DeploymentException("can't deploy " + name, e);
}
}
return info;
}
@Override
public void undeploy(final Archive<?> archive) throws DeploymentException {
final Closeables cl = closeables.get();
if (cl != null) {
try {
cl.close();
} catch (final IOException e) {
// no-op
}
}
// reset classloader for next text
// otherwise if it was closed something can fail
classLoader.set(OpenEJBDeployableContainer.class.getClassLoader());
final AppContext ctx = appContext.get();
if (ctx == null) {
return;
} else {
appContextProducer.set(NO_APP_CTX); // release all references of the previous one - classloaders whatever arquillian Instance impl is etc
}
try {
if (!configuration.isSingleDeploymentByArchiveName(archive.getName())) {
assembler.destroyApplication(info.get().path);
}
if (configuration.isStartDefaultScopes() && ctx.getWebBeansContext() != null) {
stopContexts(ctx.getWebBeansContext().getContextsService(), servletContext.get(), session.get());
}
} catch (final Exception e) {
throw new DeploymentException("can't undeploy " + archive.getName(), e);
}
}
@Override
public void stop() throws LifecycleException {
ArquillianUtil.undeploy(this, containerArchives);
try {
if (initialContext != null) {
initialContext.close();
}
Closeables closeables = SystemInstance.get().getComponent(Closeables.class);
if (closeables != null) {
closeables.close();
}
} catch (final NamingException e) {
throw new LifecycleException("can't close the OpenEJB container", e);
} catch (final IOException e) {
// no-op: close() of classloaders, not a big deal at this moment
} finally {
OpenEJB.destroy();
}
}
@Override
public ProtocolDescription getDefaultProtocol() {
return new ProtocolDescription("Local");
}
@Override
public void deploy(final Descriptor descriptor) throws DeploymentException {
throw new UnsupportedOperationException();
}
@Override
public void undeploy(final Descriptor descriptor) throws DeploymentException {
throw new UnsupportedOperationException();
}
private static final class DeploymentInfo {
public final ServletContext appServletContext;
public final HttpSession appSession;
public final AppInfo appInfo;
public final AppContext appCtx;
private DeploymentInfo(final ServletContext appServletContext, final HttpSession appSession, final AppInfo appInfo, final AppContext appCtx) {
this.appServletContext = appServletContext;
this.appSession = appSession;
this.appInfo = appInfo;
this.appCtx = appCtx;
}
}
}