Java tutorial
/* * (C) Copyright 2006-2016 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: * Bogdan Stefanescu * Florent Guillaume * Julien Carsique */ package org.nuxeo.runtime.osgi; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.common.codec.CryptoProperties; import org.nuxeo.common.utils.FileUtils; import org.nuxeo.common.utils.TextTemplate; import org.nuxeo.runtime.AbstractRuntimeService; import org.nuxeo.runtime.RuntimeServiceException; import org.nuxeo.runtime.Version; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.api.ServicePassivator; import org.nuxeo.runtime.model.ComponentName; import org.nuxeo.runtime.model.RegistrationInfo; import org.nuxeo.runtime.model.RuntimeContext; import org.nuxeo.runtime.model.impl.ComponentPersistence; import org.nuxeo.runtime.model.impl.RegistrationInfoImpl; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; /** * The default implementation of NXRuntime over an OSGi compatible environment. * * @author Bogdan Stefanescu * @author Florent Guillaume */ public class OSGiRuntimeService extends AbstractRuntimeService implements FrameworkListener { public static final ComponentName FRAMEWORK_STARTED_COMP = new ComponentName("org.nuxeo.runtime.started"); /** Can be used to change the runtime home directory */ public static final String PROP_HOME_DIR = "org.nuxeo.runtime.home"; /** The OSGi application install directory. */ public static final String PROP_INSTALL_DIR = "INSTALL_DIR"; /** The OSGi application config directory. */ public static final String PROP_CONFIG_DIR = "CONFIG_DIR"; /** The host adapter. */ public static final String PROP_HOST_ADAPTER = "HOST_ADAPTER"; public static final String PROP_NUXEO_BIND_ADDRESS = "nuxeo.bind.address"; public static final String NAME = "OSGi NXRuntime"; public static final Version VERSION = Version.parseString("1.4.0"); private static final Log log = LogFactory.getLog(OSGiRuntimeService.class); private final BundleContext bundleContext; private final Map<String, RuntimeContext> contexts; private boolean appStarted = false; /** * OSGi doesn't provide a method to lookup bundles by symbolic name. This table is used to map symbolic names to * bundles. This map is not handling bundle versions. */ final Map<String, Bundle> bundles; final ComponentPersistence persistence; public OSGiRuntimeService(BundleContext context) { this(new OSGiRuntimeContext(context.getBundle()), context); } public OSGiRuntimeService(OSGiRuntimeContext runtimeContext, BundleContext context) { super(runtimeContext); bundleContext = context; bundles = new ConcurrentHashMap<>(); contexts = new ConcurrentHashMap<>(); String bindAddress = context.getProperty(PROP_NUXEO_BIND_ADDRESS); if (bindAddress != null) { properties.put(PROP_NUXEO_BIND_ADDRESS, bindAddress); } String homeDir = getProperty(PROP_HOME_DIR); log.debug("Home directory: " + homeDir); if (homeDir != null) { workingDir = new File(homeDir); } else { workingDir = bundleContext.getDataFile("/"); } // environment may not be set by some bootstrappers (like tests) - we create it now if not yet created Environment env = Environment.getDefault(); if (env == null) { env = new Environment(workingDir); Environment.setDefault(env); env.setServerHome(workingDir); env.init(); } workingDir.mkdirs(); persistence = new ComponentPersistence(this); log.debug("Working directory: " + workingDir); } @Override public String getName() { return NAME; } @Override public Version getVersion() { return VERSION; } public BundleContext getBundleContext() { return bundleContext; } @Override public Bundle getBundle(String symbolicName) { return bundles.get(symbolicName); } public Map<String, Bundle> getBundlesMap() { return bundles; } public ComponentPersistence getComponentPersistence() { return persistence; } public synchronized RuntimeContext createContext(Bundle bundle) { RuntimeContext ctx = contexts.get(bundle.getSymbolicName()); if (ctx == null) { // workaround to handle fragment bundles ctx = new OSGiRuntimeContext(bundle); contexts.put(bundle.getSymbolicName(), ctx); loadComponents(bundle, ctx); } return ctx; } public synchronized void destroyContext(Bundle bundle) { RuntimeContext ctx = contexts.remove(bundle.getSymbolicName()); if (ctx != null) { ctx.destroy(); } } public synchronized RuntimeContext getContext(Bundle bundle) { return contexts.get(bundle.getSymbolicName()); } public synchronized RuntimeContext getContext(String symbolicName) { return contexts.get(symbolicName); } @Override protected void doStart() { bundleContext.addFrameworkListener(this); try { loadConfig(); } catch (IOException e) { throw new RuntimeServiceException(e); } // load configuration if any loadComponents(bundleContext.getBundle(), context); } @Override protected void doStop() { bundleContext.removeFrameworkListener(this); try { super.doStop(); } finally { context.destroy(); } } protected void loadComponents(Bundle bundle, RuntimeContext ctx) { String list = getComponentsList(bundle); String name = bundle.getSymbolicName(); log.debug("Bundle: " + name + " components: " + list); if (list == null) { return; } StringTokenizer tok = new StringTokenizer(list, ", \t\n\r\f"); while (tok.hasMoreTokens()) { String path = tok.nextToken(); URL url = bundle.getEntry(path); log.debug("Loading component for: " + name + " path: " + path + " url: " + url); if (url != null) { try { ctx.deploy(url); } catch (IOException e) { // just log error to know where is the cause of the exception log.error("Error deploying resource: " + url); Framework.handleDevError(e); throw new RuntimeServiceException("Cannot deploy: " + url, e); } } else { String message = "Unknown component '" + path + "' referenced by bundle '" + name + "'"; log.error(message + ". Check the MANIFEST.MF"); Framework.handleDevError(null); warnings.add(message); } } } public static String getComponentsList(Bundle bundle) { return (String) bundle.getHeaders().get("Nuxeo-Component"); } protected boolean loadConfigurationFromProvider() throws IOException { // TODO use a OSGi service for this. Iterable<URL> provider = Environment.getDefault().getConfigurationProvider(); if (provider == null) { return false; } Iterator<URL> it = provider.iterator(); ArrayList<URL> props = new ArrayList<>(); ArrayList<URL> xmls = new ArrayList<>(); while (it.hasNext()) { URL url = it.next(); String path = url.getPath(); if (path.endsWith("-config.xml")) { xmls.add(url); } else if (path.endsWith(".properties")) { props.add(url); } } Comparator<URL> comp = new Comparator<URL>() { @Override public int compare(URL o1, URL o2) { return o1.getPath().compareTo(o2.getPath()); } }; Collections.sort(xmls, comp); for (URL url : props) { loadProperties(url); } for (URL url : xmls) { context.deploy(url); } return true; } protected void loadConfig() throws IOException { Environment env = Environment.getDefault(); if (env != null) { log.debug("Configuration: host application: " + env.getHostApplicationName()); } else { log.warn("Configuration: no host application"); return; } File blacklistFile = new File(env.getConfig(), "blacklist"); if (blacklistFile.isFile()) { List<String> lines = FileUtils.readLines(blacklistFile); Set<String> blacklist = new HashSet<>(); for (String line : lines) { line = line.trim(); if (line.length() > 0) { blacklist.add(line); } } manager.setBlacklist(new HashSet<>(lines)); } if (loadConfigurationFromProvider()) { return; } String configDir = bundleContext.getProperty(PROP_CONFIG_DIR); if (configDir != null && configDir.contains(":/")) { // an url of a config file log.debug("Configuration: " + configDir); URL url = new URL(configDir); log.debug("Configuration: loading properties url: " + configDir); loadProperties(url); return; } // TODO: in JBoss there is a deployer that will deploy nuxeo // configuration files .. boolean isNotJBoss4 = !isJBoss4(env); File dir = env.getConfig(); // File dir = new File(configDir); String[] names = dir.list(); if (names != null) { Arrays.sort(names, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); printDeploymentOrderInfo(names); for (String name : names) { if (name.endsWith("-config.xml") || name.endsWith("-bundle.xml")) { // TODO because of some dep bugs (regarding the deployment of demo-ds.xml), we cannot let the // runtime deploy config dir at beginning... // until fixing this we deploy config dir from NuxeoDeployer if (isNotJBoss4) { File file = new File(dir, name); log.debug("Configuration: deploy config component: " + name); try { context.deploy(file.toURI().toURL()); } catch (IOException e) { throw new IllegalArgumentException("Cannot load config from " + file, e); } } } else if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { File file = new File(dir, name); log.debug("Configuration: loading properties: " + name); loadProperties(file); } else { log.debug("Configuration: ignoring: " + name); } } } else if (dir.isFile()) { // a file - load it log.debug("Configuration: loading properties: " + dir); loadProperties(dir); } else { log.debug("Configuration: no configuration file found"); } loadDefaultConfig(); } protected static void printDeploymentOrderInfo(String[] fileNames) { if (log.isDebugEnabled()) { StringBuilder buf = new StringBuilder(); for (String fileName : fileNames) { buf.append("\n\t" + fileName); } log.debug("Deployment order of configuration files: " + buf.toString()); } } @Override public void reloadProperties() throws IOException { File dir = Environment.getDefault().getConfig(); String[] names = dir.list(); if (names != null) { Arrays.sort(names, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); CryptoProperties props = new CryptoProperties(System.getProperties()); for (String name : names) { if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { FileInputStream in = new FileInputStream(new File(dir, name)); try { props.load(in); } finally { in.close(); } } } // replace the current runtime properties properties = props; } } /** * Loads default properties. * <p> * Used for backward compatibility when adding new mandatory properties * </p> */ protected void loadDefaultConfig() { String varName = "org.nuxeo.ecm.contextPath"; if (Framework.getProperty(varName) == null) { properties.setProperty(varName, "/nuxeo"); } } public void loadProperties(File file) throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(file)); try { loadProperties(in); } finally { in.close(); } } public void loadProperties(URL url) throws IOException { InputStream in = url.openStream(); try { loadProperties(in); } finally { if (in != null) { in.close(); } } } public void loadProperties(InputStream in) throws IOException { properties.load(in); } /** * Overrides the default method to be able to include OSGi properties. */ @Override public String getProperty(String name, String defValue) { String value = properties.getProperty(name); if (value == null) { value = bundleContext.getProperty(name); if (value == null) { return defValue == null ? null : expandVars(defValue); } } if (("${" + name + "}").equals(value)) { // avoid loop, don't expand return value; } return expandVars(value); } /** * Overrides the default method to be able to include OSGi properties. */ @Override public String expandVars(String expression) { return new TextTemplate(getProperties()) { @Override public String getVariable(String name) { String value = super.getVariable(name); if (value == null) { value = bundleContext.getProperty(name); } return value; } }.processText(expression); } protected void notifyComponentsOnStarted() { List<RegistrationInfo> ris = new ArrayList<>(manager.getRegistrations()); Collections.sort(ris, new RIApplicationStartedComparator()); for (RegistrationInfo ri : ris) { try { ri.notifyApplicationStarted(); } catch (RuntimeException e) { log.error("Failed to notify component '" + ri.getName() + "' on application started", e); } } } protected static class RIApplicationStartedComparator implements Comparator<RegistrationInfo> { @Override public int compare(RegistrationInfo r1, RegistrationInfo r2) { int cmp = Integer.compare(r1.getApplicationStartedOrder(), r2.getApplicationStartedOrder()); if (cmp == 0) { // fallback on name order, to be deterministic cmp = r1.getName().getName().compareTo(r2.getName().getName()); } return cmp; } } public void fireApplicationStarted() { synchronized (this) { if (appStarted) { return; } appStarted = true; } try { persistence.loadPersistedComponents(); } catch (RuntimeException | IOException e) { log.error("Failed to load persisted components", e); } // deploy a fake component that is marking the end of startup // XML components that needs to be deployed at the end need to put a // requirement // on this marker component deployFrameworkStartedComponent(); notifyComponentsOnStarted(); // print the startup message printStatusMessage(); } /* --------------- FrameworkListener API ------------------ */ @Override public void frameworkEvent(FrameworkEvent event) { if (event.getType() != FrameworkEvent.STARTED) { return; } ServicePassivator.proceed(Duration.ofSeconds(0), Duration.ofSeconds(0), false, this::fireApplicationStarted); } private void printStatusMessage() { StringBuilder msg = new StringBuilder(); msg.append("Nuxeo Platform Started\n"); if (getStatusMessage(msg)) { log.info(msg); } else { log.error(msg); } } protected void deployFrameworkStartedComponent() { RegistrationInfoImpl ri = new RegistrationInfoImpl(FRAMEWORK_STARTED_COMP); ri.setContext(context); // this will register any pending components that waits for the // framework to be started manager.register(ri); } public Bundle findHostBundle(Bundle bundle) { String hostId = (String) bundle.getHeaders().get(Constants.FRAGMENT_HOST); log.debug("Looking for host bundle: " + bundle.getSymbolicName() + " host id: " + hostId); if (hostId != null) { int p = hostId.indexOf(';'); if (p > -1) { // remove version or other extra information if any hostId = hostId.substring(0, p); } RuntimeContext ctx = contexts.get(hostId); if (ctx != null) { log.debug("Context was found for host id: " + hostId); return ctx.getBundle(); } else { log.warn("No context found for host id: " + hostId); } } return null; } protected File getEclipseBundleFileUsingReflection(Bundle bundle) { try { Object proxy = bundle.getClass().getMethod("getLoaderProxy").invoke(bundle); Object loader = proxy.getClass().getMethod("getBundleLoader").invoke(proxy); URL root = (URL) loader.getClass().getMethod("findResource", String.class).invoke(loader, "/"); Field field = root.getClass().getDeclaredField("handler"); field.setAccessible(true); Object handler = field.get(root); Field entryField = handler.getClass().getSuperclass().getDeclaredField("bundleEntry"); entryField.setAccessible(true); Object entry = entryField.get(handler); Field fileField = entry.getClass().getDeclaredField("file"); fileField.setAccessible(true); return (File) fileField.get(entry); } catch (ReflectiveOperationException e) { log.error("Cannot access to eclipse bundle system files of " + bundle.getSymbolicName()); return null; } } @Override public File getBundleFile(Bundle bundle) { File file; String location = bundle.getLocation(); String vendor = Framework.getProperty(Constants.FRAMEWORK_VENDOR); String name = bundle.getSymbolicName(); if ("Eclipse".equals(vendor)) { // equinox framework log.debug("getBundleFile (Eclipse): " + name + "->" + location); return getEclipseBundleFileUsingReflection(bundle); } else if (location.startsWith("file:")) { // nuxeo osgi adapter try { file = FileUtils.urlToFile(location); } catch (MalformedURLException e) { log.error("getBundleFile: Unable to create " + " for bundle: " + name + " as URI: " + location); return null; } } else { // may be a file path - this happens when using // JarFileBundle (for ex. in nxshell) file = new File(location); } if (file != null && file.exists()) { log.debug("getBundleFile: " + name + " bound to file: " + file); return file; } else { log.debug("getBundleFile: " + name + " cannot bind to nonexistent file: " + file); return null; } } public static final boolean isJBoss4(Environment env) { if (env == null) { return false; } String hn = env.getHostApplicationName(); String hv = env.getHostApplicationVersion(); if (hn == null || hv == null) { return false; } return "JBoss".equals(hn) && hv.startsWith("4"); } }