package org.ovirt.engine.core.extensions.mgr; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Observable; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.jboss.modules.Module; import org.jboss.modules.ModuleIdentifier; import org.jboss.modules.ModuleLoadException; import org.jboss.modules.ModuleLoader; import org.ovirt.engine.api.extensions.Base; import org.ovirt.engine.api.extensions.ExtKey; import org.ovirt.engine.api.extensions.ExtMap; import org.ovirt.engine.api.extensions.Extension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is responsible for loading the required {@code Configuration} in order to create an extension. It holds * the logic of ordering and solving conflicts during loading the configuration */ public class ExtensionsManager extends Observable { private static final Logger log = LoggerFactory.getLogger(ExtensionsManager.class); private static final Logger traceLog = LoggerFactory.getLogger(ExtensionsManager.class.getName() + ".trace"); public static final ExtKey TRACE_LOG_CONTEXT_KEY = new ExtKey("EXTENSION_MANAGER_TRACE_LOG", Logger.class, "863db666-3ea7-4751-9695-918a3197ad83"); public static final ExtKey CAUSE_OUTPUT_KEY = new ExtKey("EXTENSION_MANAGER_CAUSE_OUTPUT_KEY", Throwable.class, "894e1c86-518b-40a2-a92b-29ea1eb0403d"); private static interface BindingsLoader { ExtensionProxy load(Properties props) throws Exception; } private static class JBossBindingsLoader implements BindingsLoader { private Map<String, Module> loadedModules = new HashMap<>(); private Module loadModule(String moduleSpec) { // If the module was not already loaded, load it try { Module module = loadedModules.get(moduleSpec); if (module == null) { ModuleLoader loader = ModuleLoader.forClass(this.getClass()); if (loader == null) { throw new ConfigurationException(String.format("The module '%1$s' cannot be loaded as the module system isn't enabled.", moduleSpec)); } module = loader.loadModule(ModuleIdentifier.fromString(moduleSpec)); loadedModules.put(moduleSpec, module); } return module; } catch (ModuleLoadException exception) { throw new ConfigurationException(String.format("The module '%1$s' cannot be loaded: %2$s", moduleSpec, exception.getMessage()), exception); } } private <T extends Class> T lookupService(Module module, T serviceInterface, String serviceClassName) { T serviceClass = null; for (Object service : module.loadService(serviceInterface)) { if (service.getClass().getName().equals(serviceClassName)) { serviceClass = (T)service.getClass(); break; } } if (serviceClass == null) { throw new ConfigurationException(String.format("The module '%1$s' does not contain the service '%2$s'.", module.getIdentifier().getName(), serviceClassName)); } return serviceClass; } public ExtensionProxy load(Properties props) throws Exception { Module module = loadModule( props.getProperty(Base.ConfigKeys.BINDINGS_JBOSSMODULE_MODULE) ); return new ExtensionProxy( module.getClassLoader(), lookupService( module, Extension.class, props.getProperty(Base.ConfigKeys.BINDINGS_JBOSSMODULE_CLASS) ).newInstance() ); } } private static class ExtensionEntry { private static int extensionNameIndex = 0; private String name; private File file; private boolean enabled; private boolean initialized; private ExtensionProxy extension; private ExtensionEntry(Properties props, File file) { this.file = file; this.name = props.getProperty( Base.ConfigKeys.NAME, String.format("__unamed_%1$03d__", extensionNameIndex++) ); this.enabled = Boolean.valueOf(props.getProperty(Base.ConfigKeys.ENABLED, "true")); } private String getFileName() { return file != null ? file.getAbsolutePath() : "N/A"; } } private static final Map<String, BindingsLoader> bindingsLoaders = new HashMap<>(); static { bindingsLoaders.put(Base.ConfigBindingsMethods.JBOSSMODULE, new JBossBindingsLoader()); } private ConcurrentMap<String, ExtensionEntry> loadedEntries = new ConcurrentHashMap<>(); private ConcurrentMap<String, ExtensionEntry> initializedEntries = new ConcurrentHashMap<>(); private final ExtMap globalContext = new ExtMap().mput(Base.GlobalContextKeys.EXTENSIONS, new ArrayList<ExtMap>()); public String load(Properties configuration) { return loadImpl(configuration, null); } public String load(File file) { try ( InputStream is = new FileInputStream(file); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8) ) { Properties props = new Properties(); props.load(reader); return loadImpl(props, file); } catch (IOException exception) { throw new ConfigurationException(String.format("Can't load object configuration file '%1$s': %2$s", file.getAbsolutePath(), exception.getMessage()), exception); } } private void dumpConfig(ExtensionProxy extension) { Logger traceLogger = extension.getContext().get(TRACE_LOG_CONTEXT_KEY); if (traceLogger.isDebugEnabled()) { Collection sensitive = extension.getContext().get(Base.ContextKeys.CONFIGURATION_SENSITIVE_KEYS); traceLogger.debug("Config BEGIN"); for (Map.Entry<Object, Object> entry : extension.getContext().<Properties>get(Base.ContextKeys.CONFIGURATION).entrySet()) { traceLogger.debug( String.format( "%s: %s", entry.getKey(), sensitive.contains(entry.getKey()) ? "***" : entry.getValue() ) ); } traceLogger.debug("Config END"); } } private Collection<String> splitString(String s) { return new ArrayList<>(Arrays.asList(s.trim().split("\\s*,\\s*", 0))); } private synchronized String loadImpl(Properties props, File confFile) { ExtensionEntry entry = new ExtensionEntry(props, confFile); if (!entry.enabled) { return null; } ExtensionEntry alreadyLoadedEntry = loadedEntries.get(entry.name); if (alreadyLoadedEntry != null) { throw new ConfigurationException(String.format( "Could not load the configuration '%1$s' from file %2$s. A configuration with the same name was already loaded from file %3$s", entry.name, entry.getFileName(), alreadyLoadedEntry.getFileName() )); } try { entry.extension = loadExtension(props); entry.extension.getContext().mput( Base.ContextKeys.GLOBAL_CONTEXT, globalContext ).mput( TRACE_LOG_CONTEXT_KEY, traceLog ).mput( Base.ContextKeys.INTERFACE_VERSION_MIN, 0 ).mput( Base.ContextKeys.INTERFACE_VERSION_MAX, Base.INTERFACE_VERSION_CURRENT ).mput( Base.ContextKeys.LOCALE, Locale.getDefault().toString() ).mput( Base.ContextKeys.CONFIGURATION_FILE, entry.file == null ? null : entry.file.getAbsolutePath() ).mput( Base.ContextKeys.CONFIGURATION, props ).mput( Base.ContextKeys.CONFIGURATION_SENSITIVE_KEYS, splitString(props.getProperty(Base.ConfigKeys.SENSITIVE_KEYS, "")) ).mput( Base.ContextKeys.INSTANCE_NAME, entry.name ).mput( Base.ContextKeys.PROVIDES, splitString(props.getProperty(Base.ConfigKeys.PROVIDES, "")) ); log.info("Loading extension '{}'", entry.name); ExtMap output = entry.extension.invoke( new ExtMap().mput( Base.InvokeKeys.COMMAND, Base.InvokeCommands.LOAD ) ); log.info("Extension '{}' loaded", entry.name); entry.extension.getContext().put( TRACE_LOG_CONTEXT_KEY, LoggerFactory.getLogger( String.format( "%1$s.%2$s.%3$s", traceLog.getName(), entry.extension.getContext().get(Base.ContextKeys.EXTENSION_NAME), entry.extension.getContext().get(Base.ContextKeys.INSTANCE_NAME) ) ) ); if (output.<Integer>get(Base.InvokeKeys.RESULT) != Base.InvokeResult.SUCCESS) { throw new RuntimeException( String.format("Invoke of LOAD returned with error code: %1$s", output.<Integer>get(Base.InvokeKeys.RESULT) ) ); } } catch (Exception e) { throw new RuntimeException(String.format("Error loading extension '%1$s': %2$s", entry.name, e.getMessage()), e); } loadedEntries.put(entry.name, entry); dumpConfig(entry.extension); setChanged(); notifyObservers(); return entry.name; } public ExtMap getGlobalContext() { return globalContext; } public List<ExtensionProxy> getExtensionsByService(String provides) { List<ExtensionProxy> results = new ArrayList<>(); for (ExtensionEntry entry : initializedEntries.values()) { if (entry.extension.getContext().<Collection<String>> get(Base.ContextKeys.PROVIDES).contains(provides)) { results.add(entry.extension); } } return results; } public ExtensionProxy getExtensionByName(String name) throws ConfigurationException { if (name == null) { throw new ConfigurationException("Extension was not specified"); } ExtensionEntry entry = initializedEntries.get(name); if (entry == null) { throw new ConfigurationException(String.format("Extension %1$s could not be found", name)); } return entry.extension; } public List<ExtensionProxy> getLoadedExtensions() { List<ExtensionProxy> results = new ArrayList<>(loadedEntries.size()); for (ExtensionEntry entry : loadedEntries.values()) { results.add(entry.extension); } return results; } public List<ExtensionProxy> getExtensions() { List<ExtensionProxy> results = new ArrayList<>(initializedEntries.size()); for (ExtensionEntry entry : initializedEntries.values()) { results.add(entry.extension); } return results; } public ExtensionProxy initialize(String extensionName) { ExtensionEntry entry = loadedEntries.get(extensionName); if (entry == null) { throw new RuntimeException(String.format("No extensioned with instance name %1$s could be found", extensionName)); } try { log.info("Initializing extension '{}'", entry.name); entry.extension.invoke(new ExtMap().mput(Base.InvokeKeys.COMMAND, Base.InvokeCommands.INITIALIZE)); log.info("Extension '{}' initialized", entry.name); } catch (Exception ex) { log.error("Error in activating extension '{}': {}", entry.name, ex.getMessage()); if (log.isDebugEnabled()) { log.debug(ex.toString(), ex); } throw new RuntimeException(ex); } entry.initialized = true; initializedEntries.put(extensionName, entry); synchronized (globalContext) { globalContext.<Collection<ExtMap>> get(Base.GlobalContextKeys.EXTENSIONS).add( new ExtMap().mput( Base.ExtensionRecord.INSTANCE_NAME, entry.extension.getContext().get(Base.ContextKeys.INSTANCE_NAME) ).mput( Base.ExtensionRecord.PROVIDES, entry.extension.getContext().get(Base.ContextKeys.PROVIDES) ).mput( Base.ExtensionRecord.CLASS_LOADER, entry.extension.getClassLoader() ).mput( Base.ExtensionRecord.EXTENSION, entry.extension.getExtension() ).mput( Base.ExtensionRecord.CONTEXT, entry.extension.getContext() ) ); } setChanged(); notifyObservers(); return entry.extension; } private ExtensionProxy loadExtension(Properties props) throws Exception { BindingsLoader loader = bindingsLoaders.get(props.getProperty(Base.ConfigKeys.BINDINGS_METHOD)); if (loader == null) { throw new ConfigurationException(String.format("Invalid binding method '%1$s'.", props.getProperty(Base.ConfigKeys.BINDINGS_METHOD))); } return loader.load(props); } public void dump() { log.info("Start of enabled extensions list"); for (ExtensionEntry entry : loadedEntries.values()) { if (entry.extension != null) { ExtMap context = entry.extension.getContext(); log.info("Instance name: '{}', Extension name: '{}', Version: '{}', Notes: '{}', License: '{}', Home: '{}', Author '{}', Build interface Version: '{}', File: '{}', Initialized: '{}'", emptyIfNull(context.get(Base.ContextKeys.INSTANCE_NAME)), emptyIfNull(context.get(Base.ContextKeys.EXTENSION_NAME)), emptyIfNull(context.get(Base.ContextKeys.VERSION)), emptyIfNull(context.get(Base.ContextKeys.EXTENSION_NOTES)), emptyIfNull(context.get(Base.ContextKeys.LICENSE)), emptyIfNull(context.get(Base.ContextKeys.HOME_URL)), emptyIfNull(context.get(Base.ContextKeys.AUTHOR)), emptyIfNull(context.get(Base.ContextKeys.BUILD_INTERFACE_VERSION)), entry.getFileName(), entry.initialized ); } } log.info("End of enabled extensions list"); } private Object emptyIfNull(Object value) { return value == null ? "" : value; } }