/*
* (C) Copyright 2006-2017 Nuxeo (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:
* Nuxeo - initial API and implementation
*/
package org.nuxeo.runtime.api;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.util.List;
import java.util.Properties;
import java.util.function.Supplier;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.io.FileDeleteStrategy;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.Environment;
import org.nuxeo.common.collections.ListenerList;
import org.nuxeo.runtime.RuntimeService;
import org.nuxeo.runtime.RuntimeServiceEvent;
import org.nuxeo.runtime.RuntimeServiceException;
import org.nuxeo.runtime.RuntimeServiceListener;
import org.nuxeo.runtime.api.login.LoginAs;
import org.nuxeo.runtime.api.login.LoginService;
import org.nuxeo.runtime.trackers.files.FileEvent;
import org.nuxeo.runtime.trackers.files.FileEventTracker;
/**
* This class is the main entry point to a Nuxeo runtime application.
* <p>
* It offers an easy way to create new sessions, to access system services and other resources.
* <p>
* There are two type of services:
* <ul>
* <li>Global Services - these services are uniquely defined by a service class, and there is an unique instance of the
* service in the system per class.
* <li>Local Services - these services are defined by a class and an URI. This type of service allows multiple service
* instances for the same class of services. Each instance is uniquely defined in the system by an URI.
* </ul>
*
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public final class Framework {
private static final Log log = LogFactory.getLog(Framework.class);
private static Boolean testModeSet;
/**
* Global dev property
*
* @since 5.6
* @see #isDevModeSet()
*/
public static final String NUXEO_DEV_SYSTEM_PROP = "org.nuxeo.dev";
/**
* Global testing property
*
* @since 5.6
* @see #isTestModeSet()
*/
public static final String NUXEO_TESTING_SYSTEM_PROP = "org.nuxeo.runtime.testing";
/**
* Property to control strict runtime mode
*
* @since 5.6
* @see #handleDevError(Throwable)
* @deprecated since 9.1 This property is not documented and doesn't work.
*/
@Deprecated
public static final String NUXEO_STRICT_RUNTIME_SYSTEM_PROP = "org.nuxeo.runtime.strict";
/**
* The runtime instance.
*/
private static RuntimeService runtime;
private static final ListenerList listeners = new ListenerList();
/**
* A class loader used to share resources between all bundles.
* <p>
* This is useful to put resources outside any bundle (in a directory on the file system) and then refer them from
* XML contributions.
* <p>
* The resource directory used by this loader is ${nuxeo_data_dir}/resources whee ${nuxeo_data_dir} is usually
* ${nuxeo_home}/data
*/
protected static SharedResourceLoader resourceLoader;
/**
* Whether or not services should be exported as OSGI services. This is controlled by the ${ecr.osgi.services}
* property. The default is false.
*/
protected static Boolean isOSGiServiceSupported;
// Utility class.
private Framework() {
}
public static void initialize(RuntimeService runtimeService) {
if (runtime != null) {
throw new RuntimeServiceException("Nuxeo Framework was already initialized");
}
runtime = runtimeService;
reloadResourceLoader();
runtime.start();
}
public static void reloadResourceLoader() {
File rs = new File(Environment.getDefault().getData(), "resources");
rs.mkdirs();
URL url;
try {
url = rs.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeServiceException(e);
}
resourceLoader = new SharedResourceLoader(new URL[] { url }, Framework.class.getClassLoader());
}
/**
* Reload the resources loader, keeping URLs already tracked, and adding possibility to add or remove some URLs.
* <p>
* Useful for hot reload of jars.
*
* @since 5.6
*/
public static void reloadResourceLoader(List<URL> urlsToAdd, List<URL> urlsToRemove) {
File rs = new File(Environment.getDefault().getData(), "resources");
rs.mkdirs();
URL[] existing = null;
if (resourceLoader != null) {
existing = resourceLoader.getURLs();
}
// reinit
URL url;
try {
url = rs.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
resourceLoader = new SharedResourceLoader(new URL[] { url }, Framework.class.getClassLoader());
// add back existing urls unless they should be removed, and add new
// urls
if (existing != null) {
for (URL oldURL : existing) {
if (urlsToRemove == null || !urlsToRemove.contains(oldURL)) {
resourceLoader.addURL(oldURL);
}
}
}
if (urlsToAdd != null) {
for (URL newURL : urlsToAdd) {
resourceLoader.addURL(newURL);
}
}
}
public static void shutdown() throws InterruptedException {
if (runtime == null) {
throw new IllegalStateException("runtime not exist");
}
try {
runtime.stop();
} finally {
runtime = null;
}
}
/**
* Tests whether or not the runtime was initialized.
*
* @return true if the runtime was initialized, false otherwise
*/
public static synchronized boolean isInitialized() {
return runtime != null;
}
public static SharedResourceLoader getResourceLoader() {
return resourceLoader;
}
/**
* Gets the runtime service instance.
*
* @return the runtime service instance
*/
public static RuntimeService getRuntime() {
return runtime;
}
/**
* Gets a service given its class.
*/
public static <T> T getService(Class<T> serviceClass) {
ServiceProvider provider = DefaultServiceProvider.getProvider();
if (provider != null) {
return provider.getService(serviceClass);
}
checkRuntimeInitialized();
// TODO impl a runtime service provider
return runtime.getService(serviceClass);
}
/**
* Gets a service given its class.
*/
public static <T> T getLocalService(Class<T> serviceClass) {
return getService(serviceClass);
}
/**
* Lookup a registered object given its key.
*/
public static Object lookup(String key) {
return null; // TODO
}
/**
* Runs the given {@link Runnable} while logged in as a system user.
*
* @param runnable what to run
* @since 8.4
*/
public static void doPrivileged(Runnable runnable) {
try {
LoginContext loginContext = login();
try {
runnable.run();
} finally {
if (loginContext != null) { // may be null in tests
loginContext.logout();
}
}
} catch (LoginException e) {
throw new RuntimeException(e);
}
}
/**
* Calls the given {@link Supplier} while logged in as a system user and returns its result.
*
* @param supplier what to call
* @return the supplier's result
* @since 8.4
*/
public static <T> T doPrivileged(Supplier<T> supplier) {
try {
LoginContext loginContext = login();
try {
return supplier.get();
} finally {
if (loginContext != null) { // may be null in tests
loginContext.logout();
}
}
} catch (LoginException e) {
throw new RuntimeException(e);
}
}
/**
* Login in the system as the system user (a pseudo-user having all privileges).
*
* @return the login session if successful. Never returns null.
* @throws LoginException on login failure
*/
public static LoginContext login() throws LoginException {
checkRuntimeInitialized();
LoginService loginService = runtime.getService(LoginService.class);
if (loginService != null) {
return loginService.login();
}
return null;
}
/**
* Login in the system as the system user (a pseudo-user having all privileges). The given username will be used to
* identify the user id that called this method.
*
* @param username the originating user id
* @return the login session if successful. Never returns null.
* @throws LoginException on login failure
*/
public static LoginContext loginAs(String username) throws LoginException {
checkRuntimeInitialized();
LoginService loginService = runtime.getService(LoginService.class);
if (loginService != null) {
return loginService.loginAs(username);
}
return null;
}
/**
* Login in the system as the given user without checking the password.
*
* @param username the user name to login as.
* @return the login context
* @throws LoginException if any error occurs
* @since 5.4.2
*/
public static LoginContext loginAsUser(String username) throws LoginException {
return getLocalService(LoginAs.class).loginAs(username);
}
/**
* Login in the system as the given user using the given password.
*
* @param username the username to login
* @param password the password
* @return a login session if login was successful. Never returns null.
* @throws LoginException if login failed
*/
public static LoginContext login(String username, Object password) throws LoginException {
checkRuntimeInitialized();
LoginService loginService = runtime.getService(LoginService.class);
if (loginService != null) {
return loginService.login(username, password);
}
return null;
}
/**
* Login in the system using the given callback handler for login info resolution.
*
* @param cbHandler used to fetch the login info
* @return the login context
* @throws LoginException
*/
public static LoginContext login(CallbackHandler cbHandler) throws LoginException {
checkRuntimeInitialized();
LoginService loginService = runtime.getService(LoginService.class);
if (loginService != null) {
return loginService.login(cbHandler);
}
return null;
}
public static void sendEvent(RuntimeServiceEvent event) {
Object[] listenersArray = listeners.getListeners();
for (Object listener : listenersArray) {
((RuntimeServiceListener) listener).handleEvent(event);
}
}
/**
* Registers a listener to be notified about runtime events.
* <p>
* If the listener is already registered, do nothing.
*
* @param listener the listener to register
*/
public static void addListener(RuntimeServiceListener listener) {
listeners.add(listener);
}
/**
* Removes the given listener.
* <p>
* If the listener is not registered, do nothing.
*
* @param listener the listener to remove
*/
public static void removeListener(RuntimeServiceListener listener) {
listeners.remove(listener);
}
/**
* Gets the given property value if any, otherwise null.
* <p>
* The framework properties will be searched first then if any matching property is found the system properties are
* searched too.
*
* @param key the property key
* @return the property value if any or null otherwise
*/
public static String getProperty(String key) {
return getProperty(key, null);
}
/**
* Gets the given property value if any, otherwise returns the given default value.
* <p>
* The framework properties will be searched first then if any matching property is found the system properties are
* searched too.
*
* @param key the property key
* @param defValue the default value to use
* @return the property value if any otherwise the default value
*/
public static String getProperty(String key, String defValue) {
checkRuntimeInitialized();
return runtime.getProperty(key, defValue);
}
/**
* Gets all the framework properties. The system properties are not included in the returned map.
*
* @return the framework properties map. Never returns null.
*/
public static Properties getProperties() {
checkRuntimeInitialized();
return runtime.getProperties();
}
/**
* Expands any variable found in the given expression with the value of the corresponding framework property.
* <p>
* The variable format is ${property_key}.
* <p>
* System properties are also expanded.
*/
public static String expandVars(String expression) {
checkRuntimeInitialized();
return runtime.expandVars(expression);
}
public static boolean isOSGiServiceSupported() {
if (isOSGiServiceSupported == null) {
isOSGiServiceSupported = Boolean.valueOf(isBooleanPropertyTrue("ecr.osgi.services"));
}
return isOSGiServiceSupported.booleanValue();
}
/**
* Returns true if dev mode is set.
* <p>
* Activating this mode, some of the code may not behave as it would in production, to ease up debugging and working
* on developing the application.
* <p>
* For instance, it'll enable hot-reload if some packages are installed while the framework is running. It will also
* reset some caches when that happens.
*/
public static boolean isDevModeSet() {
return isBooleanPropertyTrue(NUXEO_DEV_SYSTEM_PROP);
}
/**
* Returns true if test mode is set.
* <p>
* Activating this mode, some of the code may not behave as it would in production, to ease up testing.
*/
public static boolean isTestModeSet() {
if (testModeSet == null) {
testModeSet = isBooleanPropertyTrue(NUXEO_TESTING_SYSTEM_PROP);
}
return testModeSet;
}
/**
* Returns true if given property is false when compared to a boolean value. Returns false if given property in
* unset.
* <p>
* Checks for the system properties if property is not found in the runtime properties.
*
* @since 5.8
*/
public static boolean isBooleanPropertyFalse(String propName) {
String v = getProperty(propName);
if (v == null) {
v = System.getProperty(propName);
}
if (StringUtils.isBlank(v)) {
return false;
}
return !Boolean.parseBoolean(v);
}
/**
* Returns true if given property is true when compared to a boolean value.
* <p>
* Checks for the system properties if property is not found in the runtime properties.
*
* @since 5.6
*/
public static boolean isBooleanPropertyTrue(String propName) {
String v = getProperty(propName);
if (v == null) {
v = System.getProperty(propName);
}
return Boolean.parseBoolean(v);
}
/**
* Since 5.6, this method stops the application if property {@link #NUXEO_STRICT_RUNTIME_SYSTEM_PROP} is set to
* true, and one of the following errors occurred during startup.
* <ul>
* <li>Component XML parse error.
* <li>Contribution to an unknown extension point.
* <li>Component with an unknown implementation class (the implementation entry exists in the XML descriptor but
* cannot be resolved to a class).
* <li>Uncatched exception on extension registration / unregistration (either in framework or user component code)
* <li>Uncatched exception on component activation / deactivation (either in framework or user component code)
* <li>Broken Nuxeo-Component MANIFEST entry. (i.e. the entry cannot be resolved to a resource)
* </ul>
* <p>
* Before 5.6, this method stopped the application if development mode was enabled (i.e. org.nuxeo.dev system
* property is set) but this is not the case anymore to handle a dev mode that does not stop the runtime framework
* when using hot reload.
*
* @param t the exception or null if none
* @deprecated since 9.1 DON'T USE THIS METHOD ANYMORE, its behavior is not documented. It also seems to not work.
* If you want to stop server startup add messages to {@link RuntimeService#getErrors()}.
*/
@Deprecated
public static void handleDevError(Throwable t) {
if (isBooleanPropertyTrue(NUXEO_STRICT_RUNTIME_SYSTEM_PROP)) {
System.err.println("Fatal error caught in strict " + "runtime mode => exiting.");
if (t != null) {
t.printStackTrace();
}
System.exit(1);
} else if (t != null) {
log.error(t, t);
}
}
/**
* @see FileEventTracker
* @param aFile The file to delete
* @param aMarker the marker Object
*/
public static void trackFile(File aFile, Object aMarker) {
FileEvent.onFile(Framework.class, aFile, aMarker).send();
}
/**
* Strategy is not customizable anymore.
*
* @deprecated
* @since 6.0
* @see #trackFile(File, Object)
* @see org.nuxeo.runtime.trackers.files.FileEventTracker.SafeFileDeleteStrategy
* @param file The file to delete
* @param marker the marker Object
* @param fileDeleteStrategy ignored deprecated parameter
*/
@Deprecated
public static void trackFile(File file, Object marker, FileDeleteStrategy fileDeleteStrategy) {
trackFile(file, marker);
}
/**
* @since 6.0
*/
protected static void checkRuntimeInitialized() {
if (runtime == null) {
throw new IllegalStateException("Runtime not initialized");
}
}
/**
* Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir}
* ), using the given prefix and suffix to generate its name.
* <p>
* Invoking this method is equivalent to invoking
* <code>{@link File#createTempFile(java.lang.String, java.lang.String, java.io.File)
* File.createTempFile(prefix, suffix, Environment.getDefault().getTemp())}</code>.
* <p>
* The {@link #createTempFilePath(String, String, FileAttribute...)} method provides an alternative method to create
* an empty file in the framework temporary-file directory. Files created by that method may have more restrictive
* access permissions to files created by this method and so may be more suited to security-sensitive applications.
*
* @param prefix The prefix string to be used in generating the file's name; must be at least three characters long
* @param suffix The suffix string to be used in generating the file's name; may be <code>null</code>, in which case
* the suffix <code>".tmp"</code> will be used
* @return An abstract pathname denoting a newly-created empty file
* @throws IllegalArgumentException If the <code>prefix</code> argument contains fewer than three characters
* @throws IOException If a file could not be created
* @throws SecurityException If a security manager exists and its <code>
* {@link java.lang.SecurityManager#checkWrite(java.lang.String)}</code> method does not allow a file to
* be created
* @since 8.1
* @see File#createTempFile(String, String, File)
* @see Environment#getTemp()
* @see #createTempFilePath(String, String, FileAttribute...)
* @see #createTempDirectory(String, FileAttribute...)
*/
public static File createTempFile(String prefix, String suffix) throws IOException {
try {
return File.createTempFile(prefix, suffix, getTempDir());
} catch (IOException e) {
throw new IOException("Could not create temp file in " + getTempDir(), e);
}
}
/**
* @return the Nuxeo temp dir returned by {@link Environment#getTemp()}. If the Environment fails to initialize,
* then returns the File denoted by {@code "nuxeo.tmp.dir"} System property, or {@code "java.io.tmpdir"}.
* @since 8.1
*/
private static File getTempDir() {
Environment env = Environment.getDefault();
File temp = env != null ? env.getTemp()
: new File(System.getProperty("nuxeo.tmp.dir", System.getProperty("java.io.tmpdir")));
temp.mkdirs();
return temp;
}
/**
* Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir}
* ), using the given prefix and suffix to generate its name. The resulting {@code Path} is associated with the
* default {@code FileSystem}.
* <p>
* Invoking this method is equivalent to invoking
* {@link Files#createTempFile(Path, String, String, FileAttribute...)
* Files.createTempFile(Environment.getDefault().getTemp().toPath(), prefix, suffix, attrs)}.
*
* @param prefix the prefix string to be used in generating the file's name; may be {@code null}
* @param suffix the suffix string to be used in generating the file's name; may be {@code null}, in which case "
* {@code .tmp}" is used
* @param attrs an optional list of file attributes to set atomically when creating the file
* @return the path to the newly created file that did not exist before this method was invoked
* @throws IllegalArgumentException if the prefix or suffix parameters cannot be used to generate a candidate file
* name
* @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when
* creating the directory
* @throws IOException if an I/O error occurs or the temporary-file directory does not exist
* @throws SecurityException In the case of the default provider, and a security manager is installed, the
* {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access to the
* file.
* @since 8.1
* @see Files#createTempFile(Path, String, String, FileAttribute...)
* @see Environment#getTemp()
* @see #createTempFile(String, String)
*/
public static Path createTempFilePath(String prefix, String suffix, FileAttribute<?>... attrs) throws IOException {
try {
return Files.createTempFile(getTempDir().toPath(), prefix, suffix, attrs);
} catch (IOException e) {
throw new IOException("Could not create temp file in " + getTempDir(), e);
}
}
/**
* Creates a new directory in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs
* {@code java.io.tmpdir}), using the given prefix to generate its name. The resulting {@code Path} is associated
* with the default {@code FileSystem}.
* <p>
* Invoking this method is equivalent to invoking {@link Files#createTempDirectory(Path, String, FileAttribute...)
* Files.createTempDirectory(Environment.getDefault().getTemp().toPath(), prefix, suffix, attrs)}.
*
* @param prefix the prefix string to be used in generating the directory's name; may be {@code null}
* @param attrs an optional list of file attributes to set atomically when creating the directory
* @return the path to the newly created directory that did not exist before this method was invoked
* @throws IllegalArgumentException if the prefix cannot be used to generate a candidate directory name
* @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when
* creating the directory
* @throws IOException if an I/O error occurs or the temporary-file directory does not exist
* @throws SecurityException In the case of the default provider, and a security manager is installed, the
* {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access when
* creating the directory.
* @since 8.1
* @see Files#createTempDirectory(Path, String, FileAttribute...)
* @see Environment#getTemp()
* @see #createTempFile(String, String)
*/
public static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) throws IOException {
try {
return Files.createTempDirectory(getTempDir().toPath(), prefix, attrs);
} catch (IOException e) {
throw new IOException("Could not create temp directory in " + getTempDir(), e);
}
}
}