blob: 5dc46968259b61e0a5aebcd1bd62a6bf3d680cf3 [file] [log] [blame]
/*
* 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.host;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.FileManagerFactory;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterfaceDelegate;
import com.opensymphony.xwork2.util.finder.ResourceFinder;
import com.opensymphony.xwork2.util.fs.DefaultFileManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.main.AutoProcessor;
import org.apache.felix.shell.ShellService;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsException;
import org.apache.struts2.osgi.OsgiUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.util.tracker.ServiceTracker;
import javax.servlet.ServletContext;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
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;
/**
* A base OsgiHost implementation
* <br>
* Servlet config params:
* <p>struts.osgi.searchForPropertiesFilesInRelativePath: Defaults to "false". Set to "true" for fallback search for properties files in relative path (e.g. for unit testing).</p>
*/
public abstract class BaseOsgiHost implements OsgiHost {
private static final Logger LOG = LogManager.getLogger(BaseOsgiHost.class);
protected static final Pattern VERSION_PATTERN = Pattern.compile("([\\d]+)(?:[\\.-]|$)");
protected static final String SCANNING_PACKAGE_INCLUDES = "scanning.package.includes";
protected static final String JRE_JAVA_SPECIFICATION_VERSION = "${jre-${java.specification.version}}";
protected static final String DETECT_JAVA_VERSION = "${detect.java.version}";
/**
* @deprecated use {@link #VERSION_PATTERN} instead
*/
@Deprecated
protected static final Pattern versionPattern = VERSION_PATTERN;
protected ServletContext servletContext;
@Override
public abstract void init(ServletContext servletContext);
@Override
public abstract void destroy() throws Exception;
/**
* This bundle map will not change, but the status of the bundles can change over time.
* Use getActiveBundles() for active bundles
*
* @return map with bundles
*/
@Override
public abstract Map<String, Bundle> getBundles();
@Override
public abstract Map<String, Bundle> getActiveBundles();
@Override
public abstract BundleContext getBundleContext();
protected abstract void addSpringOSGiSupport();
/**
* 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 param from the ServletContext, returning the default value if the param is not set
*/
protected String getServletContextParam(String paramName, String defaultValue) {
if (this.servletContext == null) {
throw new IllegalStateException("OSGi Host servlet context is null!");
}
return StringUtils.defaultString(this.servletContext.getInitParameter(paramName), defaultValue);
}
protected void addAutoStartBundles(Properties configProps) {
//starts system bundles in level 1
List<String> bundleJarsLevel1 = new ArrayList<>();
bundleJarsLevel1.add(getJarUrl(ShellService.class));
bundleJarsLevel1.add(getJarUrl(ServiceTracker.class));
configProps.put(AutoProcessor.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(AutoProcessor.AUTO_START_PROP + ".2", StringUtils.join(bundles, " "));
}
} else {
runLevels.entrySet().forEach(runLevel -> {
String runLevelKey = runLevel.getKey();
if ("1".endsWith(runLevelKey)) {
throw new StrutsException("Run level dirs must be greater than 1. Run level 1 is reserved for the Felix bundles");
}
List<String> bundles = getBundlesInDir(runLevel.getValue());
configProps.put(AutoProcessor.AUTO_START_PROP + "." + runLevelKey, StringUtils.join(bundles, " "));
});
}
}
/**
* @param dir directory
* @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<>();
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((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.removeEnd(dir, "/") + "/" + runLevel);
}
} else {
LOG.debug("No run level directories found under the [{}] directory", dir);
}
} else {
LOG.warn("Unable to read [{}] directory. Protocol [{}] is not supported (try exploded WAR files)", dir, url.getProtocol());
}
} else {
LOG.warn("The [{}] directory was not found", dir);
}
} catch (Exception e) {
LOG.warn("Unable load bundles from the [{}] directory", dir, e);
}
return dirs;
}
protected List<String> getBundlesInDir(String dir) {
List<String> bundleJars = new ArrayList<>();
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((File file, String name) -> 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();
LOG.debug("Adding bundle [{}]", externalForm);
bundleJars.add(externalForm);
}
} else {
LOG.debug("No bundles found under the [{}] directory", dir);
}
} else {
LOG.warn("Unable to read [{}] directory. Protocol [{}] is not supported (try exploded WAR files)", dir, url.getProtocol());
}
} else {
LOG.warn("The [{}] directory was not found", dir);
}
} catch (Exception e) {
LOG.warn("Unable load bundles from the [{}] directory", dir, e);
}
return bundleJars;
}
protected String getJarUrl(Class clazz) {
ProtectionDomain protectionDomain = clazz.getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URL loc = codeSource.getLocation();
return loc.toString();
}
/**
* Replace all instances of {@link JRE_JAVA_SPECIFICATION_VERSION}, within the {@link Constants.FRAMEWORK_SYSTEMPACKAGES}
* property of the provided properties. The replacement will be the value for the key "jre-x.y"
* in the properties parameter (where x.y is the JRE version after transforming the System "java.version" property.
* For example: "jre-x.y" is "jre-1.8" for Java 8, "jre-9.0" for Java 9, and "jre-11.0" for Java 11.
*
* While performing the replacement, the elements within jre-x.y will also undergo a replacement of the substring {@link DETECT_JAVA_VERSION}
* into something like "0.0.0.JavaSE_001_008" for Java 8 and earlier, "0.0.0.JavaSE_009" for Java 9 and newer. If you prefer
* manual control, use literal strings rather than {@link DETECT_JAVA_VERSION} in your properties file.
*
* @param properties OSGi properties for which the {@link Constants.FRAMEWORK_SYSTEMPACKAGES} property's values
* substrings {@link JRE_JAVA_SPECIFICATION_VERSION} will be replaced by the value for the key
* "jre-xxx" (where xxx is the JRE version).
* If no {@link Constants.FRAMEWORK_SYSTEMPACKAGES} property exists, this is a no-op.
*/
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)
if (properties == null) {
throw new IllegalArgumentException("Cannot replace system packages using a null properties reference");
}
//${jre-${java.specification.version}}
String systemPackages = (String) properties.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
if (systemPackages != null && !systemPackages.isEmpty()) {
LOG.debug("OSGi System Packages (before replacement): [{}]", systemPackages);
final String systemJavaVersion = System.getProperty("java.version");
if (systemJavaVersion != null && !systemJavaVersion.isEmpty()) {
final String jreJavaSpecificationVersion = "jre-" + OsgiUtil.generateJavaVersionForSystemPackages(systemJavaVersion);
LOG.debug(" System java.version: [{}], generated Java Specification Version: [{}]", systemJavaVersion, jreJavaSpecificationVersion);
String jreJavaSpecificationVersionSubstitution = (String) properties.get(jreJavaSpecificationVersion);
if (jreJavaSpecificationVersionSubstitution != null && !jreJavaSpecificationVersionSubstitution.isEmpty()) {
//${detect.java.version}
jreJavaSpecificationVersionSubstitution = jreJavaSpecificationVersionSubstitution.replace(DETECT_JAVA_VERSION, OsgiUtil.generateJava_SE_SystemPackageVersionString(systemJavaVersion)).trim();
systemPackages = systemPackages.replace(JRE_JAVA_SPECIFICATION_VERSION, jreJavaSpecificationVersionSubstitution);
properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
} else {
LOG.warn("Properties property [{}] is null or empty. Unable to replace JRE system packages. JRE system packages will be cleared", jreJavaSpecificationVersion);
systemPackages = systemPackages.replace(JRE_JAVA_SPECIFICATION_VERSION, "");
properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
}
} else {
LOG.warn("System property [{}] is null or empty. Unable to replace JRE system packages", "java.version");
}
LOG.debug("OSGi System Packages (after replacement): [{}]", systemPackages);
} else {
LOG.warn("Unable to replace JRE system packages. Properties required key [{}] is missing or empty",
Constants.FRAMEWORK_SYSTEMPACKAGES);
}
}
/**
* Find sub-packages of the packages defined in the property file and export them
*
* @param strutsConfigProps Struts-OSGi configuration properties containing the {@link SCANNING_PACKAGE_INCLUDES} property
* containing comma-separated top-level package values.
* @param configProps OSGi configuration properties for which the {@link Constants.FRAMEWORK_SYSTEMPACKAGES} property's
* value will have the sub-packages of strutsConfigProps appended to it.
* If no {@link Constants.FRAMEWORK_SYSTEMPACKAGES} property exists, and the exported packages is non-empty,
* then one is created that will contain the sub-packages of strutsConfigProps.
*/
protected void addExportedPackages(Properties strutsConfigProps, Properties configProps) {
if (strutsConfigProps == null) {
throw new IllegalArgumentException("Cannot add exported packages using a null struts config properties reference");
}
LOG.debug(" scanning.package.includes lookup returns: [{}]", (String) strutsConfigProps.get(SCANNING_PACKAGE_INCLUDES));
String[] rootPackages = StringUtils.split((String) strutsConfigProps.get(SCANNING_PACKAGE_INCLUDES), ",");
ResourceFinder finder = new ResourceFinder(StringUtils.EMPTY);
List<String> exportedPackages = new ArrayList<>();
if (rootPackages == null || rootPackages.length == 0) {
LOG.warn("Struts config roperties required key [{}] is missing or empty. No exported packages are available to add", SCANNING_PACKAGE_INCLUDES);
return;
}
//build a list of subpackages
for (String rootPackage : rootPackages) {
LOG.debug(" Looking for root package: [{}] ", rootPackage);
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(), ".", "/"));
LOG.debug(" Trimmed package map for: [{}] has size: [{}]", rootPackage.trim(), subpackagesMap.size());
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);
LOG.debug(" Version was null. Retrieved version: [{}] for [{}]", version, (url != null ? url.toString() : null) );
}
if (packages != null) {
LOG.debug(" Subpackages size: [{}]", packages.size());
for (String subpackage : packages) {
LOG.trace(" Adding subppackage: [{}; version=\"{}\"]", subpackage, version);
exportedPackages.add(subpackage + "; version=\"" + version + "\"");
}
} else {
LOG.debug(" Subpackages is null");
}
}
} catch (IOException e) {
LOG.error("Unable to find subpackages of [{}]", rootPackage, e);
}
}
//make a string with the exported packages and add it to the system properties
if (!exportedPackages.isEmpty() && configProps != null) {
String systemPackages = (String) configProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
if (systemPackages == null || systemPackages.isEmpty()) {
LOG.warn("Config properties required key [{}] is missing or empty. Only the exported packages will be present",
Constants.FRAMEWORK_SYSTEMPACKAGES);
systemPackages = StringUtils.join(exportedPackages, ",");
} else {
systemPackages = StringUtils.removeEnd(systemPackages, ",") + "," + StringUtils.join(exportedPackages, ",");
}
configProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
LOG.debug("Adding exported framework packages: [{}]", systemPackages);
} else {
LOG.warn("NOT adding any exported framework packages. No exported packages or config props is null");
}
}
/**
* @param url URL for package
* @return 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())) {
JarFile jarFile = null;
try {
ActionContext actionContext = null;
FileManagerFactory fileManagerFactory = null;
FileManager fileManager = null;
actionContext = ServletActionContext.getContext();
if (actionContext == null) {
LOG.warn("ActionContext is null. Cannot load FileManagerFactory from it");
}
if (actionContext != null) {
fileManagerFactory = actionContext.getInstance(FileManagerFactory.class);
}
if (fileManagerFactory == null) {
LOG.warn("FileManagerFactory is null in ActionContext. Cannot load FileManager from it");
} else {
fileManager = fileManagerFactory.getFileManager();
}
if (fileManager == null) {
if (fileManagerFactory != null) {
LOG.debug("FileManager is null in FileManagerFactory. Using a DefaultFileManager");
} else {
LOG.debug("Using a DefaultFileManager");
}
fileManager = new DefaultFileManager();
}
jarFile = new JarFile(new File(fileManager.normalizeToFileProtocol(url).toURI()));
Manifest manifest = jarFile.getManifest();
String jarFileName = jarFile.getName();
if (jarFileName != null) {
// Strip extraneous file path elements to limit the string to the JAR name itself, if possible.
int lastExclamationIndex = jarFileName.lastIndexOf("!");
if (lastExclamationIndex != -1) {
jarFileName = jarFileName.substring(0, lastExclamationIndex);
}
if (jarFileName.toLowerCase().endsWith(".jar")) {
int lastPathSeparatorIndex = jarFileName.lastIndexOf(File.separator);
if (lastPathSeparatorIndex != -1) {
jarFileName = jarFileName.substring(lastPathSeparatorIndex + 1);
}
}
}
if (manifest != null) {
String version = manifest.getMainAttributes().getValue("Bundle-Version");
if (StringUtils.isNotBlank(version)) {
LOG.debug("Attempting to get bundle version for [{}] via its JAR manifest", url.toExternalForm());
return getVersionFromString(version);
} else {
//try to get the version from the file name
LOG.debug("Attempting to get bundle version for [{}] via its filename [{}]", url.toExternalForm(), jarFileName);
return getVersionFromString(jarFileName);
}
} else {
//try to get the version from the file name
LOG.debug("Attempting to get bundle version for [{}] via its filename [{}]", url.toExternalForm(), jarFileName);
return getVersionFromString(jarFileName);
}
} catch (Exception e) {
LOG.error("Unable to extract version from [{}], defaulting to '1.0.0'", url.toExternalForm());
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (Exception ex) {}
}
}
}
return "1.0.0";
}
/**
* @param str string for extract version
* @return Extracts numbers followed by "." or "-" from the string and joins them with "."
*/
protected String getVersionFromString(String str) {
final String trimmedString = (str != null ? str.trim() : str);
Matcher matcher = VERSION_PATTERN.matcher(trimmedString);
List<String> parts = new ArrayList<>();
while (matcher.find()) {
parts.add(matcher.group(1));
}
//default
if (parts.isEmpty()) {
return "1.0.0";
}
while (parts.size() < 3) {
parts.add("0");
}
while (parts.size() > 3) {
parts.remove(0); // Assume a bad match with extra digits picked up from earlier in the path. Only keep last three parts (x.y.z).
}
return StringUtils.join(parts, ".");
}
protected Properties getProperties(String fileName) {
ResourceFinder finder = new ResourceFinder(StringUtils.EMPTY);
try {
return finder.findProperties(fileName);
} catch (IOException e) {
LOG.error("Unable to read the property file [{}]", fileName, e);
if (searchForPropertiesFilesInRelativePath()) {
try {
LOG.warn("Relative path search is enabled. Retry attempt to read the property file [{}] from the relative path", fileName);
return findPropertiesFileInRelativePath(fileName);
} catch (IOException ioex) {
LOG.error("Unable to read the property file [{}] using the relative path", fileName, ioex);
}
}
return new Properties();
}
}
/**
* Check ServletContext initialization parameter "struts.osgi.searchForPropertiesFilesInRelativePath".
*
* @return true if "struts.osgi.searchForPropertiesFilesInRelativePath" is "true" in ServletContext initialization parameters, false otherwise.
*/
protected boolean searchForPropertiesFilesInRelativePath() {
if (this.servletContext != null) {
final String searchForPropertiesFilesInRelativePath = getServletContextParam("struts.osgi.searchForPropertiesFilesInRelativePath", "false");
return Boolean.parseBoolean(searchForPropertiesFilesInRelativePath);
}
return false;
}
/**
* Attempt to read a properties file from the relative path of the current classloader.
* Intended as an alternate configuration fallback for special scenarios where the default lookup
* is not functional (such as for unit tests).
*
* @param fileName the filename (relative path) of the properties file.
* @return a Properties bundle loaded from the provided fileName.
* @throws IOException if the properties file does not exist or cannot be loaded.
*/
protected Properties findPropertiesFileInRelativePath(String fileName) throws IOException {
if (fileName == null || fileName.toLowerCase().endsWith(".class") || fileName.toLowerCase().endsWith(".jar")) {
throw new IllegalArgumentException("Provided file name cannot be null, nor should it be a class or jar file");
}
final ClassLoaderInterface classLoaderInterface = new ClassLoaderInterfaceDelegate(Thread.currentThread().getContextClassLoader());
final URL fileUrl = classLoaderInterface.getResource(fileName);
try (InputStream reader = new BufferedInputStream(fileUrl.openStream())) {
Properties properties = new Properties();
properties.load(reader);
return properties;
}
}
}