001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.module;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.configuration.CmsConfigurationManager;
032import org.opencms.configuration.CmsModuleConfiguration;
033import org.opencms.db.CmsExportPoint;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsProject;
036import org.opencms.file.CmsResource;
037import org.opencms.i18n.CmsMessageContainer;
038import org.opencms.importexport.CmsImportExportManager;
039import org.opencms.importexport.CmsImportParameters;
040import org.opencms.lock.CmsLock;
041import org.opencms.lock.CmsLockException;
042import org.opencms.lock.CmsLockFilter;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsIllegalArgumentException;
045import org.opencms.main.CmsIllegalStateException;
046import org.opencms.main.CmsLog;
047import org.opencms.main.CmsRuntimeException;
048import org.opencms.main.OpenCms;
049import org.opencms.report.I_CmsReport;
050import org.opencms.security.CmsRole;
051import org.opencms.security.CmsRoleViolationException;
052import org.opencms.security.CmsSecurityException;
053import org.opencms.util.CmsStringUtil;
054
055import java.io.File;
056import java.util.ArrayList;
057import java.util.Collections;
058import java.util.HashMap;
059import java.util.HashSet;
060import java.util.Hashtable;
061import java.util.Iterator;
062import java.util.List;
063import java.util.Map;
064import java.util.Optional;
065import java.util.Set;
066
067import org.apache.commons.logging.Log;
068
069/**
070 * Manages the modules of an OpenCms installation.<p>
071 *
072 * @since 6.0.0
073 */
074public class CmsModuleManager {
075
076    /** Indicates dependency check for module deletion. */
077    public static final int DEPENDENCY_MODE_DELETE = 0;
078
079    /** Indicates dependency check for module import. */
080    public static final int DEPENDENCY_MODE_IMPORT = 1;
081
082    /** The log object for this class. */
083    private static final Log LOG = CmsLog.getLog(CmsModuleManager.class);
084
085    /** The import/export repository. */
086    private CmsModuleImportExportRepository m_importExportRepository = new CmsModuleImportExportRepository();
087
088    /** The list of module export points. */
089    private Set<CmsExportPoint> m_moduleExportPoints;
090
091    /** The map of configured modules. */
092    private Map<String, CmsModule> m_modules;
093
094    /**
095     * Basic constructor.<p>
096     *
097     * @param configuredModules the list of configured modules
098     */
099    public CmsModuleManager(List<CmsModule> configuredModules) {
100
101        if (CmsLog.INIT.isInfoEnabled()) {
102            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_MOD_MANAGER_CREATED_0));
103        }
104
105        m_modules = new Hashtable<String, CmsModule>();
106        for (int i = 0; i < configuredModules.size(); i++) {
107            CmsModule module = configuredModules.get(i);
108            m_modules.put(module.getName(), module);
109            if (CmsLog.INIT.isInfoEnabled()) {
110                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_MOD_CONFIGURED_1, module.getName()));
111            }
112        }
113
114        if (CmsLog.INIT.isInfoEnabled()) {
115            CmsLog.INIT.info(
116                Messages.get().getBundle().key(Messages.INIT_NUM_MODS_CONFIGURED_1, new Integer(m_modules.size())));
117        }
118        m_moduleExportPoints = Collections.emptySet();
119    }
120
121    /**
122     * Returns a map of dependencies.<p>
123     *
124     * The module dependencies are get from the installed modules or
125     * from the module manifest.xml files found in the given FRS path.<p>
126     *
127     * Two types of dependency lists can be generated:<br>
128     * <ul>
129     *   <li>Forward dependency lists: a list of modules that depends on a module</li>
130     *   <li>Backward dependency lists: a list of modules that a module depends on</li>
131     * </ul>
132     *
133     * @param rfsAbsPath a RFS absolute path to search for modules, or <code>null</code> to use the installed modules
134     * @param mode if <code>true</code> a list of forward dependency is build, is not a list of backward dependency
135     *
136     * @return a Map of module names as keys and a list of dependency names as values
137     *
138     * @throws CmsConfigurationException if something goes wrong
139     */
140    public static Map<String, List<String>> buildDepsForAllModules(String rfsAbsPath, boolean mode)
141    throws CmsConfigurationException {
142
143        Map<String, List<String>> ret = new HashMap<String, List<String>>();
144        List<CmsModule> modules;
145        if (rfsAbsPath == null) {
146            modules = OpenCms.getModuleManager().getAllInstalledModules();
147        } else {
148            modules = new ArrayList<CmsModule>(getAllModulesFromPath(rfsAbsPath).keySet());
149        }
150        Iterator<CmsModule> itMods = modules.iterator();
151        while (itMods.hasNext()) {
152            CmsModule module = itMods.next();
153
154            // if module a depends on module b, and module c depends also on module b:
155            // build a map with a list containing "a" and "c" keyed by "b" to get a
156            // list of modules depending on module "b"...
157            Iterator<CmsModuleDependency> itDeps = module.getDependencies().iterator();
158            while (itDeps.hasNext()) {
159                CmsModuleDependency dependency = itDeps.next();
160                // module dependency package name
161                String moduleDependencyName = dependency.getName();
162
163                if (mode) {
164                    // get the list of dependent modules
165                    List<String> moduleDependencies = ret.get(moduleDependencyName);
166                    if (moduleDependencies == null) {
167                        // build a new list if "b" has no dependent modules yet
168                        moduleDependencies = new ArrayList<String>();
169                        ret.put(moduleDependencyName, moduleDependencies);
170                    }
171                    // add "a" as a module depending on "b"
172                    moduleDependencies.add(module.getName());
173                } else {
174                    List<String> moduleDependencies = ret.get(module.getName());
175                    if (moduleDependencies == null) {
176                        moduleDependencies = new ArrayList<String>();
177                        ret.put(module.getName(), moduleDependencies);
178                    }
179                    moduleDependencies.add(dependency.getName());
180                }
181            }
182        }
183        itMods = modules.iterator();
184        while (itMods.hasNext()) {
185            CmsModule module = itMods.next();
186            if (ret.get(module.getName()) == null) {
187                ret.put(module.getName(), new ArrayList<String>());
188            }
189        }
190        return ret;
191    }
192
193    /**
194     * Returns a map of dependencies between the given modules.<p>
195     *
196     * The module dependencies are get from the installed modules or
197     * from the module manifest.xml files found in the given FRS path.<p>
198     *
199     * Two types of dependency lists can be generated:<br>
200     * <ul>
201     *   <li>Forward dependency lists: a list of modules that depends on a module</li>
202     *   <li>Backward dependency lists: a list of modules that a module depends on</li>
203     * </ul>
204     *
205     * @param moduleNames a list of module names
206     * @param rfsAbsPath a RFS absolute path to search for modules, or <code>null</code> to use the installed modules
207     * @param mode if <code>true</code> a list of forward dependency is build, is not a list of backward dependency
208     *
209     * @return a Map of module names as keys and a list of dependency names as values
210     *
211     * @throws CmsConfigurationException if something goes wrong
212     */
213    public static Map<String, List<String>> buildDepsForModulelist(
214        List<String> moduleNames,
215        String rfsAbsPath,
216        boolean mode)
217    throws CmsConfigurationException {
218
219        Map<String, List<String>> ret = buildDepsForAllModules(rfsAbsPath, mode);
220        Iterator<CmsModule> itMods;
221        if (rfsAbsPath == null) {
222            itMods = OpenCms.getModuleManager().getAllInstalledModules().iterator();
223        } else {
224            itMods = getAllModulesFromPath(rfsAbsPath).keySet().iterator();
225        }
226        while (itMods.hasNext()) {
227            CmsModule module = itMods.next();
228            if (!moduleNames.contains(module.getName())) {
229                Iterator<List<String>> itDeps = ret.values().iterator();
230                while (itDeps.hasNext()) {
231                    List<String> dependencies = itDeps.next();
232                    dependencies.remove(module.getName());
233                }
234                ret.remove(module.getName());
235            }
236        }
237        return ret;
238    }
239
240    /**
241     * Returns a map of modules found in the given RFS absolute path.<p>
242     *
243     * @param rfsAbsPath the path to look for module distributions
244     *
245     * @return a map of <code>{@link CmsModule}</code> objects for keys and filename for values
246     *
247     * @throws CmsConfigurationException if something goes wrong
248     */
249    public static Map<CmsModule, String> getAllModulesFromPath(String rfsAbsPath) throws CmsConfigurationException {
250
251        Map<CmsModule, String> modules = new HashMap<CmsModule, String>();
252        if (rfsAbsPath == null) {
253            return modules;
254        }
255        File folder = new File(rfsAbsPath);
256        if (folder.exists()) {
257            // list all child resources in the given folder
258            File[] folderFiles = folder.listFiles();
259            if (folderFiles != null) {
260                for (int i = 0; i < folderFiles.length; i++) {
261                    File moduleFile = folderFiles[i];
262                    if (moduleFile.isFile() && !(moduleFile.getAbsolutePath().toLowerCase().endsWith(".zip"))) {
263                        // skip non-ZIP files
264                        continue;
265                    }
266                    if (moduleFile.isDirectory()) {
267                        File manifest = new File(moduleFile, CmsImportExportManager.EXPORT_MANIFEST);
268                        if (!manifest.exists() || !manifest.canRead()) {
269                            // skip unused directories
270                            continue;
271                        }
272                    }
273                    modules.put(
274                        CmsModuleImportExportHandler.readModuleFromImport(moduleFile.getAbsolutePath()),
275                        moduleFile.getName());
276                }
277            }
278        }
279        return modules;
280    }
281
282    /**
283     * Sorts a given list of module names by dependencies,
284     * so that the resulting list can be imported in that given order,
285     * that means modules without dependencies first.<p>
286     *
287     * The module dependencies are get from the installed modules or
288     * from the module manifest.xml files found in the given FRS path.<p>
289     *
290     * @param moduleNames a list of module names
291     * @param rfsAbsPath a RFS absolute path to search for modules, or <code>null</code> to use the installed modules
292     *
293     * @return a sorted list of module names
294     *
295     * @throws CmsConfigurationException if something goes wrong
296     */
297    public static List<String> topologicalSort(List<String> moduleNames, String rfsAbsPath)
298    throws CmsConfigurationException {
299
300        List<String> modules = new ArrayList<String>(moduleNames);
301        List<String> retList = new ArrayList<String>();
302        Map<String, List<String>> moduleDependencies = buildDepsForModulelist(moduleNames, rfsAbsPath, true);
303        boolean finished = false;
304        while (!finished) {
305            finished = true;
306            Iterator<String> itMods = modules.iterator();
307            while (itMods.hasNext()) {
308                String moduleName = itMods.next();
309                List<String> deps = moduleDependencies.get(moduleName);
310                if ((deps == null) || deps.isEmpty()) {
311                    retList.add(moduleName);
312                    Iterator<List<String>> itDeps = moduleDependencies.values().iterator();
313                    while (itDeps.hasNext()) {
314                        List<String> dependencies = itDeps.next();
315                        dependencies.remove(moduleName);
316                    }
317                    finished = false;
318                    itMods.remove();
319                }
320            }
321        }
322        if (!modules.isEmpty()) {
323            throw new CmsIllegalStateException(
324                Messages.get().container(Messages.ERR_MODULE_DEPENDENCY_CYCLE_1, modules.toString()));
325        }
326        Collections.reverse(retList);
327        return retList;
328    }
329
330    /**
331     * Adds a new module to the module manager.<p>
332     *
333     * @param cms must be initialized with "Admin" permissions
334     * @param module the module to add
335     *
336     * @throws CmsSecurityException if the required permissions are not available (i.e. no "Admin" CmsObject has been provided)
337     * @throws CmsConfigurationException if a module with this name is already configured
338     */
339    public synchronized void addModule(CmsObject cms, CmsModule module)
340    throws CmsSecurityException, CmsConfigurationException {
341
342        // check the role permissions
343        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
344
345        if (m_modules.containsKey(module.getName())) {
346            // module is currently configured, no create possible
347            throw new CmsConfigurationException(
348                Messages.get().container(Messages.ERR_MODULE_ALREADY_CONFIGURED_1, module.getName()));
349
350        }
351
352        if (LOG.isInfoEnabled()) {
353            LOG.info(Messages.get().getBundle().key(Messages.LOG_CREATE_NEW_MOD_1, module.getName()));
354        }
355
356        // initialize the module
357        module.initialize(cms);
358
359        m_modules.put(module.getName(), module);
360
361        try {
362            I_CmsModuleAction moduleAction = module.getActionInstance();
363            String className = module.getActionClass();
364            if ((moduleAction == null) && (className != null)) {
365                Class<?> actionClass = Class.forName(className, false, getClass().getClassLoader());
366                if (I_CmsModuleAction.class.isAssignableFrom(actionClass)) {
367                    moduleAction = ((Class<? extends I_CmsModuleAction>)actionClass).newInstance();
368                    module.setActionInstance(moduleAction);
369                }
370            }
371            // handle module action instance if initialized
372            if (moduleAction != null) {
373
374                moduleAction.moduleUpdate(module);
375            }
376        } catch (Throwable t) {
377            LOG.error(Messages.get().getBundle().key(Messages.LOG_MOD_UPDATE_ERR_1, module.getName()), t);
378        }
379
380        // initialize the export points
381        initModuleExportPoints();
382
383        // update the configuration
384        updateModuleConfiguration();
385
386        // reinit the workplace CSS URIs
387        if (!module.getParameters().isEmpty()) {
388            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
389        }
390    }
391
392    /**
393     * Checks if a modules dependencies are fulfilled.<p>
394     *
395     * The possible values for the <code>mode</code> parameter are:<dl>
396     * <dt>{@link #DEPENDENCY_MODE_DELETE}</dt>
397     *      <dd>Check for module deleting, i.e. are other modules dependent on the
398     *          given module?</dd>
399     * <dt>{@link #DEPENDENCY_MODE_IMPORT}</dt>
400     *      <dd>Check for module importing, i.e. are all dependencies required by the given
401     *          module available?</dd></dl>
402     *
403     * @param module the module to check the dependencies for
404     * @param mode the dependency check mode
405     * @return a list of dependencies that are not fulfilled, if empty all dependencies are fulfilled
406     */
407    public List<CmsModuleDependency> checkDependencies(CmsModule module, int mode) {
408
409        List<CmsModuleDependency> result = new ArrayList<CmsModuleDependency>();
410
411        if (mode == DEPENDENCY_MODE_DELETE) {
412            // delete mode, check if other modules depend on this module
413            Iterator<CmsModule> i = m_modules.values().iterator();
414            while (i.hasNext()) {
415                CmsModule otherModule = i.next();
416                CmsModuleDependency dependency = otherModule.checkDependency(module);
417                if (dependency != null) {
418                    // dependency found, add to list
419                    result.add(new CmsModuleDependency(otherModule.getName(), otherModule.getVersion()));
420                }
421            }
422
423        } else if (mode == DEPENDENCY_MODE_IMPORT) {
424            // import mode, check if all module dependencies are fulfilled
425            Iterator<CmsModule> i = m_modules.values().iterator();
426            // add all dependencies that must be found
427            result.addAll(module.getDependencies());
428            while (i.hasNext() && (result.size() > 0)) {
429                CmsModule otherModule = i.next();
430                CmsModuleDependency dependency = module.checkDependency(otherModule);
431                if (dependency != null) {
432                    // dependency found, remove from list
433                    result.remove(dependency);
434                }
435            }
436        } else {
437            // invalid mode selected
438            throw new CmsRuntimeException(
439                Messages.get().container(Messages.ERR_CHECK_DEPENDENCY_INVALID_MODE_1, new Integer(mode)));
440        }
441
442        return result;
443    }
444
445    /**
446     * Checks the module selection list for consistency, that means
447     * that if a module is selected, all its dependencies are also selected.<p>
448     *
449     * The module dependencies are get from the installed modules or
450     * from the module manifest.xml files found in the given FRS path.<p>
451     *
452     * @param moduleNames a list of module names
453     * @param rfsAbsPath a RFS absolute path to search for modules, or <code>null</code> to use the installed modules
454     * @param forDeletion there are two modes, one for installation of modules, and one for deletion.
455     *
456     * @throws CmsIllegalArgumentException if the module list is not consistent
457     * @throws CmsConfigurationException if something goes wrong
458     */
459    public void checkModuleSelectionList(List<String> moduleNames, String rfsAbsPath, boolean forDeletion)
460    throws CmsIllegalArgumentException, CmsConfigurationException {
461
462        Map<String, List<String>> moduleDependencies = buildDepsForAllModules(rfsAbsPath, forDeletion);
463        Iterator<String> itMods = moduleNames.iterator();
464        while (itMods.hasNext()) {
465            String moduleName = itMods.next();
466            List<String> dependencies = moduleDependencies.get(moduleName);
467            if (dependencies != null) {
468                List<String> depModules = new ArrayList<String>(dependencies);
469                depModules.removeAll(moduleNames);
470                if (!depModules.isEmpty()) {
471                    throw new CmsIllegalArgumentException(
472                        Messages.get().container(
473                            Messages.ERR_MODULE_SELECTION_INCONSISTENT_2,
474                            moduleName,
475                            depModules.toString()));
476                }
477            }
478        }
479    }
480
481    /**
482     * Deletes a module from the configuration.<p>
483     *
484     * @param cms must be initialized with "Admin" permissions
485     * @param moduleName the name of the module to delete
486     * @param replace indicates if the module is replaced (true) or finally deleted (false)
487     * @param preserveLibs <code>true</code> to keep any exported file exported into the WEB-INF lib folder
488     * @param report the report to print progress messages to
489     *
490     * @throws CmsRoleViolationException if the required module manager role permissions are not available
491     * @throws CmsConfigurationException if a module with this name is not available for deleting
492     * @throws CmsLockException if the module resources can not be locked
493     */
494    public synchronized void deleteModule(
495        CmsObject cms,
496        String moduleName,
497        boolean replace,
498        boolean preserveLibs,
499        I_CmsReport report)
500    throws CmsRoleViolationException, CmsConfigurationException, CmsLockException {
501
502        // check for module manager role permissions
503        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
504
505        if (!m_modules.containsKey(moduleName)) {
506            // module is not currently configured, no update possible
507            throw new CmsConfigurationException(
508                Messages.get().container(Messages.ERR_MODULE_NOT_CONFIGURED_1, moduleName));
509        }
510
511        if (LOG.isInfoEnabled()) {
512            LOG.info(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_1, moduleName));
513        }
514
515        CmsModule module = m_modules.get(moduleName);
516        String importSite = module.getSite();
517        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(importSite)) {
518            CmsObject newCms;
519            try {
520                newCms = OpenCms.initCmsObject(cms);
521                newCms.getRequestContext().setSiteRoot(importSite);
522                cms = newCms;
523            } catch (CmsException e) {
524                LOG.error(e.getLocalizedMessage(), e);
525            }
526        }
527
528        if (!replace) {
529            // module is deleted, not replaced
530
531            // perform dependency check
532            List<CmsModuleDependency> dependencies = checkDependencies(module, DEPENDENCY_MODE_DELETE);
533            if (!dependencies.isEmpty()) {
534                StringBuffer message = new StringBuffer();
535                Iterator<CmsModuleDependency> it = dependencies.iterator();
536                while (it.hasNext()) {
537                    message.append("  ").append(it.next().getName()).append("\r\n");
538                }
539                throw new CmsConfigurationException(
540                    Messages.get().container(Messages.ERR_MOD_DEPENDENCIES_2, moduleName, message.toString()));
541            }
542            try {
543                I_CmsModuleAction moduleAction = module.getActionInstance();
544                // handle module action instance if initialized
545                if (moduleAction != null) {
546                    moduleAction.moduleUninstall(module);
547                }
548            } catch (Throwable t) {
549                LOG.error(Messages.get().getBundle().key(Messages.LOG_MOD_UNINSTALL_ERR_1, moduleName), t);
550                report.println(
551                    Messages.get().container(Messages.LOG_MOD_UNINSTALL_ERR_1, moduleName),
552                    I_CmsReport.FORMAT_WARNING);
553            }
554        }
555
556        boolean removeResourceTypes = !module.getResourceTypes().isEmpty();
557        if (removeResourceTypes) {
558            // mark the resource manager to reinitialize if necessary
559            OpenCms.getWorkplaceManager().removeExplorerTypeSettings(module);
560        }
561
562        CmsProject previousProject = cms.getRequestContext().getCurrentProject();
563        // try to create a new offline project for deletion
564        CmsProject deleteProject = null;
565        try {
566            // try to read a (leftover) module delete project
567            deleteProject = cms.readProject(
568                Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
569                    Messages.GUI_DELETE_MODULE_PROJECT_NAME_1,
570                    new Object[] {moduleName}));
571        } catch (CmsException e) {
572            try {
573                // create a Project to delete the module
574                deleteProject = cms.createProject(
575                    Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
576                        Messages.GUI_DELETE_MODULE_PROJECT_NAME_1,
577                        new Object[] {moduleName}),
578                    Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
579                        Messages.GUI_DELETE_MODULE_PROJECT_DESC_1,
580                        new Object[] {moduleName}),
581                    OpenCms.getDefaultUsers().getGroupAdministrators(),
582                    OpenCms.getDefaultUsers().getGroupAdministrators(),
583                    CmsProject.PROJECT_TYPE_TEMPORARY);
584            } catch (CmsException e1) {
585                throw new CmsConfigurationException(e1.getMessageContainer(), e1);
586            }
587        }
588
589        try {
590            cms.getRequestContext().setCurrentProject(deleteProject);
591
592            // check locks
593            List<String> lockedResources = new ArrayList<String>();
594            CmsLockFilter filter1 = CmsLockFilter.FILTER_ALL.filterNotLockableByUser(
595                cms.getRequestContext().getCurrentUser());
596            CmsLockFilter filter2 = CmsLockFilter.FILTER_INHERITED;
597            List<String> moduleResources = module.getResources();
598            for (int iLock = 0; iLock < moduleResources.size(); iLock++) {
599                String resourceName = moduleResources.get(iLock);
600                try {
601                    lockedResources.addAll(cms.getLockedResources(resourceName, filter1));
602                    lockedResources.addAll(cms.getLockedResources(resourceName, filter2));
603                } catch (CmsException e) {
604                    // may happen if the resource has already been deleted
605                    if (LOG.isDebugEnabled()) {
606                        LOG.debug(e.getMessageContainer(), e);
607                    }
608                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
609                }
610            }
611            if (!lockedResources.isEmpty()) {
612                CmsMessageContainer msg = Messages.get().container(
613                    Messages.ERR_DELETE_MODULE_CHECK_LOCKS_2,
614                    moduleName,
615                    CmsStringUtil.collectionAsString(lockedResources, ","));
616                report.addError(msg.key(cms.getRequestContext().getLocale()));
617                report.println(msg);
618                cms.getRequestContext().setCurrentProject(previousProject);
619                try {
620                    cms.deleteProject(deleteProject.getUuid());
621                } catch (CmsException e1) {
622                    throw new CmsConfigurationException(e1.getMessageContainer(), e1);
623                }
624                throw new CmsLockException(msg);
625            }
626        } finally {
627            cms.getRequestContext().setCurrentProject(previousProject);
628        }
629
630        // now remove the module
631        module = m_modules.remove(moduleName);
632
633        if (preserveLibs) {
634            // to preserve the module libs, remove the responsible export points, before deleting module resources
635            Set<CmsExportPoint> exportPoints = new HashSet<CmsExportPoint>(m_moduleExportPoints);
636            Iterator<CmsExportPoint> it = exportPoints.iterator();
637            while (it.hasNext()) {
638                CmsExportPoint point = it.next();
639                if ((point.getUri().endsWith(module.getName() + "/lib/")
640                    || point.getUri().endsWith(module.getName() + "/lib"))
641                    && point.getConfiguredDestination().equals("WEB-INF/lib/")) {
642                    it.remove();
643                }
644            }
645
646            m_moduleExportPoints = Collections.unmodifiableSet(exportPoints);
647        }
648
649        try {
650            cms.getRequestContext().setCurrentProject(deleteProject);
651
652            // copy the module resources to the project
653            List<CmsResource> moduleResources = CmsModule.calculateModuleResources(cms, module);
654            for (CmsResource resource : moduleResources) {
655                try {
656                    cms.copyResourceToProject(resource);
657                } catch (CmsException e) {
658                    // may happen if the resource has already been deleted
659                    if (LOG.isDebugEnabled()) {
660                        LOG.debug(
661                            Messages.get().getBundle().key(
662                                Messages.LOG_MOVE_RESOURCE_FAILED_1,
663                                cms.getSitePath(resource)));
664                    }
665                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
666                }
667            }
668
669            report.print(Messages.get().container(Messages.RPT_DELETE_MODULE_BEGIN_0), I_CmsReport.FORMAT_HEADLINE);
670            report.println(
671                org.opencms.report.Messages.get().container(
672                    org.opencms.report.Messages.RPT_ARGUMENT_HTML_ITAG_1,
673                    moduleName),
674                I_CmsReport.FORMAT_HEADLINE);
675
676            // move through all module resources and delete them
677            for (CmsResource resource : moduleResources) {
678                String sitePath = cms.getSitePath(resource);
679                try {
680                    if (LOG.isDebugEnabled()) {
681                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_RESOURCE_1, sitePath));
682                    }
683                    CmsLock lock = cms.getLock(resource);
684                    if (lock.isUnlocked()) {
685                        // lock the resource
686                        cms.lockResource(resource);
687                    } else if (lock.isLockableBy(cms.getRequestContext().getCurrentUser())) {
688                        // steal the resource
689                        cms.changeLock(resource);
690                    }
691                    if (!resource.getState().isDeleted()) {
692                        // delete the resource
693                        cms.deleteResource(sitePath, CmsResource.DELETE_PRESERVE_SIBLINGS);
694                    }
695                    // update the report
696                    report.print(Messages.get().container(Messages.RPT_DELETE_0), I_CmsReport.FORMAT_NOTE);
697                    report.println(
698                        org.opencms.report.Messages.get().container(
699                            org.opencms.report.Messages.RPT_ARGUMENT_1,
700                            sitePath));
701                    if (!resource.getState().isNew()) {
702                        // unlock the resource (so it gets deleted with next publish)
703                        cms.unlockResource(resource);
704                    }
705                } catch (CmsException e) {
706                    // ignore the exception and delete the next resource
707                    LOG.error(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_EXC_1, sitePath), e);
708                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
709                }
710            }
711
712            report.println(Messages.get().container(Messages.RPT_PUBLISH_PROJECT_BEGIN_0), I_CmsReport.FORMAT_HEADLINE);
713
714            // now unlock and publish the project
715            cms.unlockProject(deleteProject.getUuid());
716            OpenCms.getPublishManager().publishProject(cms, report);
717            OpenCms.getPublishManager().waitWhileRunning();
718
719            report.println(Messages.get().container(Messages.RPT_PUBLISH_PROJECT_END_0), I_CmsReport.FORMAT_HEADLINE);
720            report.println(Messages.get().container(Messages.RPT_DELETE_MODULE_END_0), I_CmsReport.FORMAT_HEADLINE);
721        } catch (CmsException e) {
722            throw new CmsConfigurationException(e.getMessageContainer(), e);
723        } finally {
724            cms.getRequestContext().setCurrentProject(previousProject);
725        }
726
727        // initialize the export points (removes export points from deleted module)
728        initModuleExportPoints();
729
730        // update the configuration
731        updateModuleConfiguration();
732
733        // reinit the manager is necessary
734        if (removeResourceTypes) {
735            OpenCms.getResourceManager().initialize(cms);
736        }
737
738        // reinit the workplace CSS URIs
739        if (!module.getParameters().isEmpty()) {
740            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
741        }
742    }
743
744    /**
745     * Deletes a module from the configuration.<p>
746     *
747     * @param cms must be initialized with "Admin" permissions
748     * @param moduleName the name of the module to delete
749     * @param replace indicates if the module is replaced (true) or finally deleted (false)
750     * @param report the report to print progress messages to
751     *
752     * @throws CmsRoleViolationException if the required module manager role permissions are not available
753     * @throws CmsConfigurationException if a module with this name is not available for deleting
754     * @throws CmsLockException if the module resources can not be locked
755     */
756    public synchronized void deleteModule(CmsObject cms, String moduleName, boolean replace, I_CmsReport report)
757    throws CmsRoleViolationException, CmsConfigurationException, CmsLockException {
758
759        deleteModule(cms, moduleName, replace, false, report);
760    }
761
762    /**
763     * Returns a list of installed modules.<p>
764     *
765     * @return a list of <code>{@link CmsModule}</code> objects
766     */
767    public List<CmsModule> getAllInstalledModules() {
768
769        return new ArrayList<CmsModule>(m_modules.values());
770    }
771
772    /**
773     * Returns the (immutable) list of configured module export points.<p>
774     *
775     * @return the (immutable) list of configured module export points
776     * @see CmsExportPoint
777     */
778    public Set<CmsExportPoint> getExportPoints() {
779
780        return m_moduleExportPoints;
781    }
782
783    /**
784     * Returns the importExportRepository.<p>
785     *
786     * @return the importExportRepository
787     */
788    public CmsModuleImportExportRepository getImportExportRepository() {
789
790        return m_importExportRepository;
791    }
792
793    /**
794     * Returns the module with the given module name,
795     * or <code>null</code> if no module with the given name is configured.<p>
796     *
797     * @param name the name of the module to return
798     * @return the module with the given module name
799     */
800    public CmsModule getModule(String name) {
801
802        return m_modules.get(name);
803    }
804
805    /**
806     * Returns the set of names of all the installed modules.<p>
807     *
808     * @return the set of names of all the installed modules
809     */
810    public Set<String> getModuleNames() {
811
812        synchronized (m_modules) {
813            return new HashSet<String>(m_modules.keySet());
814        }
815    }
816
817    /**
818     * Checks if this module manager has a module with the given name installed.<p>
819     *
820     * @param name the name of the module to check
821     * @return true if this module manager has a module with the given name installed
822     */
823    public boolean hasModule(String name) {
824
825        return m_modules.containsKey(name);
826    }
827
828    /**
829     * Initializes all module instance classes managed in this module manager.<p>
830     *
831     * @param cms an initialized CmsObject with "manage modules" role permissions
832     * @param configurationManager the initialized OpenCms configuration manager
833     *
834     * @throws CmsRoleViolationException if the provided OpenCms context does not have "manage modules" role permissions
835     */
836    public synchronized void initialize(CmsObject cms, CmsConfigurationManager configurationManager)
837    throws CmsRoleViolationException {
838
839        if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
840            // certain test cases won't have an OpenCms context
841            OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
842        }
843
844        Iterator<String> it;
845        int count = 0;
846        it = m_modules.keySet().iterator();
847        while (it.hasNext()) {
848            // get the module description
849            CmsModule module = m_modules.get(it.next());
850
851            if (module.getActionClass() != null) {
852                // create module instance class
853                I_CmsModuleAction moduleAction = module.getActionInstance();
854                if (module.getActionClass() != null) {
855                    try {
856                        moduleAction = (I_CmsModuleAction)Class.forName(module.getActionClass()).newInstance();
857                    } catch (Exception e) {
858                        CmsLog.INIT.info(
859                            Messages.get().getBundle().key(Messages.INIT_CREATE_INSTANCE_FAILED_1, module.getName()),
860                            e);
861                    }
862                }
863                if (moduleAction != null) {
864                    count++;
865                    module.setActionInstance(moduleAction);
866                    if (CmsLog.INIT.isInfoEnabled()) {
867                        CmsLog.INIT.info(
868                            Messages.get().getBundle().key(
869                                Messages.INIT_INITIALIZE_MOD_CLASS_1,
870                                moduleAction.getClass().getName()));
871                    }
872                    try {
873                        // create a copy of the adminCms so that each module instance does have
874                        // it's own context, a shared context might introduce side - effects
875                        CmsObject adminCmsCopy = OpenCms.initCmsObject(cms);
876                        // initialize the module
877                        moduleAction.initialize(adminCmsCopy, configurationManager, module);
878                    } catch (Throwable t) {
879                        LOG.error(
880                            Messages.get().getBundle().key(
881                                Messages.LOG_INSTANCE_INIT_ERR_1,
882                                moduleAction.getClass().getName()),
883                            t);
884                    }
885                }
886            }
887        }
888
889        // initialize the export points
890        initModuleExportPoints();
891        m_importExportRepository.initialize(cms);
892
893        if (CmsLog.INIT.isInfoEnabled()) {
894            CmsLog.INIT.info(
895                Messages.get().getBundle().key(Messages.INIT_NUM_CLASSES_INITIALIZED_1, new Integer(count)));
896        }
897    }
898
899    /**
900     * Replaces an existing module with the one read from an import ZIP file.<p>
901     *
902     * If there is not already a module with the same name installed, then the module will just be imported normally.
903     *
904     * @param cms the CMS context
905     * @param importFile the import file
906     * @param report the report
907     *
908     * @return the module replacement status
909     * @throws CmsException if something goes wrong
910     */
911    public CmsReplaceModuleInfo replaceModule(CmsObject cms, String importFile, I_CmsReport report)
912    throws CmsException {
913
914        CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(importFile);
915
916        boolean hasModule = hasModule(module.getName());
917        boolean usedNewUpdate = false;
918        if (hasModule) {
919            Optional<CmsModuleUpdater> optModuleUpdater = CmsModuleUpdater.create(cms, importFile, report);
920            if (optModuleUpdater.isPresent()) {
921                usedNewUpdate = true;
922                optModuleUpdater.get().run();
923            } else {
924                deleteModule(cms, module.getName(), true, report);
925                CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
926                OpenCms.getImportExportManager().importData(cms, report, params);
927            }
928
929        } else {
930            CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
931            OpenCms.getImportExportManager().importData(cms, report, params);
932        }
933        return new CmsReplaceModuleInfo(module, usedNewUpdate);
934    }
935
936    /**
937     * Shuts down all module instance classes managed in this module manager.<p>
938     */
939    public synchronized void shutDown() {
940
941        int count = 0;
942        Iterator<String> it = getModuleNames().iterator();
943        while (it.hasNext()) {
944            String moduleName = it.next();
945            // get the module
946            CmsModule module = m_modules.get(moduleName);
947            if (module == null) {
948                continue;
949            }
950            // get the module action instance
951            I_CmsModuleAction moduleAction = module.getActionInstance();
952            if (moduleAction == null) {
953                continue;
954            }
955
956            count++;
957            if (CmsLog.INIT.isInfoEnabled()) {
958                CmsLog.INIT.info(
959                    Messages.get().getBundle().key(
960                        Messages.INIT_SHUTDOWN_MOD_CLASS_1,
961                        moduleAction.getClass().getName()));
962            }
963            try {
964                // shut down the module
965                moduleAction.shutDown(module);
966            } catch (Throwable t) {
967                LOG.error(
968                    Messages.get().getBundle().key(
969                        Messages.LOG_INSTANCE_SHUTDOWN_ERR_1,
970                        moduleAction.getClass().getName()),
971                    t);
972            }
973        }
974
975        if (CmsLog.INIT.isInfoEnabled()) {
976            CmsLog.INIT.info(
977                Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_NUM_MOD_CLASSES_1, new Integer(count)));
978        }
979
980        if (CmsLog.INIT.isInfoEnabled()) {
981            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_1, this.getClass().getName()));
982        }
983    }
984
985    /**
986     * Updates a already configured module with new values.<p>
987     *
988     * @param cms must be initialized with "Admin" permissions
989     * @param module the module to update
990     *
991     * @throws CmsRoleViolationException if the required module manager role permissions are not available
992     * @throws CmsConfigurationException if a module with this name is not available for updating
993     */
994    public synchronized void updateModule(CmsObject cms, CmsModule module)
995    throws CmsRoleViolationException, CmsConfigurationException {
996
997        // check for module manager role permissions
998        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
999
1000        CmsModule oldModule = m_modules.get(module.getName());
1001
1002        if (oldModule == null) {
1003            // module is not currently configured, no update possible
1004            throw new CmsConfigurationException(Messages.get().container(Messages.ERR_OLD_MOD_ERR_1, module.getName()));
1005        }
1006
1007        if (LOG.isInfoEnabled()) {
1008            LOG.info(Messages.get().getBundle().key(Messages.LOG_MOD_UPDATE_1, module.getName()));
1009        }
1010
1011        // indicate that the version number was recently updated
1012        module.getVersion().setUpdated(true);
1013
1014        // initialize (freeze) the module
1015        module.initialize(cms);
1016
1017        // replace old version of module with new version
1018        m_modules.put(module.getName(), module);
1019
1020        try {
1021            I_CmsModuleAction moduleAction = oldModule.getActionInstance();
1022            // handle module action instance if initialized
1023            if (moduleAction != null) {
1024                moduleAction.moduleUpdate(module);
1025                // set the old action instance
1026                // the new action instance will be used after a system restart
1027                module.setActionInstance(moduleAction);
1028            }
1029        } catch (Throwable t) {
1030            LOG.error(Messages.get().getBundle().key(Messages.LOG_INSTANCE_UPDATE_ERR_1, module.getName()), t);
1031        }
1032
1033        // initialize the export points
1034        initModuleExportPoints();
1035
1036        // update the configuration
1037        updateModuleConfiguration();
1038
1039        // reinit the workplace CSS URIs
1040        if (!module.getParameters().isEmpty()) {
1041            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
1042        }
1043    }
1044
1045    /**
1046     * Updates the module configuration.<p>
1047     */
1048    public void updateModuleConfiguration() {
1049
1050        OpenCms.writeConfiguration(CmsModuleConfiguration.class);
1051    }
1052
1053    /**
1054     * Initializes the list of export points from all configured modules.<p>
1055     */
1056    private synchronized void initModuleExportPoints() {
1057
1058        Set<CmsExportPoint> exportPoints = new HashSet<CmsExportPoint>();
1059        Iterator<CmsModule> i = m_modules.values().iterator();
1060        while (i.hasNext()) {
1061            CmsModule module = i.next();
1062            List<CmsExportPoint> moduleExportPoints = module.getExportPoints();
1063            for (int j = 0; j < moduleExportPoints.size(); j++) {
1064                CmsExportPoint point = moduleExportPoints.get(j);
1065                if (exportPoints.contains(point)) {
1066                    if (LOG.isWarnEnabled()) {
1067                        LOG.warn(
1068                            Messages.get().getBundle().key(
1069                                Messages.LOG_DUPLICATE_EXPORT_POINT_2,
1070                                point,
1071                                module.getName()));
1072                    }
1073                } else {
1074                    exportPoints.add(point);
1075                    if (LOG.isDebugEnabled()) {
1076                        LOG.debug(
1077                            Messages.get().getBundle().key(Messages.LOG_ADD_EXPORT_POINT_2, point, module.getName()));
1078                    }
1079                }
1080            }
1081        }
1082        m_moduleExportPoints = Collections.unmodifiableSet(exportPoints);
1083    }
1084}