/* * $Id$ * * 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.struts2.osgi; import com.opensymphony.xwork2.config.ConfigurationException; import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.util.URLUtil; import com.opensymphony.xwork2.util.finder.ResourceFinder; import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import com.opensymphony.xwork2.ActionContext; import org.apache.commons.lang.StringUtils; import org.apache.felix.framework.Felix; import org.apache.felix.framework.util.FelixConstants; import org.apache.felix.main.AutoActivator; import org.apache.felix.main.Main; import org.apache.felix.shell.ShellService; import org.apache.struts2.StrutsStatics; import org.apache.struts2.StrutsException; import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.Constants; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleListener; import org.osgi.framework.BundleEvent; import org.osgi.util.tracker.ServiceTracker; import javax.servlet.ServletContext; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Apache felix implementation of an OsgiHost * See http://felix.apache.org/site/apache-felix-framework-launching-and-embedding.html * <br/> * Servlet config params: * <p>struts.osgi.clearBundleCache: Defaults to "true" delete installed bundles when the comntainer starts</p> * <p>struts.osgi.logLevel: Defaults to "1". Felix log level. 1 = error, 2 = warning, 3 = information, and 4 = debug </p> * <p>struts.osgi.runLevel: Defaults to "3". Run level to start the container.</p> */ public class FelixOsgiHost implements OsgiHost { private static final Logger LOG = LoggerFactory.getLogger(FelixOsgiHost.class); private Felix felix; private static final Pattern versionPattern = Pattern.compile("([\\d])+[\\.-]"); private ServletContext servletContext; protected void startFelix() { //load properties from felix embedded file Properties configProps = getProperties("default.properties"); // Copy framework properties from the system properties. Main.copySystemProperties(configProps); replaceSystemPackages(configProps); //struts, xwork and felix exported packages Properties strutsConfigProps = getProperties("struts-osgi.properties"); addExportedPackages(strutsConfigProps, configProps); //find bundles and adde em to autostart property addAutoStartBundles(configProps); // Bundle cache String storageDir = System.getProperty("java.io.tmpdir") + ".felix-cache"; configProps.setProperty(Constants.FRAMEWORK_STORAGE, storageDir); if (LOG.isDebugEnabled()) LOG.debug("Storing bundles at [#0]", storageDir); String cleanBundleCache = getServletContextParam("struts.osgi.clearBundleCache", "true"); if ("true".equalsIgnoreCase(cleanBundleCache)) { if (LOG.isDebugEnabled()) LOG.debug("Clearing bundle cache"); configProps.put(FelixConstants.FRAMEWORK_STORAGE_CLEAN, FelixConstants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); } //other properties configProps.put(FelixConstants.SERVICE_URLHANDLERS_PROP, "false"); configProps.put(FelixConstants.LOG_LEVEL_PROP, getServletContextParam("struts.osgi.logLevel", "1")); configProps.put(FelixConstants.BUNDLE_CLASSPATH, "."); configProps.put(FelixConstants.FRAMEWORK_BEGINNING_STARTLEVEL, getServletContextParam("struts.osgi.runLevel", "3")); try { List<BundleActivator> list = new ArrayList<BundleActivator>(); list.add(new AutoActivator(configProps)); configProps.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, list); felix = new Felix(configProps); felix.start(); if (LOG.isTraceEnabled()) LOG.trace("Apache Felix is running"); } catch (Exception ex) { throw new ConfigurationException("Couldn't start Apache Felix", ex); } addSpringOSGiSupport(); //add the bundle context to the ServletContext servletContext.setAttribute(OSGI_BUNDLE_CONTEXT, felix.getBundleContext()); } /** * Gets a param from the ServletContext, returning the default value if the param is not set * * @param paramName the name of the param to get from the ServletContext * @param defaultValue value to return if the param is not set * @return */ private String getServletContextParam(String paramName, String defaultValue) { return StringUtils.defaultString(this.servletContext.getInitParameter(paramName), defaultValue); } protected void addAutoStartBundles(Properties configProps) { //starts system bundles in level 1 List<String> bundleJarsLevel1 = new ArrayList<String>(); bundleJarsLevel1.add(getJarUrl(ShellService.class)); bundleJarsLevel1.add(getJarUrl(ServiceTracker.class)); configProps.put(AutoActivator.AUTO_START_PROP + ".1", StringUtils.join(bundleJarsLevel1, " ")); //get a list of directories under /bundles with numeric names (the runlevel) Map<String, String> runLevels = getRunLevelDirs("bundles"); if (runLevels.isEmpty()) { //there are no run level dirs, search for bundles in that dir List<String> bundles = getBundlesInDir("bundles"); if (!bundles.isEmpty()) configProps.put(AutoActivator.AUTO_START_PROP + ".2", StringUtils.join(bundles, " ")); } else { for (String runLevel : runLevels.keySet()) { if ("1".endsWith(runLevel)) throw new StrutsException("Run level dirs must be greater than 1. Run level 1 is reserved for the Felix bundles"); List<String> bundles = getBundlesInDir(runLevels.get(runLevel)); configProps.put(AutoActivator.AUTO_START_PROP + "." + runLevel, StringUtils.join(bundles, " ")); } } } /** * Return a list of directories under a directory whose name is a number */ protected Map<String, String> getRunLevelDirs(String dir) { Map<String, String> dirs = new HashMap<String, String>(); try { ResourceFinder finder = new ResourceFinder(); URL url = finder.find("bundles"); if (url != null) { if ("file".equals(url.getProtocol())) { File bundlesDir = new File(url.toURI()); String[] runLevelDirs = bundlesDir.list(new FilenameFilter() { public boolean accept(File file, String name) { try { return file.isDirectory() && Integer.valueOf(name) > 0; } catch (NumberFormatException ex) { //the name is not a number return false; } } }); if (runLevelDirs != null && runLevelDirs.length > 0) { //add all the dirs to the list for (String runLevel : runLevelDirs) dirs.put(runLevel, StringUtils.chomp(dir, "/") + "/" + runLevel); } else if (LOG.isDebugEnabled()) { LOG.debug("No run level directories found under the [#0] directory", dir); } } else if (LOG.isWarnEnabled()) LOG.warn("Unable to read [#0] directory", dir); } else if (LOG.isWarnEnabled()) LOG.warn("The [#0] directory was not found", dir); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Unable load bundles from the [#0] directory", e, dir); } return dirs; } protected List<String> getBundlesInDir(String dir) { List<String> bundleJars = new ArrayList<String>(); try { ResourceFinder finder = new ResourceFinder(); URL url = finder.find(dir); if (url != null) { if ("file".equals(url.getProtocol())) { File bundlesDir = new File(url.toURI()); File[] bundles = bundlesDir.listFiles(new FilenameFilter() { public boolean accept(File file, String name) { return StringUtils.endsWith(name, ".jar"); } }); if (bundles != null && bundles.length > 0) { //add all the bundles to the list for (File bundle : bundles) { String externalForm = bundle.toURI().toURL().toExternalForm(); if (LOG.isDebugEnabled()) { LOG.debug("Adding bundle [#0]", externalForm); } bundleJars.add(externalForm); } } else if (LOG.isDebugEnabled()) { LOG.debug("No bundles found under the [#0] directory", dir); } } else if (LOG.isWarnEnabled()) LOG.warn("Unable to read [#0] directory", dir); } else if (LOG.isWarnEnabled()) LOG.warn("The [#0] directory was not found", dir); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Unable load bundles from the [#0] directory", e, dir); } return bundleJars; } protected void addSpringOSGiSupport() { // see the javadoc for org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext for more details // OsgiBundleXmlWebApplicationContext expects the the BundleContext to be set in the ServletContext under the attribute // OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE try { Class clazz = Class.forName("org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext"); String key = (String) clazz.getDeclaredField("BUNDLE_CONTEXT_ATTRIBUTE").get(null); servletContext.setAttribute(key, felix.getBundleContext()); } catch (ClassNotFoundException e) { if (LOG.isDebugEnabled()) { LOG.debug("Spring OSGi support is not enabled"); } } catch (Exception e) { if (LOG.isErrorEnabled()) { LOG.error("The API of Spring OSGi has changed and the field [#0] is no longer available. The OSGi plugin needs to be updated", e, "org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE"); } } } protected String getJarUrl(Class clazz) { ProtectionDomain protectionDomain = clazz.getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URL loc = codeSource.getLocation(); return loc.toString(); } protected void replaceSystemPackages(Properties properties) { //Felix has a way to load the config file and substitution expressions //but the method does not have a way to specify the file (other than in an env variable) //${jre-${java.specification.version}} String systemPackages = (String) properties.get(Constants.FRAMEWORK_SYSTEMPACKAGES); String jreVersion = "jre-" + System.getProperty("java.version").substring(0, 3); systemPackages = systemPackages.replace("${jre-${java.specification.version}}", (String) properties.get(jreVersion)); properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages); } /* Find subpackages of the packages defined in the property file and export them */ protected void addExportedPackages(Properties strutsConfigProps, Properties configProps) { String[] rootPackages = StringUtils.split((String) strutsConfigProps.get("scanning.package.includes"), ","); ResourceFinder finder = new ResourceFinder(StringUtils.EMPTY); List<String> exportedPackages = new ArrayList<String>(); //build a list of subpackages for (String rootPackage : rootPackages) { try { String version = null; if (rootPackage.indexOf(";") > 0) { String[] splitted = rootPackage.split(";"); rootPackage = splitted[0]; version = splitted[1]; } Map<URL, Set<String>> subpackagesMap = finder.findPackagesMap(StringUtils.replace(rootPackage.trim(), ".", "/")); for (Map.Entry<URL, Set<String>> entry : subpackagesMap.entrySet()) { URL url = entry.getKey(); Set<String> packages = entry.getValue(); //get version if not set if (StringUtils.isBlank(version)) version = getVersion(url); if (packages != null) { for (String subpackage : packages) { exportedPackages.add(subpackage + "; version=" + version); } } } } catch (IOException e) { if (LOG.isErrorEnabled()) LOG.error("Unable to find subpackages of [#0]", e, rootPackage); } } //make a string with the exported packages and add it to the system properties if (!exportedPackages.isEmpty()) { String systemPackages = (String) configProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES); systemPackages = StringUtils.chomp(systemPackages, ",") + "," + StringUtils.join(exportedPackages, ","); configProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages); } } /** * Gets the version used to export the packages. it tries to get it from MANIFEST.MF, or the file name */ protected String getVersion(URL url) { if ("jar".equals(url.getProtocol())) { try { JarFile jarFile = new JarFile(new File(URLUtil.normalizeToFileProtocol(url).toURI())); Manifest manifest = jarFile.getManifest(); if (manifest != null) { String version = manifest.getMainAttributes().getValue("Bundle-Version"); if (StringUtils.isNotBlank(version)) { return getVersionFromString(version); } } else { //try to get the version from the file name return getVersionFromString(jarFile.getName()); } } catch (Exception e) { if (LOG.isErrorEnabled()) LOG.error("Unable to extract version from [#0], defaulting to '1.0.0'", url.toExternalForm()); } } return "1.0.0"; } /** * Extracts numbers followed by "." or "-" from the string and joins them with "." */ protected static String getVersionFromString(String str) { Matcher matcher = versionPattern.matcher(str); List<String> parts = new ArrayList<String>(); while (matcher.find()) { parts.add(matcher.group(1)); } //default if (parts.size() == 0) return "1.0.0"; while (parts.size() < 3) parts.add("0"); return StringUtils.join(parts, "."); } protected Properties getProperties(String fileName) { ResourceFinder finder = new ResourceFinder(""); try { return finder.findProperties(fileName); } catch (IOException e) { if (LOG.isErrorEnabled()) LOG.error("Unable to read property file [#]", fileName); return new Properties(); } } /** * This bundle map will not change, but the status of the bundles can change over time. * Use getActiveBundles() for active bundles */ public Map<String, Bundle> getBundles() { Map<String, Bundle> bundles = new HashMap<String, Bundle>(); for (Bundle bundle : felix.getBundleContext().getBundles()) { bundles.put(bundle.getSymbolicName(), bundle); } return Collections.unmodifiableMap(bundles); } public Map<String, Bundle> getActiveBundles() { Map<String, Bundle> bundles = new HashMap<String, Bundle>(); for (Bundle bundle : felix.getBundleContext().getBundles()) { if (bundle.getState() == Bundle.ACTIVE) bundles.put(bundle.getSymbolicName(), bundle); } return Collections.unmodifiableMap(bundles); } public BundleContext getBundleContext() { return felix.getBundleContext(); } public void destroy() throws Exception { felix.stop(); if (LOG.isTraceEnabled()) LOG.trace("Apache Felix has stopped"); } public void init(ServletContext servletContext) { this.servletContext = servletContext; startFelix(); } }