View Javadoc
1   /**
2    * This file Copyright (c) 2003-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module;
35  
36  import info.magnolia.cms.core.SystemProperty;
37  import info.magnolia.cms.util.ContentUtil;
38  import info.magnolia.cms.util.ObservationUtil;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.event.EventBus;
41  import info.magnolia.event.SystemEventBus;
42  import info.magnolia.jcr.node2bean.Node2BeanException;
43  import info.magnolia.jcr.node2bean.Node2BeanProcessor;
44  import info.magnolia.jcr.node2bean.impl.Node2BeanTransformerImpl;
45  import info.magnolia.jcr.util.NodeTypes;
46  import info.magnolia.jcr.util.NodeUtil;
47  import info.magnolia.jcr.wrapper.SystemNodeWrapper;
48  import info.magnolia.module.delta.Condition;
49  import info.magnolia.module.delta.Delta;
50  import info.magnolia.module.delta.Task;
51  import info.magnolia.module.delta.TaskExecutionException;
52  import info.magnolia.module.model.ModuleDefinition;
53  import info.magnolia.module.model.RepositoryDefinition;
54  import info.magnolia.module.model.Version;
55  import info.magnolia.module.model.reader.DependencyChecker;
56  import info.magnolia.module.model.reader.ModuleDefinitionReader;
57  import info.magnolia.module.ui.ModuleManagerNullUI;
58  import info.magnolia.module.ui.ModuleManagerUI;
59  import info.magnolia.module.ui.ModuleManagerWebUI;
60  import info.magnolia.objectfactory.ClassFactory;
61  import info.magnolia.objectfactory.Classes;
62  import info.magnolia.objectfactory.Components;
63  import info.magnolia.objectfactory.MgnlInstantiationException;
64  import info.magnolia.repository.DefaultRepositoryManager;
65  import info.magnolia.repository.Provider;
66  import info.magnolia.repository.RepositoryConstants;
67  import info.magnolia.repository.RepositoryManager;
68  import info.magnolia.repository.definition.WorkspaceMappingDefinition;
69  
70  import java.util.ArrayList;
71  import java.util.Collection;
72  import java.util.Collections;
73  import java.util.HashMap;
74  import java.util.Iterator;
75  import java.util.List;
76  import java.util.Map;
77  import java.util.Set;
78  import java.util.stream.Collectors;
79  
80  import javax.inject.Inject;
81  import javax.inject.Singleton;
82  import javax.jcr.Node;
83  import javax.jcr.RepositoryException;
84  import javax.jcr.Session;
85  import javax.jcr.observation.EventIterator;
86  import javax.jcr.observation.EventListener;
87  
88  import org.apache.commons.beanutils.BeanUtils;
89  import org.apache.commons.lang3.StringUtils;
90  import org.apache.commons.lang3.exception.ExceptionUtils;
91  import org.slf4j.Logger;
92  import org.slf4j.LoggerFactory;
93  
94  import com.google.common.collect.Sets;
95  
96  /**
97   * TODO : factor out into simpler units.
98   */
99  @Singleton
100 public class ModuleManagerImpl implements ModuleManager {
101 
102     private static final Logger log = LoggerFactory.getLogger(ModuleManagerImpl.class);
103 
104     private static final int DEFAULT_MODULE_OBSERVATION_DELAY = 5000;
105     private static final int DEFAULT_MODULE_OBSERVATION_MAX_DELAY = 30000;
106 
107     // TODO : expose a method to retrieve a given module's node ?
108     // TODO : see InstallContextImpl.getOrCreateCurrentModuleConfigNode()
109     static final String MODULES_NODE = "modules";
110 
111     /**
112      * List<ModuleDefinition> of modules found to be deployed.
113      */
114     private List<ModuleDefinition> orderedModuleDescriptors;
115 
116     private ModuleManagementState state;
117 
118     // here we use the implementation, since it has extra methods that should not be exposed to Task methods.
119     private final InstallContextImpl installContext;
120     private final ModuleRegistry registry;
121     private final DependencyChecker dependencyChecker;
122     private final Node2BeanProcessor nodeToBean;
123     private final RepositoryManager repositoryManager;
124     private final Set<ModuleDefinitionReader> moduleDefinitionReaders;
125 
126     /**
127      * @deprecated since 5.4.7 - use {@link #ModuleManagerImpl(InstallContextImpl, Set, ModuleRegistry, DependencyChecker, Node2BeanProcessor, RepositoryManager)} instead.
128      */
129     @Deprecated
130     public ModuleManagerImpl(InstallContextImpl installContext, ModuleDefinitionReader moduleDefinitionReader, ModuleRegistry moduleRegistry, DependencyChecker dependencyChecker, Node2BeanProcessor nodeToBean, RepositoryManager repositoryManager) {
131         this(installContext, Sets.newHashSet(moduleDefinitionReader), moduleRegistry, dependencyChecker, nodeToBean, repositoryManager);
132     }
133 
134     @Inject
135     public ModuleManagerImpl(InstallContextImpl installContext, Set<ModuleDefinitionReader> moduleDefinitionReaders, ModuleRegistry moduleRegistry, DependencyChecker dependencyChecker, Node2BeanProcessor nodeToBean, RepositoryManager repositoryManager) {
136         this.installContext = installContext;
137         this.moduleDefinitionReaders = moduleDefinitionReaders;
138         this.registry = moduleRegistry;
139         this.dependencyChecker = dependencyChecker;
140         this.nodeToBean = nodeToBean;
141         this.repositoryManager = repositoryManager;
142     }
143 
144     @Override
145     public List<ModuleDefinition> loadDefinitions() throws ModuleManagementException {
146         if (state != null) {
147             throw new IllegalStateException("ModuleManager was already initialized !");
148         }
149         Map<String, ModuleDefinition> moduleDefinitions = new HashMap<>();
150         for (ModuleDefinitionReader moduleDefinitionReader : moduleDefinitionReaders) {
151             moduleDefinitions.putAll(moduleDefinitionReader.readAll());
152         }
153 
154         if (moduleDefinitions.isEmpty()) {
155             throw new ModuleManagementException("No module definition was found.");
156         }
157         log.debug("Loaded definitions: {}", moduleDefinitions);
158 
159         dependencyChecker.checkDependencies(moduleDefinitions);
160         orderedModuleDescriptors = dependencyChecker.sortByDependencyLevel(moduleDefinitions);
161         for (ModuleDefinition moduleDefinition : orderedModuleDescriptors) {
162             registry.registerModuleDefinition(moduleDefinition.getName(), moduleDefinition);
163         }
164         return orderedModuleDescriptors;
165     }
166 
167     /**
168      * In addition to checking for install or updates, this method also loads
169      * repositories when there are no pending install or update tasks.
170      *
171      * @see info.magnolia.module.ModuleManager#checkForInstallOrUpdates()
172      */
173     @Override
174     public void checkForInstallOrUpdates() {
175         // compare and determine if we need to do anything
176         state = new ModuleManagementState();
177         int taskCount = 0;
178         for (ModuleDefinition module : orderedModuleDescriptors) {
179             installContext.setCurrentModule(module);
180             log.debug("Checking for installation or update [{}]", module);
181             final ModuleVersionHandler versionHandler = newVersionHandler(module);
182             registry.registerModuleVersionHandler(module.getName(), versionHandler);
183 
184             final Version currentVersion = versionHandler.getCurrentlyInstalled(installContext);
185             final List<Delta> deltas = versionHandler.getDeltas(installContext, currentVersion);
186             if (deltas.size() > 0) {
187                 state.addModule(module, currentVersion, deltas);
188                 for (Delta delta : deltas) {
189                     taskCount += delta.getTasks().size();
190                 }
191             }
192         }
193         // TODO handle modules found in repo but not found on classpath
194 
195         installContext.setCurrentModule(null);
196         installContext.setTotalTaskCount(taskCount);
197 
198         // if we don't have to perform any update load repositories now
199         if (!state.needsUpdateOrInstall()) {
200             loadModulesRepositories();
201         }
202 
203         // TODO : check the force bootstrap properties
204     }
205 
206     @Override
207     public ModuleManagementState getStatus() {
208         if (state == null) {
209             throw new IllegalStateException("ModuleManager was not initialized !");
210         }
211 
212         return state;
213     }
214 
215     @Override
216     public ModuleManagerUI getUI() {
217         if (SystemProperty.getBooleanProperty("magnolia.update.auto")) {
218             return new ModuleManagerNullUI(this);
219         }
220         return new ModuleManagerWebUI(this);
221     }
222 
223     protected ModuleVersionHandler newVersionHandler(ModuleDefinition module) {
224         try {
225             final Class<? extends ModuleVersionHandler> versionHandlerClass = module.getVersionHandler();
226             if (versionHandlerClass != null) {
227                 return Components.newInstance(versionHandlerClass);
228             }
229             return new DefaultModuleVersionHandler();
230         } catch (MgnlInstantiationException e) {
231             throw e; // TODO
232         }
233     }
234 
235     @Override
236     public void performInstallOrUpdate() {
237         synchronized (installContext) {
238             if (state == null) {
239                 throw new IllegalStateException("ModuleManager was not initialized !");
240             }
241             if (!state.needsUpdateOrInstall()) {
242                 throw new IllegalStateException("ModuleManager has nothing to do !");
243             }
244             if (installContext.getStatus() != null) {
245                 throw new IllegalStateException("ModuleManager.performInstallOrUpdate() was already started !");
246             }
247             installContext.setStatus(InstallStatus.inProgress);
248         }
249 
250         // check all conditions
251         boolean conditionsChecked = true;
252         for (ModuleAndDeltas moduleAndDeltas : state.getList()) {
253             // TODO extract "do for all deltas" logic ?
254             installContext.setCurrentModule(moduleAndDeltas.getModule());
255             for (Delta delta : moduleAndDeltas.getDeltas()) {
256                 final List<Condition> conditions = delta.getConditions();
257                 for (Condition cond : conditions) {
258                     if (!cond.check(installContext)) {
259                         conditionsChecked = false;
260                         installContext.warn(cond.getDescription());
261                     }
262                 }
263             }
264         }
265         installContext.setCurrentModule(null);
266         if (!conditionsChecked) {
267             installContext.setStatus(InstallStatus.stoppedConditionsNotMet);
268             return;
269         }
270 
271         loadModulesRepositories();
272 
273         MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
274             @Override
275             public void doExec() {
276                 final Iterator<ModuleAndDeltas> it = state.getList().iterator();
277                 while (it.hasNext()) {
278                     final ModuleAndDeltas moduleAndDeltas = it.next();
279                     installOrUpdateModule(moduleAndDeltas, installContext);
280                     it.remove();
281                 }
282             }
283         }, true);
284 
285         // TODO : this isn't super clean.
286         final InstallStatus status = installContext.isRestartNeeded() ? InstallStatus.installDoneRestartNeeded : InstallStatus.installDone;
287         installContext.setStatus(status);
288 
289         // Exceptionally ask the system to do a garbage collection - installation requires a lot of memory that could be freed now.
290         // This method is only executed upon module installation, so no users should be affected.
291         System.gc();
292     }
293 
294     @Override
295     public InstallContext getInstallContext() {
296         return installContext;
297     }
298 
299     @Override
300     public void startModules() {
301         // process startup tasks before actually starting modules
302         executeStartupTasks();
303 
304         // here we use the implementation, since it has extra methods that should not be exposed to ModuleLifecycle methods.
305         // TODO we should keep only one instance of the lifecycle context
306         final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl();
307         lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_SYSTEM_STARTUP);
308         final Node modulesParentNode;
309         try {
310             final Session session = MgnlContext.getSystemContext().getJCRSession(RepositoryConstants.CONFIG);
311             modulesParentNode = NodeUtil.createPath(session.getRootNode(), MODULES_NODE, NodeTypes.Content.NAME);
312         } catch (RepositoryException e) {
313             throw new RuntimeException("Can't start module due to failing to load the /modules node.", e);
314         }
315         final Collection<Node> moduleNodes = new ArrayList<>();
316 
317         for (ModuleDefinition moduleDefinition : orderedModuleDescriptors) {
318             final String moduleClassName = moduleDefinition.getClassName();
319             final String moduleName = moduleDefinition.getName();
320             log.info("Initializing module {}", moduleName);
321 
322             try {
323                 final Object moduleInstance;
324                 if (moduleClassName != null) {
325                     try {
326                         final ClassFactory classFactory = Classes.getClassFactory();
327                         final Class<?> moduleClass = classFactory.forName(moduleClassName);
328                         // We use the currently set ComponentProvider which is main
329                         moduleInstance = Components.newInstance(moduleClass);
330                     } catch (Throwable t) {
331                         log.error("Can't instantiate {} for module {} : {} : {}", moduleClassName, moduleName, t.getClass(), t.getMessage(), t);
332                         continue;
333                     }
334                     // Still registering module instances with ModuleRegistry, although now one should use IoC and *depend* on such objects instead.
335                     registry.registerModuleInstance(moduleName, moduleInstance);
336                 } else {
337                     moduleInstance = null;
338                 }
339 
340                 if (modulesParentNode.hasNode(moduleName)) {
341                     moduleNodes.add(new SystemNodeWrapper(modulesParentNode.getNode(moduleName)));
342                 }
343 
344                 if (moduleInstance != null) {
345 
346                     populateModuleInstance(moduleInstance, getModuleInstanceProperties(moduleDefinition));
347 
348                     startModule(moduleInstance, moduleDefinition, lifecycleContext);
349 
350                     // start observation
351                     ObservationUtil.registerDeferredChangeListener(RepositoryConstants.CONFIG, "/modules/" + moduleName + "/config", new EventListener() {
352 
353                         @Override
354                         public void onEvent(EventIterator events) {
355                             final Object moduleInstance = registry.getModuleInstance(moduleName);
356                             final ModuleDefinition moduleDefinition = registry.getDefinition(moduleName);
357 
358                             // TODO we should keep only one instance of the lifecycle context
359                             final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl();
360                             lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_MODULE_RESTART);
361                             MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
362                                 @Override
363                                 public void doExec() {
364                                     stopModule(moduleInstance, moduleDefinition, lifecycleContext);
365                                     populateModuleInstance(moduleInstance, getModuleInstanceProperties(moduleDefinition));
366                                     startModule(moduleInstance, moduleDefinition, lifecycleContext);
367                                 }
368                             }, true);
369                         }
370                     }, DEFAULT_MODULE_OBSERVATION_DELAY, DEFAULT_MODULE_OBSERVATION_MAX_DELAY);
371                 }
372             } catch (Throwable th) {
373                 log.error("Can't start module {}", moduleName, th);
374             }
375         }
376 
377         lifecycleContext.start(moduleNodes.stream().map(ContentUtil::asContent).collect(Collectors.toList())); //FIXME kept for compatibility, still used for info.magnolia.module.admininterface.PageHandlerManager
378 
379         // Acquire the system event bus, it lives in the 'main' component provider while ModuleManagerImpl lives in the
380         // 'platform' so we can't get it using IoC. Instead we use Components.getComponentProvider() which at this point
381         // returns 'main'.
382         EventBus systemEventBus = Components.getComponentWithAnnotation(EventBus.class, Components.named(SystemEventBus.NAME));
383         systemEventBus.fireEvent(new ModulesStartedEvent());
384 
385         // Exceptionally ask the system to do a garbage collection - same as for installation, the startup consumes memory,
386         // which may prevent users to login after startup.
387         // This method is only executed upon startup, so no users should be affected.
388         System.gc();
389     }
390 
391     /**
392      * Process startup tasks. Tasks retured by <code>ModuleDefinition.getStartupTasks()</code> are always executed and
393      * do not require manual intervention.
394      */
395     protected void executeStartupTasks() {
396         MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
397             @Override
398             public void doExec() {
399                 for (ModuleDefinition module : orderedModuleDescriptors) {
400                     final ModuleVersionHandler versionHandler = registry.getVersionHandler(module.getName());
401                     installContext.setCurrentModule(module);
402                     final Delta startup = versionHandler.getStartupDelta(installContext);
403                     applyDeltas(module, Collections.singletonList(startup), installContext);
404                 }
405             }
406         }, false);
407     }
408 
409     protected void startModule(Object moduleInstance, final ModuleDefinition moduleDefinition, final ModuleLifecycleContextImpl lifecycleContext) {
410         if (moduleInstance instanceof ModuleLifecycle) {
411             lifecycleContext.setCurrentModuleDefinition(moduleDefinition);
412             log.info("Starting module {}", moduleDefinition.getName());
413             ((ModuleLifecycle) moduleInstance).start(lifecycleContext);
414         }
415     }
416 
417     protected void stopModule(Object moduleInstance, final ModuleDefinition moduleDefinition, final ModuleLifecycleContextImpl lifecycleContext) {
418         if (moduleInstance instanceof ModuleLifecycle) {
419             lifecycleContext.setCurrentModuleDefinition(moduleDefinition);
420             log.info("Stopping module {}", moduleDefinition.getName());
421             ((ModuleLifecycle) moduleInstance).stop(lifecycleContext);
422         }
423     }
424 
425     /**
426      * Builds a map of properties to be set on the module instance, the properties are "moduleDefinition", "name",
427      * "moduleNode" and "configNode". This map is rebuilt every-time reloading takes place just in case the nodes have
428      * changed since startup. One situation where this is necessary is when a module does not have a config node at
429      * startup but one is added later on, see MAGNOLIA-3457.
430      */
431     protected Map<String, Object> getModuleInstanceProperties(ModuleDefinition moduleDefinition) {
432         final Map<String, Object> moduleProperties = new HashMap<String, Object>();
433         final String moduleConfigPath = "/modules/" + moduleDefinition.getName() + "/config";
434         final String moduleNodePath = "/modules/" + moduleDefinition.getName();
435         try {
436             final Session session = MgnlContext.getSystemContext().getJCRSession(RepositoryConstants.CONFIG);
437             final Node moduleNode = session.nodeExists(moduleNodePath) ? new SystemNodeWrapper(session.getNode(moduleNodePath)) : null;
438             final Node configNode = session.nodeExists(moduleConfigPath) ? new SystemNodeWrapper(session.getNode(moduleConfigPath)) : null;
439             moduleProperties.put("moduleDefinition", moduleDefinition);
440             moduleProperties.put("name", moduleDefinition.getName());
441             moduleProperties.put("moduleNode", moduleNode);
442             moduleProperties.put("configNode", configNode);
443         } catch (RepositoryException e) {
444             log.error("Wasn't able to acquire module or module config node {}: {}", moduleConfigPath, e.getMessage(), e);
445         }
446         return moduleProperties;
447     }
448 
449     protected void populateModuleInstance(Object moduleInstance, Map<String, Object> moduleProperties) {
450 
451         try {
452             BeanUtils.populate(moduleInstance, moduleProperties);
453         } catch (Throwable e) {
454             log.error("Can't initialize module {}: {}", moduleInstance, e.getMessage(), e);
455         }
456 
457         Node content = (Node) moduleProperties.get("configNode");
458         if (content != null) {
459             try {
460                 nodeToBean.setProperties(moduleInstance, content, true, new Node2BeanTransformerImpl(), Components.getComponentProvider());
461             } catch (Node2BeanException e) {
462                 log.error("Wasn't able to configure module {}: {}", moduleInstance, e.getMessage(), e);
463             } catch (RepositoryException e) {
464                 log.error("Can't read module configuration {}: {}", moduleInstance, e.getMessage(), e);
465             }
466         }
467     }
468 
469     @Override
470     public void stopModules() {
471         // TODO we should keep only one instance of the lifecycle context
472         final ModuleLifecycleContextImpl lifecycleContext = new ModuleLifecycleContextImpl();
473         lifecycleContext.setPhase(ModuleLifecycleContext.PHASE_SYSTEM_SHUTDOWN);
474         if (orderedModuleDescriptors != null) {
475             // if module descriptors were read, let's shut down modules in reverse order
476             final ArrayList<ModuleDefinition> shutdownOrder = new ArrayList<ModuleDefinition>(orderedModuleDescriptors);
477             Collections.reverse(shutdownOrder);
478             for (ModuleDefinition md : shutdownOrder) {
479                 Object module = registry.getModuleInstance(md.getName());
480                 if (module instanceof ModuleLifecycle) {
481                     stopModule(module, md, lifecycleContext);
482                 }
483 
484             }
485         }
486     }
487 
488     protected void installOrUpdateModule(ModuleAndDeltas moduleAndDeltas, InstallContextImpl ctx) {
489         final ModuleDefinition moduleDef = moduleAndDeltas.getModule();
490         final List<Delta> deltas = moduleAndDeltas.getDeltas();
491         ctx.setCurrentModule(moduleDef);
492         log.debug("Install/update for {} is starting: {}", moduleDef, moduleAndDeltas);
493         applyDeltas(moduleDef, deltas, ctx);
494         log.debug("Install/update for {} has finished", moduleDef, moduleAndDeltas);
495     }
496 
497     /**
498      * Applies to given deltas for the given module. It is NOT responsible for setting the given
499      * module as being the current module in the given context, but it is responsible for unsetting
500      * it when done, and for saving upon success.
501      */
502     protected void applyDeltas(ModuleDefinition moduleDef, List<Delta> deltas, InstallContextImpl ctx) {
503         boolean success = true;
504         Task currentTask = null;
505         try {
506             for (Delta delta : deltas) {
507                 final List<Task> tasks = delta.getTasks();
508                 for (Task task : tasks) {
509                     currentTask = task;
510                     log.debug("Module {}, executing {}", moduleDef, currentTask);
511                     task.execute(ctx);
512                     ctx.incExecutedTaskCount();
513                 }
514             }
515         } catch (TaskExecutionException e) {
516             ctx.error("Could not install or update " + moduleDef.getName() + " module. Task '" + currentTask.getName() + "' failed. (" + ExceptionUtils.getRootCauseMessage(e) + ")", e);
517             success = false;
518         } catch (RuntimeException e) {
519             ctx.error("Error while installing or updating " + moduleDef.getName() + " module. Task '" + (currentTask == null ? "<null>" : currentTask.getName()) + "' failed. (" + ExceptionUtils.getRootCauseMessage(e) + ")", e);
520             ctx.setStatus(InstallStatus.installFailed);
521             throw e;
522         } finally {
523             // TODO : ctx.info("Successful installation/update."); after save ?
524             ctx.setCurrentModule(null);
525         }
526 
527         saveChanges(success);
528     }
529 
530     /**
531      * Save changes to jcr, or revert them if something went wrong.
532      *
533      * @param persist if <code>true</code>, all workspaces are save; if <code>false</code> changes will be reverted.
534      */
535     private void saveChanges(boolean persist) {
536         // save all repositories once a module was properly installed/updated, or rollback changes.
537         Collection<String> repositoryNames = repositoryManager.getWorkspaceNames();
538         for (String repoName : repositoryNames) {
539             log.debug("{} repository {}", (persist ? "Saving" : "Rolling back"), repoName);
540             try {
541                 final Session session = MgnlContext.getJCRSession(repoName);
542 
543                 if (session.hasPendingChanges()) {
544                     if (persist) {
545                         boolean isMasterCluster = isMasterCluster();
546                         boolean isClustered = isClustered(repoName);
547 
548                         if (!isClustered || isMasterCluster) {
549                             session.save();
550                         } else {
551                             session.refresh(false);
552                             log.info("Skipping bootstrapping of repository '{}' because it is clustered and node is not cluster master ", repoName);
553                         }
554                     } else {
555                         session.refresh(false);
556                     }
557                 }
558             } catch (RepositoryException e) {
559                 throw new RuntimeException(e);
560             }
561         }
562     }
563 
564     private boolean isClustered(String repositoryName) {
565         return repositoryManager instanceof DefaultRepositoryManager && ((DefaultRepositoryManager) repositoryManager).isClusteredWorkspace(repositoryName);
566     }
567 
568     private boolean isMasterCluster() {
569         return repositoryManager instanceof DefaultRepositoryManager && ((DefaultRepositoryManager) repositoryManager).isClusterMaster();
570     }
571 
572     /**
573      * Initializes repositories and workspaces defined by modules.
574      * Perform repository registration tasks (create repositories or workspace, setup nodetypes) that should be done
575      * always before starting the new module.
576      */
577     private void loadModulesRepositories() {
578         for (ModuleDefinition def : orderedModuleDescriptors) {
579             // register repositories
580             for (final RepositoryDefinition repDef : def.getRepositories()) {
581                 final String repositoryName = repDef.getName();
582 
583                 final String nodetypeFile = repDef.getNodeTypeFile();
584 
585                 final List<String> wsList = repDef.getWorkspaces();
586                 String[] workSpaces = wsList.toArray(new String[wsList.size()]);
587 
588                 loadRepository(repositoryName, nodetypeFile, workSpaces);
589             }
590         }
591     }
592 
593     /**
594      * Loads a single repository plus its workspaces, register nodetypes and grant permissions to superuser.
595      */
596     private void loadRepository(String repositoryNameFromModuleDescriptor, String nodeTypeFile, String[] workspaces) {
597 
598         if (workspaces == null || workspaces.length == 0) {
599             log.error("Trying to register the repository {} without any workspace.", repositoryNameFromModuleDescriptor);
600             return;
601         }
602 
603         final String DEFAULT_REPOSITORY_NAME = "magnolia";
604         String repositoryName = repositoryNameFromModuleDescriptor;
605 
606         if (workspaces.length > 0) {
607             // get the repository name from the mapping, users may want to manually add it here if needed
608             info.magnolia.repository.definition.RepositoryDefinition repositoryMapping = getRepositoryMapping(workspaces[0]);
609             if (repositoryMapping != null) {
610                 repositoryName = repositoryMapping.getName();
611             }
612         }
613 
614         info.magnolia.repository.definition.RepositoryDefinition rm = getRepositoryMapping(repositoryName);
615 
616         if (rm == null) {
617 
618             final info.magnolia.repository.definition.RepositoryDefinition defaultRepositoryMapping = getRepositoryMapping(DEFAULT_REPOSITORY_NAME);
619             final Map<String, String> defaultParameters = defaultRepositoryMapping.getParameters();
620 
621             rm = new info.magnolia.repository.definition.RepositoryDefinition();
622             rm.setName(repositoryName);
623             rm.setProvider(defaultRepositoryMapping.getProvider());
624             rm.setLoadOnStartup(true);
625 
626             final Map<String, String> parameters = new HashMap<>();
627             parameters.putAll(defaultParameters);
628 
629             // override changed parameters
630             final String bindName = repositoryName + StringUtils.replace(defaultParameters.get("bindName"), "magnolia", "");
631             final String repositoryHome = StringUtils.substringBeforeLast(defaultParameters.get("configFile"), "/")
632                     + "/"
633                     + repositoryName;
634 
635             parameters.put("repositoryHome", repositoryHome);
636             parameters.put("bindName", bindName);
637             parameters.put("customNodeTypes", nodeTypeFile);
638 
639             rm.setParameters(parameters);
640 
641             try {
642                 repositoryManager.loadRepository(rm);
643             } catch (Exception e) {
644                 log.error(e.getMessage(), e);
645             }
646         }
647 
648         if (nodeTypeFile != null) {
649             // register nodetypes
650             registerNodeTypeFile(repositoryName, nodeTypeFile);
651             // if this repo is not the default one, register nodetypes on default repo (MAGNOLIA-3189)
652             if (!DEFAULT_REPOSITORY_NAME.equals(repositoryName)) {
653                 registerNodeTypeFile(DEFAULT_REPOSITORY_NAME, nodeTypeFile);
654             }
655         }
656 
657         if (workspaces != null) {
658             for (String workspace : workspaces) {
659                 if (!rm.getWorkspaces().contains(workspace)) {
660                     log.debug("Loading new workspace: {}", workspace);
661 
662                     try {
663                         repositoryManager.loadWorkspace(repositoryName, workspace);
664                     } catch (RepositoryException e) {
665                         // should never happen, the only exception we can get here is during login
666                         log.error(e.getMessage(), e);
667                     }
668                 }
669             }
670         }
671 
672     }
673 
674     /**
675      * Returns the repository definition for a repository or the repository for a given workspace.
676      */
677     // method copied 1-to-1 from former ContentRepository#getRepositoryMapping
678     // TODO: ditch the terminology mix-up between former repositories and workspaces for good
679     private info.magnolia.repository.definition.RepositoryDefinition getRepositoryMapping(String repositoryOrLogicalWorkspace) {
680         if (!repositoryManager.hasRepository(repositoryOrLogicalWorkspace)) {
681             WorkspaceMappingDefinition mapping = repositoryManager.getWorkspaceMapping(repositoryOrLogicalWorkspace);
682             repositoryOrLogicalWorkspace = mapping != null ? mapping.getRepositoryName() : repositoryOrLogicalWorkspace;
683         }
684         return repositoryManager.getRepositoryDefinition(repositoryOrLogicalWorkspace);
685     }
686 
687     /**
688      * Register nodeType file in repository.
689      *
690      * @param repositoryName repository name
691      * @param nodeTypeFile nodeType file
692      */
693     private void registerNodeTypeFile(String repositoryName, String nodeTypeFile) {
694         Provider provider = repositoryManager.getRepositoryProvider(repositoryName);
695         try {
696             provider.registerNodeTypes(nodeTypeFile);
697         } catch (RepositoryException e) {
698             log.error(e.getMessage(), e);
699         }
700     }
701 }