/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.core;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.helpers.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.DatabaseService;
import org.structr.api.config.Setting;
import org.structr.api.config.Settings;
import org.structr.api.service.Command;
import org.structr.api.service.InitializationCallback;
import org.structr.api.service.RunnableService;
import org.structr.api.service.Service;
import org.structr.api.service.StructrServices;
import org.structr.common.Permission;
import org.structr.common.Permissions;
import org.structr.common.SecurityContext;
import org.structr.core.app.StructrApp;
import org.structr.core.graph.NodeService;
import org.structr.schema.ConfigurationProvider;
//~--- classes ----------------------------------------------------------------
/**
* Provides access to the service layer in structr.
*
* Use the command method to obtain an instance of the desired command.
*
*
*/
public class Services implements StructrServices {
private static final Logger logger = LoggerFactory.getLogger(StructrApp.class.getName());
// Configuration constants
public static final String LOG_SERVICE_INTERVAL = "structr.logging.interval";
public static final String LOG_SERVICE_THRESHOLD = "structr.logging.threshold";
public static final String WS_INDENTATION = "ws.indentation";
// singleton instance
private static int globalSessionTimeout = -1;
private static Services singletonInstance = null;
// non-static members
private final List<InitializationCallback> callbacks = new LinkedList<>();
private final Set<Permission> permissionsForOwnerlessNodes = new LinkedHashSet<>();
private final Map<String, Object> attributes = new ConcurrentHashMap<>(10, 0.9f, 8);
private final Map<Class, Service> serviceCache = new ConcurrentHashMap<>(10, 0.9f, 8);
private final Set<Class> registeredServiceClasses = new LinkedHashSet<>();
private final Set<String> configuredServiceClasses = new LinkedHashSet<>();
private ConfigurationProvider configuration = null;
private boolean initializationDone = false;
private boolean overridingSchemaTypesAllowed = true;
private boolean shutdownDone = false;
private String configuredServiceNames = null;
private String configurationClass = null;
private Services() { }
public static Services getInstance() {
if (singletonInstance == null) {
singletonInstance = new Services();
singletonInstance.initialize();
}
return singletonInstance;
}
/**
* Creates and returns a command of the given <code>type</code>. If a command is
* found, the corresponding service will be discovered and activated.
*
* @param <T>
* @param securityContext
* @param commandType the runtime type of the desired command
* @return the command
*/
public <T extends Command> T command(final SecurityContext securityContext, final Class<T> commandType) {
try {
final T command = commandType.newInstance();
final Class serviceClass = command.getServiceClass();
// inject security context first
command.setArgument("securityContext", securityContext);
if ((serviceClass != null) && configuredServiceClasses.contains(serviceClass.getSimpleName())) {
// search for already running service..
Service service = serviceCache.get(serviceClass);
if (service != null) {
logger.debug("Initializing command ", commandType.getName());
service.injectArguments(command);
}
}
command.initialized();
return command;
} catch (Throwable t) {
logger.error("Exception while creating command {}", commandType.getName());
}
return null;
}
private void initialize() {
// read structr.conf
final String configFileName = "structr.conf";
final File configFile = new File(configFileName);
if (Settings.Testing.getValue()) {
// simulate fully configured system
logger.info("Starting Structr for testing (structr.conf will be ignored)..");
} else if (configFile.exists()) {
logger.info("Reading {}..", configFileName);
Settings.loadConfiguration(configFileName);
} else {
// write structr.conf with random superadmin password
logger.info("Writing {}..", configFileName);
try {
Settings.storeConfiguration(configFileName);
} catch (IOException ioex) {
logger.warn("Unable to write {}: {}", configFileName, ioex.getMessage());
}
}
doInitialize();
}
private void doInitialize() {
configurationClass = Settings.Configuration.getValue();
configuredServiceNames = Settings.Services.getValue();
// create set of configured services
configuredServiceClasses.addAll(Arrays.asList(configuredServiceNames.split("[ ,]+")));
// if configuration is not yet established, instantiate it
// this is the place where the service classes get the
// opportunity to modify the default configuration
getConfigurationProvider();
logger.info("Starting services");
// initialize other services
for (final String serviceClassName : configuredServiceClasses) {
Class serviceClass = getServiceClassForName(serviceClassName);
if (serviceClass != null) {
startService(serviceClass);
}
}
logger.info("{} service(s) processed", serviceCache.size());
logger.info("Registering shutdown hook.");
// register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
shutdown();
}
});
// read permissions for ownerless nodes
final String configForOwnerlessNodes = Settings.OwnerlessNodes.getValue();
if (StringUtils.isNotBlank(configForOwnerlessNodes)) {
for (final String permission : configForOwnerlessNodes.split("[, ]+")) {
final String trimmed = permission.trim();
if (StringUtils.isNotBlank(trimmed)) {
final Permission val = Permissions.valueOf(trimmed);
if (val != null) {
permissionsForOwnerlessNodes.add(val);
} else {
logger.warn("Invalid permisson {}, ignoring.", trimmed);
}
}
}
} else {
// default
permissionsForOwnerlessNodes.add(Permission.read);
}
// only run initialization callbacks if Structr was started with
// a configuration file, i.e. when this is NOT this first start.
try {
final ExecutorService service = Executors.newSingleThreadExecutor();
service.submit(new Runnable() {
@Override
public void run() {
// wait a second
try { Thread.sleep(100); } catch (Throwable ignore) {}
// call initialization callbacks from a different thread
for (final InitializationCallback callback : singletonInstance.callbacks) {
callback.initializationDone();
}
}
}).get();
} catch (Throwable t) {
logger.warn("Exception while executing post-initialization tasks", t);
}
// Don't use logger here because start/stop scripts rely on this line.
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.ms").format(new Date()) + " ---------------- Initialization complete ----------------");
setOverridingSchemaTypesAllowed(false);
initializationDone = true;
}
@Override
public void registerInitializationCallback(final InitializationCallback callback) {
callbacks.add(callback);
// callbacks need to be sorted by priority
Collections.sort(callbacks, new Comparator<InitializationCallback>() {
@Override
public int compare(final InitializationCallback o1, final InitializationCallback o2) {
return Integer.valueOf(o1.priority()).compareTo(o2.priority());
}
});
}
public boolean isInitialized() {
return initializationDone;
}
public boolean isOverridingSchemaTypesAllowed() {
return overridingSchemaTypesAllowed;
}
public void setOverridingSchemaTypesAllowed(final boolean allow) {
overridingSchemaTypesAllowed = allow;
}
public void shutdown() {
initializationDone = false;
if (!shutdownDone) {
System.out.println("INFO: Shutting down...");
for (Service service : serviceCache.values()) {
shutdownService(service);
}
serviceCache.clear();
// shut down configuration provider
configuration.shutdown();
// clear singleton instance
singletonInstance = null;
System.out.println("INFO: Shutdown complete");
// signal shutdown is complete
shutdownDone = true;
}
}
/**
* Registers a service, enabling the service layer to automatically start
* autorun servies.
*
* @param serviceClass the service class to register
*/
public void registerServiceClass(Class serviceClass) {
registeredServiceClasses.add(serviceClass);
}
public Class getServiceClassForName(final String serviceClassName) {
for (Class serviceClass : registeredServiceClasses) {
if (serviceClass.getSimpleName().equals(serviceClassName)) {
return serviceClass;
}
}
return null;
}
public ConfigurationProvider getConfigurationProvider() {
// instantiate configuration provider
if (configuration == null) {
// when executing tests, the configuration class may already exist,
// so we don't instantiate it again since all the entities are already
// known to the ClassLoader and we would miss the code in all the static
// initializers.
try {
configuration = (ConfigurationProvider)Class.forName(configurationClass).newInstance();
configuration.initialize();
} catch (Throwable t) {
logger.error("Unable to instantiate configration provider of type {}", configurationClass);
}
}
return configuration;
}
/**
* Store an attribute value in the service config
*
* @param name
* @param value
*/
public void setAttribute(final String name, final Object value) {
synchronized (attributes) {
attributes.put(name, value);
}
}
/**
* Retrieve attribute value from service config
*
* @param name
* @return attribute
*/
public Object getAttribute(final String name) {
return attributes.get(name);
}
/**
* Remove attribute value from service config
*
* @param name
*/
public void removeAttribute(final String name) {
attributes.remove(name);
}
public void startService(final String serviceName) {
final Class serviceClass = getServiceClassForName(serviceName);
if (serviceClass != null) {
startService(serviceClass);
}
}
public void startService(final Class serviceClass) {
logger.info("Creating service {}..", serviceClass.getSimpleName());
Service service = null;
try {
service = (Service) serviceClass.newInstance();
service.initialize(this);
if (service instanceof RunnableService) {
RunnableService runnableService = (RunnableService) service;
if (runnableService.runOnStartup()) {
// start RunnableService and cache it
runnableService.startService();
}
}
if (service.isRunning()) {
// cache service instance
serviceCache.put(serviceClass, service);
}
} catch (Throwable t) {
logger.error("Service {} failed to start", service.getClass().getSimpleName(), t);
}
logger.info("Calling initialization callback");
// initialization callback
service.initialized();
logger.info("Service initialized.");
}
public void shutdownService(final String serviceName) {
final Class serviceClass = getServiceClassForName(serviceName);
if (serviceClass != null) {
shutdownService(serviceClass);
}
}
public void shutdownService(final Class serviceClass) {
final Service service = serviceCache.get(serviceClass);
if (service != null) {
shutdownService(service);
}
}
private void shutdownService(final Service service) {
try {
if (service instanceof RunnableService) {
RunnableService runnableService = (RunnableService) service;
if (runnableService.isRunning()) {
runnableService.stopService();
}
}
service.shutdown();
} catch (Throwable t) {
System.out.println("WARNING: Failed to shut down " + service.getName() + ": " + t.getMessage());
}
// remove from service cache
serviceCache.remove(service.getClass());
}
/**
* Return all registered services
*
* @return list of services
*/
public List<String> getServices() {
List<String> services = new LinkedList<>();
for (Class serviceClass : registeredServiceClasses) {
final String serviceName = serviceClass.getSimpleName();
if (configuredServiceClasses.contains(serviceName)) {
services.add(serviceName);
}
}
return services;
}
@Override
public <T extends Service> T getService(final Class<T> type) {
return (T) serviceCache.get(type);
}
@Override
public DatabaseService getDatabaseService() {
final NodeService nodeService = getService(NodeService.class);
if (nodeService != null) {
return nodeService.getGraphDb();
}
return null;
}
/**
* Return true if the given service is ready to be used,
* means initialized and running.
*
* @param serviceClass
* @return isReady
*/
public boolean isReady(final Class serviceClass) {
Service service = serviceCache.get(serviceClass);
return (service != null && service.isRunning());
}
public Set<String> getResources() {
final Set<String> resources = new LinkedHashSet<>();
// scan through structr.conf and try to identify module-specific classes
for (final Setting setting : Settings.getSettings()) {
final Object configurationValue = setting.getValue();
if (configurationValue != null) {
for (final String value : configurationValue.toString().split("[\\s ,;]+")) {
try {
// try to load class and find source code origin
final Class candidate = Class.forName(value);
if (!candidate.getName().startsWith("org.structr")) {
final String codeLocation = candidate.getProtectionDomain().getCodeSource().getLocation().toString();
if (codeLocation.startsWith("file:") && codeLocation.endsWith(".jar") || codeLocation.endsWith(".war")) {
final File file = new File(URI.create(codeLocation));
if (file.exists()) {
resources.add(file.getAbsolutePath());
}
}
}
} catch (Throwable ignore) { }
}
}
}
logger.info("Found {} possible resources: {}", new Object[] { resources.size(), resources } );
return resources;
}
// ----- static methods -----
/**
* Tries to parse the given String to an int value, returning
* defaultValue on error.
*
* @param value the source String to parse
* @param defaultValue the default value that will be returned when parsing fails
* @return the parsed value or the given default value when parsing fails
*/
public static int parseInt(String value, int defaultValue) {
if (StringUtils.isBlank(value)) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException ignore) {}
return defaultValue;
}
public static boolean parseBoolean(String value, boolean defaultValue) {
if (StringUtils.isBlank(value)) {
return defaultValue;
}
try {
return Boolean.parseBoolean(value);
} catch(Throwable ignore) {}
return defaultValue;
}
public static int getGlobalSessionTimeout() {
if (globalSessionTimeout == -1) {
globalSessionTimeout = Settings.SessionTimeout.getValue();
}
return globalSessionTimeout;
}
public static Set<Permission> getPermissionsForOwnerlessNodes() {
return getInstance().permissionsForOwnerlessNodes;
}
private static String cachedEdition = null;
public static String getEdition() {
if (cachedEdition == null) {
try (final InputStream is = Services.class.getResourceAsStream("/structr.properties")) {
cachedEdition = IOUtils.toString(is);
} catch (Throwable t) {}
// fallback
if (StringUtils.isBlank(cachedEdition)) {
cachedEdition = "Source";
}
}
return cachedEdition;
}
}