/** * This file Copyright (c) 2003-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.module; import info.magnolia.cms.beans.config.ContentRepository; import info.magnolia.cms.core.Content; import info.magnolia.cms.core.HierarchyManager; import info.magnolia.cms.core.SystemProperty; import info.magnolia.cms.util.ObservationUtil; import info.magnolia.cms.util.SystemContentWrapper; import info.magnolia.context.MgnlContext; import info.magnolia.jcr.node2bean.Node2BeanException; import info.magnolia.jcr.node2bean.Node2BeanProcessor; import info.magnolia.jcr.node2bean.impl.Node2BeanProcessorImpl; import info.magnolia.jcr.node2bean.impl.Node2BeanTransformerImpl; import info.magnolia.jcr.node2bean.impl.TypeMappingImpl; import info.magnolia.module.delta.Condition; import info.magnolia.module.delta.Delta; import info.magnolia.module.delta.Task; import info.magnolia.module.delta.TaskExecutionException; import info.magnolia.module.model.ModuleDefinition; import info.magnolia.module.model.RepositoryDefinition; import info.magnolia.module.model.Version; import info.magnolia.module.model.reader.BetwixtModuleDefinitionReader; import info.magnolia.module.model.reader.DependencyChecker; import info.magnolia.module.model.reader.DependencyCheckerImpl; import info.magnolia.module.model.reader.ModuleDefinitionReader; import info.magnolia.module.ui.ModuleManagerNullUI; import info.magnolia.module.ui.ModuleManagerUI; import info.magnolia.module.ui.ModuleManagerWebUI; import info.magnolia.objectfactory.ClassFactory; import info.magnolia.objectfactory.Classes; import info.magnolia.objectfactory.Components; import info.magnolia.objectfactory.MgnlInstantiationException; import info.magnolia.repository.Provider; import info.magnolia.repository.RepositoryConstants; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import javax.jcr.RepositoryException; import javax.jcr.observation.EventIterator; import javax.jcr.observation.EventListener; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; /** * TODO : factor out into simpler units. * * @author gjoseph * @version $Revision: $ ($Author: $) */ @Singleton public class ModuleManagerImpl implements ModuleManager { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ModuleManagerImpl.class); private static final int DEFAULT_MODULE_OBSERVATION_DELAY = 5000; private static final int DEFAULT_MODULE_OBSERVATION_MAX_DELAY = 30000; // TODO : expose a method to retrieve a given module's node ? // TODO : see InstallContextImpl.getOrCreateCurrentModuleConfigNode() static final String MODULES_NODE = "modules"; /** * List<ModuleDefinition> of modules found to be deployed. */ private List<ModuleDefinition> orderedModuleDescriptors; private ModuleManagementState state; // here we use the implementation, since it has extra methods that should not be exposed to Task methods. private final InstallContextImpl installContext; private final ModuleRegistry registry; private final ModuleDefinitionReader moduleDefinitionReader; private final DependencyChecker dependencyChecker; private final Node2BeanProcessor nodeToBean; /** * @deprecated since 4.5 - use IoC - temporarily kept for tests ? */ @Deprecated protected ModuleManagerImpl() { this(new InstallContextImpl(ModuleRegistry.Factory.getInstance()), new BetwixtModuleDefinitionReader()); } /** * @deprecated since 4.5 - use IoC - temporarily kept for tests ? */ @Deprecated protected ModuleManagerImpl(InstallContextImpl installContext, ModuleDefinitionReader moduleDefinitionReader) { this(installContext, moduleDefinitionReader, ModuleRegistry.Factory.getInstance(), new DependencyCheckerImpl(), new Node2BeanProcessorImpl(new TypeMappingImpl(), new Node2BeanTransformerImpl())); } @Inject public ModuleManagerImpl(InstallContextImpl installContext, ModuleDefinitionReader moduleDefinitionReader, ModuleRegistry moduleRegistry, DependencyChecker dependencyChecker, Node2BeanProcessor nodeToBean) { this.installContext = installContext; this.moduleDefinitionReader = moduleDefinitionReader; this.registry = moduleRegistry; this.dependencyChecker = dependencyChecker; this.nodeToBean = nodeToBean; } @Override public List<ModuleDefinition> loadDefinitions() throws ModuleManagementException { if (state != null) { throw new IllegalStateException("ModuleManager was already initialized !"); } final Map<String, ModuleDefinition> moduleDefinitions = moduleDefinitionReader.readAll(); if (moduleDefinitions.isEmpty()) { throw new ModuleManagementException("No module definition was found."); } log.debug("Loaded definitions: {}", moduleDefinitions); dependencyChecker.checkDependencies(moduleDefinitions); orderedModuleDescriptors = dependencyChecker.sortByDependencyLevel(moduleDefinitions); for (ModuleDefinition moduleDefinition : orderedModuleDescriptors) { registry.registerModuleDefinition(moduleDefinition.getName(), moduleDefinition); } return orderedModuleDescriptors; } /** * In addition to checking for install or updates, this method also loads * repositories when there are no pending install or update tasks. * * @see info.magnolia.module.ModuleManager#checkForInstallOrUpdates() */ @Override public void checkForInstallOrUpdates() { // compare and determine if we need to do anything state = new ModuleManagementState(); int taskCount = 0; for (ModuleDefinition module : orderedModuleDescriptors) { installContext.setCurrentModule(module); log.debug("Checking for installation or update [{}]", module); final ModuleVersionHandler versionHandler = newVersionHandler(module); registry.registerModuleVersionHandler(module.getName(), versionHandler); final Version currentVersion = versionHandler.getCurrentlyInstalled(installContext); final List<Delta> deltas = versionHandler.getDeltas(installContext, currentVersion); if (deltas.size() > 0) { state.addModule(module, currentVersion, deltas); for (Delta delta : deltas) { taskCount += delta.getTasks().size(); } } } // TODO handle modules found in repo but not found on classpath installContext.setCurrentModule(null); installContext.setTotalTaskCount(taskCount); // if we don't have to perform any update load repositories now if (!state.needsUpdateOrInstall()) { loadModulesRepositories(); } // TODO : check the force bootstrap properties } @Override public ModuleManagementState getStatus() { if (state == null) { throw new IllegalStateException("ModuleManager was not initialized !"); } return state; } @Override public ModuleManagerUI getUI() { if (SystemProperty.getBooleanProperty("magnolia.update.auto")) { return new ModuleManagerNullUI(this); } return new ModuleManagerWebUI(this); } protected ModuleVersionHandler newVersionHandler(ModuleDefinition module) { try { final Class<? extends ModuleVersionHandler> versionHandlerClass = module.getVersionHandler(); if (versionHandlerClass != null) { return Classes.getClassFactory().newInstance(versionHandlerClass); } return new DefaultModuleVersionHandler(); } catch (MgnlInstantiationException e) { throw e; // TODO } } @Override public void performInstallOrUpdate() { synchronized (installContext) { if (state == null) { throw new IllegalStateException("ModuleManager was not initialized !"); } if (!state.needsUpdateOrInstall()) { throw new IllegalStateException("ModuleManager has nothing to do !"); } if (installContext.getStatus() != null) { throw new IllegalStateException("ModuleManager.performInstallOrUpdate() was already started !"); } installContext.setStatus(InstallStatus.inProgress); } // check all conditions boolean conditionsChecked = true; for (ModuleAndDeltas moduleAndDeltas : state.getList()) { // TODO extract "do for all deltas" logic ? installContext.setCurrentModule(moduleAndDeltas.getModule()); for (Delta delta : moduleAndDeltas.getDeltas()) { final List<Condition> conditions = delta.getConditions(); for (Condition cond : conditions) { if (!cond.check(installContext)) { conditionsChecked = false; installContext.warn(cond.getDescription()); } } } } installContext.setCurrentModule(null); if (!conditionsChecked) { installContext.setStatus(InstallStatus.stoppedConditionsNotMet); return; } loadModulesRepositories(); MgnlContext.doInSystemContext(new MgnlContext.VoidOp() { @Override public void doExec() { final Iterator<ModuleAndDeltas> it = state.getList().iterator(); while (it.hasNext()) { final ModuleAndDeltas moduleAndDeltas = it.next(); installOrUpdateModule(moduleAndDeltas, installContext); it.remove(); } } }, true); // TODO : this isn't super clean. final InstallStatus status = installContext.isRestartNeeded() ? InstallStatus.installDoneRestartNeeded : InstallStatus.installDone; installContext.setStatus(status); } @Override public InstallContext getInstallContext() { return installContext; } @Override public void startModules() { // process startup tasks before actually starting modules executeStartupTasks(); // here we use the implementation, since it has extra methods that should not be exposed to ModuleLifecycle methods. // TODO we should keep only one instance of the lifecycle context final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl(); lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_SYSTEM_STARTUP); final HierarchyManager hm = MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.CONFIG); Content modulesParentNode; try { modulesParentNode = hm.getContent(MODULES_NODE); } catch (RepositoryException e) { throw new RuntimeException("Can't start module due to failing to load the /modules node.",e); } final Collection<Content> moduleNodes = new ArrayList<Content>(); for (ModuleDefinition moduleDefinition : orderedModuleDescriptors) { final String moduleClassName = moduleDefinition.getClassName(); final String moduleName = moduleDefinition.getName(); log.info("Initializing module {}", moduleName); try { final Object moduleInstance; if (moduleClassName != null) { try { final ClassFactory classFactory = Classes.getClassFactory(); final Class<?> moduleClass = classFactory.forName(moduleClassName); // We use the currently set ComponentProvider which is main moduleInstance = Components.newInstance(moduleClass); } catch (Throwable t) { log.error("Can't instantiate " + moduleClassName + " for module " + moduleName + " : " + t.getClass() + " : " + t.getMessage(), t); continue; } // Still registering module instances with ModuleRegistry, although now one should use IoC and *depend* on such objects instead. registry.registerModuleInstance(moduleName, moduleInstance); } else { moduleInstance = null; } if (modulesParentNode.hasContent(moduleName)) { moduleNodes.add(new SystemContentWrapper(modulesParentNode.getContent(moduleName))); } if (moduleInstance != null) { populateModuleInstance(moduleInstance, getModuleInstanceProperties(moduleDefinition)); startModule(moduleInstance, moduleDefinition, lifecycleContext); // start observation ObservationUtil.registerDeferredChangeListener(RepositoryConstants.CONFIG, "/modules/" + moduleName + "/config", new EventListener() { @Override public void onEvent(EventIterator events) { final Object moduleInstance = registry.getModuleInstance(moduleName); final ModuleDefinition moduleDefinition = registry.getDefinition(moduleName); // TODO we should keep only one instance of the lifecycle context final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl(); lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_MODULE_RESTART); MgnlContext.doInSystemContext(new MgnlContext.VoidOp() { @Override public void doExec() { stopModule(moduleInstance, moduleDefinition, lifecycleContext); populateModuleInstance(moduleInstance, getModuleInstanceProperties(moduleDefinition)); startModule(moduleInstance, moduleDefinition, lifecycleContext); } }, true); } }, DEFAULT_MODULE_OBSERVATION_DELAY, DEFAULT_MODULE_OBSERVATION_MAX_DELAY); } } catch (Throwable th) { log.error("Can't start module " + moduleName, th); } } lifecycleContext.start(moduleNodes); } /** * Process startup tasks. Tasks retured by <code>ModuleDefinition.getStartupTasks()</code> are always executed and * do not require manual intervention. */ protected void executeStartupTasks() { MgnlContext.doInSystemContext(new MgnlContext.VoidOp() { @Override public void doExec() { for (ModuleDefinition module : orderedModuleDescriptors) { final ModuleVersionHandler versionHandler = registry.getVersionHandler(module.getName()); installContext.setCurrentModule(module); final Delta startup = versionHandler.getStartupDelta(installContext); applyDeltas(module, Collections.singletonList(startup), installContext); } } }, false); } protected void startModule(Object moduleInstance, final ModuleDefinition moduleDefinition, final ModuleLifecycleContextImpl lifecycleContext) { if (moduleInstance instanceof ModuleLifecycle) { lifecycleContext.setCurrentModuleDefinition(moduleDefinition); log.info("Starting module {}", moduleDefinition.getName()); ((ModuleLifecycle) moduleInstance).start(lifecycleContext); } } protected void stopModule(Object moduleInstance, final ModuleDefinition moduleDefinition, final ModuleLifecycleContextImpl lifecycleContext) { if (moduleInstance instanceof ModuleLifecycle) { lifecycleContext.setCurrentModuleDefinition(moduleDefinition); log.info("Stopping module {}", moduleDefinition.getName()); ((ModuleLifecycle) moduleInstance).stop(lifecycleContext); } } /** * Builds a map of properties to be set on the module instance, the properties are "moduleDefinition", "name", * "moduleNode" and "configNode". This map is rebuilt every-time reloading takes place just in case the nodes have * changed since startup. One situation where this is necessary is when a module does not have a config node at * startup but one is added later on, see MAGNOLIA-3457. */ protected Map<String, Object> getModuleInstanceProperties(ModuleDefinition moduleDefinition) { final HierarchyManager hm = MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.CONFIG); final String moduleNodePath = "/modules/" + moduleDefinition.getName(); Content moduleNode = null; try { moduleNode = hm.isExist(moduleNodePath) ? new SystemContentWrapper(hm.getContent(moduleNodePath)) : null; } catch (RepositoryException e) { log.error("Wasn't able to acquire module node " + moduleNodePath + ": " + e.getMessage(), e); } final String moduleConfigPath = "/modules/" + moduleDefinition.getName() + "/config"; Content configNode = null; try { configNode = hm.isExist(moduleConfigPath) ? new SystemContentWrapper(hm.getContent(moduleConfigPath)) : null; } catch (RepositoryException e) { log.error("Wasn't able to acquire module config node " + moduleConfigPath + ": " + e.getMessage(), e); } final Map<String, Object> moduleProperties = new HashMap<String, Object>(); moduleProperties.put("moduleDefinition", moduleDefinition); moduleProperties.put("name", moduleDefinition.getName()); moduleProperties.put("moduleNode", moduleNode); moduleProperties.put("configNode", configNode); return moduleProperties; } protected void populateModuleInstance(Object moduleInstance, Map<String, Object> moduleProperties) { try { BeanUtils.populate(moduleInstance, moduleProperties); } catch (Throwable e) { log.error("Can't initialize module " + moduleInstance + ": " + e.getMessage(), e); } Content content = (Content) moduleProperties.get("configNode"); if (content != null) { try { nodeToBean.setProperties(moduleInstance, content.getJCRNode(), true, new Node2BeanTransformerImpl(), Components.getComponentProvider()); } catch (Node2BeanException e) { log.error("Wasn't able to configure module " + moduleInstance + ": " + e.getMessage(), e); } catch (RepositoryException e) { log.error("Can't read module configuration " + moduleInstance + ": " + e.getMessage(), e); } } } @Override public void stopModules() { // TODO we should keep only one instance of the lifecycle context final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl(); lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_SYSTEM_SHUTDOWN); if (orderedModuleDescriptors != null) { // if module descriptors were read, let's shut down modules in reverse order final ArrayList<ModuleDefinition> shutdownOrder = new ArrayList<ModuleDefinition>(orderedModuleDescriptors); Collections.reverse(shutdownOrder); for (ModuleDefinition md : shutdownOrder) { Object module = registry.getModuleInstance(md.getName()); if (module instanceof ModuleLifecycle) { stopModule(module, md, lifecycleContext); } } } } protected void installOrUpdateModule(ModuleAndDeltas moduleAndDeltas, InstallContextImpl ctx) { final ModuleDefinition moduleDef = moduleAndDeltas.getModule(); final List<Delta> deltas = moduleAndDeltas.getDeltas(); ctx.setCurrentModule(moduleDef); log.debug("Install/update for {} is starting: {}", moduleDef, moduleAndDeltas); applyDeltas(moduleDef, deltas, ctx); log.debug("Install/update for {} has finished", moduleDef, moduleAndDeltas); } /** * Applies to given deltas for the given module. It is NOT responsible for setting the given * module as being the current module in the given context, but it is responsible for unsetting * it when done, and for saving upon success. */ protected void applyDeltas(ModuleDefinition moduleDef, List<Delta> deltas, InstallContextImpl ctx) { boolean success = true; Task currentTask = null; try { for (Delta delta : deltas) { final List<Task> tasks = delta.getTasks(); for (Task task : tasks) { currentTask = task; log.debug("Module {}, executing {}", moduleDef, currentTask); task.execute(ctx); ctx.incExecutedTaskCount(); } } } catch (TaskExecutionException e) { ctx.error("Could not install or update " + moduleDef.getName() + " module. Task '" + currentTask.getName() + "' failed. (" + ExceptionUtils.getRootCauseMessage(e) + ")", e); success = false; } catch (RuntimeException e) { ctx.error("Error while installing or updating " + moduleDef.getName() + " module. Task '" + currentTask.getName() + "' failed. (" + ExceptionUtils.getRootCauseMessage(e) + ")", e); throw e; } finally { // TODO : ctx.info("Successful installation/update."); after save ? ctx.setCurrentModule(null); } saveChanges(success); } /** * Save changes to jcr, or revert them if something went wrong. * @param persist if <code>true</code>, all workspaces are save; if <code>false</code> changes will be reverted. */ private void saveChanges(boolean persist) { // save all repositories once a module was properly installed/updated, or rollback changes. final Iterator<String> reposIt = ContentRepository.getAllRepositoryNames(); while (reposIt.hasNext()) { final String repoName = reposIt.next(); log.debug((persist ? "Saving" : "Rolling back") + " repository " + repoName); final HierarchyManager hm = MgnlContext.getHierarchyManager(repoName); try { // don't call save or refresh if useless if (hm.getWorkspace().getSession().hasPendingChanges()) { if (persist) { hm.save(); } else { hm.refresh(false); } } } catch (RepositoryException e) { throw new RuntimeException(e); // TODO } } } /** * Initializes repositories and workspaces defined by modules. * Perform repository registration tasks (create repositories or workspace, setup nodetypes) that should be done * always before starting the new module. */ private void loadModulesRepositories() { for (ModuleDefinition def : orderedModuleDescriptors) { // register repositories for (final RepositoryDefinition repDef : def.getRepositories()) { final String repositoryName = repDef.getName(); final String nodetypeFile = repDef.getNodeTypeFile(); final List<String> wsList = repDef.getWorkspaces(); String[] workSpaces = wsList.toArray(new String[wsList.size()]); loadRepository(repositoryName, nodetypeFile, workSpaces); } } } /** * Loads a single repository plus its workspaces, register nodetypes and grant permissions to superuser. */ private void loadRepository(String repositoryNameFromModuleDescriptor, String nodeTypeFile, String[] workspaces) { if (workspaces == null || workspaces.length == 0) { log.error("Trying to register the repository {} without any workspace.", repositoryNameFromModuleDescriptor); return; } final String DEFAULT_REPOSITORY_NAME = "magnolia"; String repositoryName = repositoryNameFromModuleDescriptor; if (workspaces.length > 0) { // get the repository name from the mapping, users may want to manually add it here if needed info.magnolia.repository.definition.RepositoryDefinition repositoryMapping = ContentRepository.getRepositoryMapping(workspaces[0]); if (repositoryMapping != null) { repositoryName = repositoryMapping.getName(); } } info.magnolia.repository.definition.RepositoryDefinition rm = ContentRepository.getRepositoryMapping(repositoryName); if (rm == null) { final info.magnolia.repository.definition.RepositoryDefinition defaultRepositoryMapping = ContentRepository.getRepositoryMapping(DEFAULT_REPOSITORY_NAME); final Map<String, String> defaultParameters = defaultRepositoryMapping.getParameters(); rm = new info.magnolia.repository.definition.RepositoryDefinition(); rm.setName(repositoryName); rm.setProvider(defaultRepositoryMapping.getProvider()); rm.setLoadOnStartup(true); final Map<String, String> parameters = new HashMap<String, String>(); parameters.putAll(defaultParameters); // override changed parameters final String bindName = repositoryName + StringUtils.replace(defaultParameters.get("bindName"), "magnolia", ""); final String repositoryHome = StringUtils.substringBeforeLast(defaultParameters.get("configFile"), "/") + "/" + repositoryName; parameters.put("repositoryHome", repositoryHome); parameters.put("bindName", bindName); parameters.put("customNodeTypes", nodeTypeFile); rm.setParameters(parameters); try { ContentRepository.loadRepository(rm); } catch (Exception e) { log.error(e.getMessage(), e); } } if (nodeTypeFile != null) { // register nodetypes registerNodeTypeFile(repositoryName, nodeTypeFile); // if this repo is not the default one, register nodetypes on default repo (MAGNOLIA-3189) if (!DEFAULT_REPOSITORY_NAME.equals(repositoryName)) { registerNodeTypeFile(DEFAULT_REPOSITORY_NAME, nodeTypeFile); } } if (workspaces != null) { for (String workspace : workspaces) { if (!rm.getWorkspaces().contains(workspace)) { log.debug("Loading new workspace: {}", workspace); try { ContentRepository.loadWorkspace(repositoryName, workspace); } catch (RepositoryException e) { // should never happen, the only exception we can get here is during login log.error(e.getMessage(), e); } } } } } /** * Register nodeType file in repository. * @param repositoryName repository name * @param nodeTypeFile nodeType file */ private void registerNodeTypeFile(String repositoryName, String nodeTypeFile) { Provider provider = ContentRepository.getRepositoryProvider(repositoryName); try { provider.registerNodeTypes(nodeTypeFile); } catch (RepositoryException e) { log.error(e.getMessage(), e); } } }