/* * Copyright 2014 Harald Wellmann. * * 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.ops4j.pax.web.extender.impl; import java.io.IOException; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletContainerInitializer; import org.ops.pax.web.spi.WabModel; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.util.tracker.BundleTracker; import org.osgi.util.tracker.BundleTrackerCustomizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Extender which manages the web bundle lifecycle. This extender watches for bundles getting * started and stopped. If the bundle is a web application bundle (WAB) as indicated by the presence * of the {@code Web-ContextPath} manifest header, the extender deploys the bundle to the servlet * container, provided that all required dependencies are satisfied. It is assumed that the system * contains exactly one servlet container. * <p> * For bean bundles (i.e. WABs with CDI beans managed by Pax CDI), this extender collaborates with * the Pax CDI extender and waits for a {@code ServletContainerInitializer} service to be registered * by Pax CDI. There are no compile-time dependencies between the two extenders. * <p> * The extender builds an extended classloader for each web bundle which is passed to the servlet * container and must be used as context class loader for the web application. The bundle * classloader is <em>not</em> sufficient for this purpose. The servlet container needs to load * ServletContainerInitializers from META-INF/services resources and the corresponding classes. Any * such extensions (e.g. JSF) may need to load further META-INF resources (like tag library * descriptors) from other bundles. * <p> * For this reason, the extended classloader permits loading META-INF resources from any bundle * wired to the given WAB, delivering standard URLs (as opposed to bundle: or bundleresource: URLs). * * @author Harald Wellmann * */ @Component(immediate = true, service = {}) public class WebBundleExtender implements BundleTrackerCustomizer<WabContext> { private static Logger log = LoggerFactory.getLogger(WebBundleExtender.class); private BundleTracker<WabContext> bundleWatcher; /** * Maps bundle IDs to wab contexts. Contains an entry for each WAB. */ private Map<Long, WabContext> wabContextMap = new HashMap<>(); private Map<String, WebBundleConfiguration> configMap = new ConcurrentHashMap<>(); /** * Context of extender bundle. */ private BundleContext context; /** * Servlet container service. */ private ConfigurationAdmin configAdmin; private DeploymentService deploymentService; /** * Activates the extender and starts tracking bundles. * * @param ctx * bundle context */ @Activate public void activate(BundleContext ctx) { this.context = ctx; deploymentService.setExtender(context.getBundle()); log.info("starting WAB extender {}", context.getBundle().getSymbolicName()); this.bundleWatcher = new BundleTracker<WabContext>(context, Bundle.ACTIVE, this); bundleWatcher.open(); } /** * Deactivates the extender and stops tracking bundles. * * @param ctx * bundle context */ @Deactivate public void deactivate(BundleContext ctx) { log.info("stopping WAB extender {}", context.getBundle().getSymbolicName()); bundleWatcher.close(); } /** * Event handler for bundle start. Does nothing when the bundle is not a WAB. Parses web.xml if * present and builds a metadata model which is passed to the servlet container. * <p> * Posts DEPLOYING and DEPLOYED events. * <p> * TODO Post FAILED event on failure. Handle conflicting context paths. */ public synchronized WabContext addingBundle(Bundle bundle, BundleEvent event) { Dictionary<String, String> headers = bundle.getHeaders(); String contextPath = headers.get("Web-ContextPath"); WabContext wabContext = null; if (contextPath != null) { wabContext = wabContextMap.get(bundle.getBundleId()); boolean beanBundle = Bundles.isBeanBundle(bundle); if (wabContext == null) { wabContext = new WabContext(bundle); wabContext.setBeanBundle(beanBundle); wabContextMap.put(bundle.getBundleId(), wabContext); } WabModel webApp = new WabModel(); webApp.setBundle(bundle); webApp.setBeanBundle(beanBundle); webApp.setContextPath(contextPath); wabContext.setWabModel(webApp); findConfiguration(wabContext); if (canDeploy(wabContext)) { deploymentService.deploy(wabContext); } } return wabContext; } private WebBundleConfiguration findConfiguration(WabContext wabContext) { Bundle bundle = wabContext.getBundle(); WebBundleConfiguration deployer = configMap.get(bundle.getSymbolicName()); if (deployer == null) { try { log.info("creating new configuration for {}", bundle.getSymbolicName()); Configuration config = configAdmin.createFactoryConfiguration("org.ops4j.pax.web.deployment"); Dictionary<String,Object> props = new Hashtable<>(); props.put("bundle.symbolicName", bundle.getSymbolicName()); props.put("context.path", wabContext.getWabModel().getContextPath()); props.put("bundle.id", bundle.getBundleId()); config.update(props); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { log.info("found configuration for {}", bundle.getSymbolicName()); wabContext.setConfiguration(deployer); } return null; } /** * Checks if the given WAB can be deployed. This requires a servlet container, parsed metadata * and (if the bundle is a bean bundle) a ServletContainerInitializer provided by Pax CDI. * * @param wabContext * context of current WAB * @return true if the WAB can be deployed */ private boolean canDeploy(WabContext wabContext) { if (deploymentService == null) { log.trace("deploymentService is null"); return false; } if (wabContext.getWabModel() == null) { log.trace("wabModel is null"); return false; } if (wabContext.getConfiguration() == null) { log.trace("configuration is null"); return false; } if (wabContext.isBeanBundle()) { boolean hasInitializer = wabContext.getBeanBundleInitializer() != null; if (!hasInitializer) { log.trace("initializer is null"); } return hasInitializer; } else { return true; } } /** * TODO */ public synchronized void modifiedBundle(Bundle bundle, BundleEvent event, WabContext object) { } /** * Event handler for bundle stop. If the bundle is a WAB, the web application is undeployed. * TODO proper synchronization, the bundle might not be deployed. * */ public synchronized void removedBundle(Bundle bundle, BundleEvent event, WabContext object) { WabContext wabContext = wabContextMap.remove(bundle.getBundleId()); if (wabContext == null) { return; } deploymentService.undeploy(wabContext); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public synchronized void addServletContainerInitializer(ServletContainerInitializer sci, Map<String, Object> props) { Long bundleId = (Long) props.get(Bundles.CDI_BUNDLE_ID); if (bundleId != null) { WabContext wabContext = wabContextMap.get(bundleId); if (wabContext == null) { wabContext = new WabContext(context.getBundle(bundleId)); wabContext.setBeanBundle(true); wabContextMap.put(bundleId, wabContext); } wabContext.setBeanBundleInitializer(sci); if (canDeploy(wabContext)) { deploymentService.deploy(wabContext); } } } public synchronized void removeServletContainerInitializer(ServletContainerInitializer sci, Map<String, Object> props) { Long bundleId = (Long) props.get(Bundles.CDI_BUNDLE_ID); if (bundleId != null) { WabContext wabContext = wabContextMap.get(bundleId); if (wabContext != null) { if (wabContext.isDeployed()) { deploymentService.undeploy(wabContext); } wabContext.setBeanBundleInitializer(null); } } } @Reference public void setConfigAdmin(ConfigurationAdmin configAdmin) { this.configAdmin = configAdmin; } public void unsetConfigAdmin(ConfigurationAdmin configAdmin) { this.configAdmin = null; } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public synchronized void addConfiguration(WebBundleConfiguration deployer, Map<String, Object> props) { String symbolicName = (String) props.get("bundle.symbolicName"); if (symbolicName != null) { configMap.put(symbolicName, deployer); } Long bundleId = (Long) props.get("bundle.id"); if (bundleId != null) { WabContext wabContext = wabContextMap.get(bundleId); wabContext.setConfiguration(deployer); if (wabContext != null && canDeploy(wabContext)) { deploymentService.deploy(wabContext); } } } public synchronized void removeConfiguration(WebBundleConfiguration deployer, Map<String, Object> props) { String symbolicName = (String) props.get("bundle.symbolicName"); if (symbolicName != null) { configMap.remove(symbolicName); } } @Reference public void setDeploymentService(DeploymentService deploymentService) { this.deploymentService = deploymentService; } }