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            // handle module action instance if initialized
364            if (moduleAction != null) {
365                moduleAction.moduleUpdate(module);
366            }
367        } catch (Throwable t) {
368            LOG.error(Messages.get().getBundle().key(Messages.LOG_MOD_UPDATE_ERR_1, module.getName()), t);
369        }
370
371        // initialize the export points
372        initModuleExportPoints();
373
374        // update the configuration
375        updateModuleConfiguration();
376
377        // reinit the workplace CSS URIs
378        if (!module.getParameters().isEmpty()) {
379            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
380        }
381    }
382
383    /**
384     * Checks if a modules dependencies are fulfilled.<p>
385     *
386     * The possible values for the <code>mode</code> parameter are:<dl>
387     * <dt>{@link #DEPENDENCY_MODE_DELETE}</dt>
388     *      <dd>Check for module deleting, i.e. are other modules dependent on the
389     *          given module?</dd>
390     * <dt>{@link #DEPENDENCY_MODE_IMPORT}</dt>
391     *      <dd>Check for module importing, i.e. are all dependencies required by the given
392     *          module available?</dd></dl>
393     *
394     * @param module the module to check the dependencies for
395     * @param mode the dependency check mode
396     * @return a list of dependencies that are not fulfilled, if empty all dependencies are fulfilled
397     */
398    public List<CmsModuleDependency> checkDependencies(CmsModule module, int mode) {
399
400        List<CmsModuleDependency> result = new ArrayList<CmsModuleDependency>();
401
402        if (mode == DEPENDENCY_MODE_DELETE) {
403            // delete mode, check if other modules depend on this module
404            Iterator<CmsModule> i = m_modules.values().iterator();
405            while (i.hasNext()) {
406                CmsModule otherModule = i.next();
407                CmsModuleDependency dependency = otherModule.checkDependency(module);
408                if (dependency != null) {
409                    // dependency found, add to list
410                    result.add(new CmsModuleDependency(otherModule.getName(), otherModule.getVersion()));
411                }
412            }
413
414        } else if (mode == DEPENDENCY_MODE_IMPORT) {
415            // import mode, check if all module dependencies are fulfilled
416            Iterator<CmsModule> i = m_modules.values().iterator();
417            // add all dependencies that must be found
418            result.addAll(module.getDependencies());
419            while (i.hasNext() && (result.size() > 0)) {
420                CmsModule otherModule = i.next();
421                CmsModuleDependency dependency = module.checkDependency(otherModule);
422                if (dependency != null) {
423                    // dependency found, remove from list
424                    result.remove(dependency);
425                }
426            }
427        } else {
428            // invalid mode selected
429            throw new CmsRuntimeException(
430                Messages.get().container(Messages.ERR_CHECK_DEPENDENCY_INVALID_MODE_1, new Integer(mode)));
431        }
432
433        return result;
434    }
435
436    /**
437     * Checks the module selection list for consistency, that means
438     * that if a module is selected, all its dependencies are also selected.<p>
439     *
440     * The module dependencies are get from the installed modules or
441     * from the module manifest.xml files found in the given FRS path.<p>
442     *
443     * @param moduleNames a list of module names
444     * @param rfsAbsPath a RFS absolute path to search for modules, or <code>null</code> to use the installed modules
445     * @param forDeletion there are two modes, one for installation of modules, and one for deletion.
446     *
447     * @throws CmsIllegalArgumentException if the module list is not consistent
448     * @throws CmsConfigurationException if something goes wrong
449     */
450    public void checkModuleSelectionList(List<String> moduleNames, String rfsAbsPath, boolean forDeletion)
451    throws CmsIllegalArgumentException, CmsConfigurationException {
452
453        Map<String, List<String>> moduleDependencies = buildDepsForAllModules(rfsAbsPath, forDeletion);
454        Iterator<String> itMods = moduleNames.iterator();
455        while (itMods.hasNext()) {
456            String moduleName = itMods.next();
457            List<String> dependencies = moduleDependencies.get(moduleName);
458            if (dependencies != null) {
459                List<String> depModules = new ArrayList<String>(dependencies);
460                depModules.removeAll(moduleNames);
461                if (!depModules.isEmpty()) {
462                    throw new CmsIllegalArgumentException(
463                        Messages.get().container(
464                            Messages.ERR_MODULE_SELECTION_INCONSISTENT_2,
465                            moduleName,
466                            depModules.toString()));
467                }
468            }
469        }
470    }
471
472    /**
473     * Deletes a module from the configuration.<p>
474     *
475     * @param cms must be initialized with "Admin" permissions
476     * @param moduleName the name of the module to delete
477     * @param replace indicates if the module is replaced (true) or finally deleted (false)
478     * @param preserveLibs <code>true</code> to keep any exported file exported into the WEB-INF lib folder
479     * @param report the report to print progress messages to
480     *
481     * @throws CmsRoleViolationException if the required module manager role permissions are not available
482     * @throws CmsConfigurationException if a module with this name is not available for deleting
483     * @throws CmsLockException if the module resources can not be locked
484     */
485    public synchronized void deleteModule(
486        CmsObject cms,
487        String moduleName,
488        boolean replace,
489        boolean preserveLibs,
490        I_CmsReport report)
491    throws CmsRoleViolationException, CmsConfigurationException, CmsLockException {
492
493        // check for module manager role permissions
494        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
495
496        if (!m_modules.containsKey(moduleName)) {
497            // module is not currently configured, no update possible
498            throw new CmsConfigurationException(
499                Messages.get().container(Messages.ERR_MODULE_NOT_CONFIGURED_1, moduleName));
500        }
501
502        if (LOG.isInfoEnabled()) {
503            LOG.info(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_1, moduleName));
504        }
505
506        CmsModule module = m_modules.get(moduleName);
507        String importSite = module.getSite();
508        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(importSite)) {
509            CmsObject newCms;
510            try {
511                newCms = OpenCms.initCmsObject(cms);
512                newCms.getRequestContext().setSiteRoot(importSite);
513                cms = newCms;
514            } catch (CmsException e) {
515                LOG.error(e.getLocalizedMessage(), e);
516            }
517        }
518
519        if (!replace) {
520            // module is deleted, not replaced
521
522            // perform dependency check
523            List<CmsModuleDependency> dependencies = checkDependencies(module, DEPENDENCY_MODE_DELETE);
524            if (!dependencies.isEmpty()) {
525                StringBuffer message = new StringBuffer();
526                Iterator<CmsModuleDependency> it = dependencies.iterator();
527                while (it.hasNext()) {
528                    message.append("  ").append(it.next().getName()).append("\r\n");
529                }
530                throw new CmsConfigurationException(
531                    Messages.get().container(Messages.ERR_MOD_DEPENDENCIES_2, moduleName, message.toString()));
532            }
533            try {
534                I_CmsModuleAction moduleAction = module.getActionInstance();
535                // handle module action instance if initialized
536                if (moduleAction != null) {
537                    moduleAction.moduleUninstall(module);
538                }
539            } catch (Throwable t) {
540                LOG.error(Messages.get().getBundle().key(Messages.LOG_MOD_UNINSTALL_ERR_1, moduleName), t);
541                report.println(
542                    Messages.get().container(Messages.LOG_MOD_UNINSTALL_ERR_1, moduleName),
543                    I_CmsReport.FORMAT_WARNING);
544            }
545        }
546
547        boolean removeResourceTypes = !module.getResourceTypes().isEmpty();
548        if (removeResourceTypes) {
549            // mark the resource manager to reinitialize if necessary
550            OpenCms.getWorkplaceManager().removeExplorerTypeSettings(module);
551        }
552
553        CmsProject previousProject = cms.getRequestContext().getCurrentProject();
554        // try to create a new offline project for deletion
555        CmsProject deleteProject = null;
556        try {
557            // try to read a (leftover) module delete project
558            deleteProject = cms.readProject(
559                Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
560                    Messages.GUI_DELETE_MODULE_PROJECT_NAME_1,
561                    new Object[] {moduleName}));
562        } catch (CmsException e) {
563            try {
564                // create a Project to delete the module
565                deleteProject = cms.createProject(
566                    Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
567                        Messages.GUI_DELETE_MODULE_PROJECT_NAME_1,
568                        new Object[] {moduleName}),
569                    Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
570                        Messages.GUI_DELETE_MODULE_PROJECT_DESC_1,
571                        new Object[] {moduleName}),
572                    OpenCms.getDefaultUsers().getGroupAdministrators(),
573                    OpenCms.getDefaultUsers().getGroupAdministrators(),
574                    CmsProject.PROJECT_TYPE_TEMPORARY);
575            } catch (CmsException e1) {
576                throw new CmsConfigurationException(e1.getMessageContainer(), e1);
577            }
578        }
579
580        try {
581            cms.getRequestContext().setCurrentProject(deleteProject);
582
583            // check locks
584            List<String> lockedResources = new ArrayList<String>();
585            CmsLockFilter filter1 = CmsLockFilter.FILTER_ALL.filterNotLockableByUser(
586                cms.getRequestContext().getCurrentUser());
587            CmsLockFilter filter2 = CmsLockFilter.FILTER_INHERITED;
588            List<String> moduleResources = module.getResources();
589            for (int iLock = 0; iLock < moduleResources.size(); iLock++) {
590                String resourceName = moduleResources.get(iLock);
591                try {
592                    lockedResources.addAll(cms.getLockedResources(resourceName, filter1));
593                    lockedResources.addAll(cms.getLockedResources(resourceName, filter2));
594                } catch (CmsException e) {
595                    // may happen if the resource has already been deleted
596                    if (LOG.isDebugEnabled()) {
597                        LOG.debug(e.getMessageContainer(), e);
598                    }
599                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
600                }
601            }
602            if (!lockedResources.isEmpty()) {
603                CmsMessageContainer msg = Messages.get().container(
604                    Messages.ERR_DELETE_MODULE_CHECK_LOCKS_2,
605                    moduleName,
606                    CmsStringUtil.collectionAsString(lockedResources, ","));
607                report.addError(msg.key(cms.getRequestContext().getLocale()));
608                report.println(msg);
609                cms.getRequestContext().setCurrentProject(previousProject);
610                try {
611                    cms.deleteProject(deleteProject.getUuid());
612                } catch (CmsException e1) {
613                    throw new CmsConfigurationException(e1.getMessageContainer(), e1);
614                }
615                throw new CmsLockException(msg);
616            }
617        } finally {
618            cms.getRequestContext().setCurrentProject(previousProject);
619        }
620
621        // now remove the module
622        module = m_modules.remove(moduleName);
623
624        if (preserveLibs) {
625            // to preserve the module libs, remove the responsible export points, before deleting module resources
626            Set<CmsExportPoint> exportPoints = new HashSet<CmsExportPoint>(m_moduleExportPoints);
627            Iterator<CmsExportPoint> it = exportPoints.iterator();
628            while (it.hasNext()) {
629                CmsExportPoint point = it.next();
630                if ((point.getUri().endsWith(module.getName() + "/lib/")
631                    || point.getUri().endsWith(module.getName() + "/lib"))
632                    && point.getConfiguredDestination().equals("WEB-INF/lib/")) {
633                    it.remove();
634                }
635            }
636
637            m_moduleExportPoints = Collections.unmodifiableSet(exportPoints);
638        }
639
640        try {
641            cms.getRequestContext().setCurrentProject(deleteProject);
642
643            // copy the module resources to the project
644            List<CmsResource> moduleResources = CmsModule.calculateModuleResources(cms, module);
645            for (CmsResource resource : moduleResources) {
646                try {
647                    cms.copyResourceToProject(resource);
648                } catch (CmsException e) {
649                    // may happen if the resource has already been deleted
650                    if (LOG.isDebugEnabled()) {
651                        LOG.debug(
652                            Messages.get().getBundle().key(
653                                Messages.LOG_MOVE_RESOURCE_FAILED_1,
654                                cms.getSitePath(resource)));
655                    }
656                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
657                }
658            }
659
660            report.print(Messages.get().container(Messages.RPT_DELETE_MODULE_BEGIN_0), I_CmsReport.FORMAT_HEADLINE);
661            report.println(
662                org.opencms.report.Messages.get().container(
663                    org.opencms.report.Messages.RPT_ARGUMENT_HTML_ITAG_1,
664                    moduleName),
665                I_CmsReport.FORMAT_HEADLINE);
666
667            // move through all module resources and delete them
668            for (CmsResource resource : moduleResources) {
669                String sitePath = cms.getSitePath(resource);
670                try {
671                    if (LOG.isDebugEnabled()) {
672                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_RESOURCE_1, sitePath));
673                    }
674                    CmsLock lock = cms.getLock(resource);
675                    if (lock.isUnlocked()) {
676                        // lock the resource
677                        cms.lockResource(resource);
678                    } else if (lock.isLockableBy(cms.getRequestContext().getCurrentUser())) {
679                        // steal the resource
680                        cms.changeLock(resource);
681                    }
682                    if (!resource.getState().isDeleted()) {
683                        // delete the resource
684                        cms.deleteResource(sitePath, CmsResource.DELETE_PRESERVE_SIBLINGS);
685                    }
686                    // update the report
687                    report.print(Messages.get().container(Messages.RPT_DELETE_0), I_CmsReport.FORMAT_NOTE);
688                    report.println(
689                        org.opencms.report.Messages.get().container(
690                            org.opencms.report.Messages.RPT_ARGUMENT_1,
691                            sitePath));
692                    if (!resource.getState().isNew()) {
693                        // unlock the resource (so it gets deleted with next publish)
694                        cms.unlockResource(resource);
695                    }
696                } catch (CmsException e) {
697                    // ignore the exception and delete the next resource
698                    LOG.error(Messages.get().getBundle().key(Messages.LOG_DEL_MOD_EXC_1, sitePath), e);
699                    report.println(e.getMessageContainer(), I_CmsReport.FORMAT_WARNING);
700                }
701            }
702
703            report.println(Messages.get().container(Messages.RPT_PUBLISH_PROJECT_BEGIN_0), I_CmsReport.FORMAT_HEADLINE);
704
705            // now unlock and publish the project
706            cms.unlockProject(deleteProject.getUuid());
707            OpenCms.getPublishManager().publishProject(cms, report);
708            OpenCms.getPublishManager().waitWhileRunning();
709
710            report.println(Messages.get().container(Messages.RPT_PUBLISH_PROJECT_END_0), I_CmsReport.FORMAT_HEADLINE);
711            report.println(Messages.get().container(Messages.RPT_DELETE_MODULE_END_0), I_CmsReport.FORMAT_HEADLINE);
712        } catch (CmsException e) {
713            throw new CmsConfigurationException(e.getMessageContainer(), e);
714        } finally {
715            cms.getRequestContext().setCurrentProject(previousProject);
716        }
717
718        // initialize the export points (removes export points from deleted module)
719        initModuleExportPoints();
720
721        // update the configuration
722        updateModuleConfiguration();
723
724        // reinit the manager is necessary
725        if (removeResourceTypes) {
726            OpenCms.getResourceManager().initialize(cms);
727        }
728
729        // reinit the workplace CSS URIs
730        if (!module.getParameters().isEmpty()) {
731            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
732        }
733    }
734
735    /**
736     * Deletes a module from the configuration.<p>
737     *
738     * @param cms must be initialized with "Admin" permissions
739     * @param moduleName the name of the module to delete
740     * @param replace indicates if the module is replaced (true) or finally deleted (false)
741     * @param report the report to print progress messages to
742     *
743     * @throws CmsRoleViolationException if the required module manager role permissions are not available
744     * @throws CmsConfigurationException if a module with this name is not available for deleting
745     * @throws CmsLockException if the module resources can not be locked
746     */
747    public synchronized void deleteModule(CmsObject cms, String moduleName, boolean replace, I_CmsReport report)
748    throws CmsRoleViolationException, CmsConfigurationException, CmsLockException {
749
750        deleteModule(cms, moduleName, replace, false, report);
751    }
752
753    /**
754     * Returns a list of installed modules.<p>
755     *
756     * @return a list of <code>{@link CmsModule}</code> objects
757     */
758    public List<CmsModule> getAllInstalledModules() {
759
760        return new ArrayList<CmsModule>(m_modules.values());
761    }
762
763    /**
764     * Returns the (immutable) list of configured module export points.<p>
765     *
766     * @return the (immutable) list of configured module export points
767     * @see CmsExportPoint
768     */
769    public Set<CmsExportPoint> getExportPoints() {
770
771        return m_moduleExportPoints;
772    }
773
774    /**
775     * Returns the importExportRepository.<p>
776     *
777     * @return the importExportRepository
778     */
779    public CmsModuleImportExportRepository getImportExportRepository() {
780
781        return m_importExportRepository;
782    }
783
784    /**
785     * Returns the module with the given module name,
786     * or <code>null</code> if no module with the given name is configured.<p>
787     *
788     * @param name the name of the module to return
789     * @return the module with the given module name
790     */
791    public CmsModule getModule(String name) {
792
793        return m_modules.get(name);
794    }
795
796    /**
797     * Returns the set of names of all the installed modules.<p>
798     *
799     * @return the set of names of all the installed modules
800     */
801    public Set<String> getModuleNames() {
802
803        synchronized (m_modules) {
804            return new HashSet<String>(m_modules.keySet());
805        }
806    }
807
808    /**
809     * Checks if this module manager has a module with the given name installed.<p>
810     *
811     * @param name the name of the module to check
812     * @return true if this module manager has a module with the given name installed
813     */
814    public boolean hasModule(String name) {
815
816        return m_modules.containsKey(name);
817    }
818
819    /**
820     * Initializes all module instance classes managed in this module manager.<p>
821     *
822     * @param cms an initialized CmsObject with "manage modules" role permissions
823     * @param configurationManager the initialized OpenCms configuration manager
824     *
825     * @throws CmsRoleViolationException if the provided OpenCms context does not have "manage modules" role permissions
826     */
827    public synchronized void initialize(CmsObject cms, CmsConfigurationManager configurationManager)
828    throws CmsRoleViolationException {
829
830        if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
831            // certain test cases won't have an OpenCms context
832            OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
833        }
834
835        Iterator<String> it;
836        int count = 0;
837        it = m_modules.keySet().iterator();
838        while (it.hasNext()) {
839            // get the module description
840            CmsModule module = m_modules.get(it.next());
841
842            if (module.getActionClass() != null) {
843                // create module instance class
844                I_CmsModuleAction moduleAction = module.getActionInstance();
845                if (module.getActionClass() != null) {
846                    try {
847                        moduleAction = (I_CmsModuleAction)Class.forName(module.getActionClass()).newInstance();
848                    } catch (Exception e) {
849                        CmsLog.INIT.info(
850                            Messages.get().getBundle().key(Messages.INIT_CREATE_INSTANCE_FAILED_1, module.getName()),
851                            e);
852                    }
853                }
854                if (moduleAction != null) {
855                    count++;
856                    module.setActionInstance(moduleAction);
857                    if (CmsLog.INIT.isInfoEnabled()) {
858                        CmsLog.INIT.info(
859                            Messages.get().getBundle().key(
860                                Messages.INIT_INITIALIZE_MOD_CLASS_1,
861                                moduleAction.getClass().getName()));
862                    }
863                    try {
864                        // create a copy of the adminCms so that each module instance does have
865                        // it's own context, a shared context might introduce side - effects
866                        CmsObject adminCmsCopy = OpenCms.initCmsObject(cms);
867                        // initialize the module
868                        moduleAction.initialize(adminCmsCopy, configurationManager, module);
869                    } catch (Throwable t) {
870                        LOG.error(
871                            Messages.get().getBundle().key(
872                                Messages.LOG_INSTANCE_INIT_ERR_1,
873                                moduleAction.getClass().getName()),
874                            t);
875                    }
876                }
877            }
878        }
879
880        // initialize the export points
881        initModuleExportPoints();
882        m_importExportRepository.initialize(cms);
883
884        if (CmsLog.INIT.isInfoEnabled()) {
885            CmsLog.INIT.info(
886                Messages.get().getBundle().key(Messages.INIT_NUM_CLASSES_INITIALIZED_1, new Integer(count)));
887        }
888    }
889
890    /**
891     * Replaces an existing module with the one read from an import ZIP file.<p>
892     *
893     * If there is not already a module with the same name installed, then the module will just be imported normally.
894     *
895     * @param cms the CMS context
896     * @param importFile the import file
897     * @param report the report
898     *
899     * @return the module replacement status
900     * @throws CmsException if something goes wrong
901     */
902    public CmsReplaceModuleInfo replaceModule(CmsObject cms, String importFile, I_CmsReport report)
903    throws CmsException {
904
905        CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(importFile);
906
907        boolean hasModule = hasModule(module.getName());
908        boolean usedNewUpdate = false;
909        if (hasModule) {
910            Optional<CmsModuleUpdater> optModuleUpdater = CmsModuleUpdater.create(cms, importFile, report);
911            if (optModuleUpdater.isPresent()) {
912                usedNewUpdate = true;
913                optModuleUpdater.get().run();
914            } else {
915                deleteModule(cms, module.getName(), true, report);
916                CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
917                OpenCms.getImportExportManager().importData(cms, report, params);
918            }
919
920        } else {
921            CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
922            OpenCms.getImportExportManager().importData(cms, report, params);
923        }
924        return new CmsReplaceModuleInfo(module, usedNewUpdate);
925    }
926
927    /**
928     * Shuts down all module instance classes managed in this module manager.<p>
929     */
930    public synchronized void shutDown() {
931
932        int count = 0;
933        Iterator<String> it = getModuleNames().iterator();
934        while (it.hasNext()) {
935            String moduleName = it.next();
936            // get the module
937            CmsModule module = m_modules.get(moduleName);
938            if (module == null) {
939                continue;
940            }
941            // get the module action instance
942            I_CmsModuleAction moduleAction = module.getActionInstance();
943            if (moduleAction == null) {
944                continue;
945            }
946
947            count++;
948            if (CmsLog.INIT.isInfoEnabled()) {
949                CmsLog.INIT.info(
950                    Messages.get().getBundle().key(
951                        Messages.INIT_SHUTDOWN_MOD_CLASS_1,
952                        moduleAction.getClass().getName()));
953            }
954            try {
955                // shut down the module
956                moduleAction.shutDown(module);
957            } catch (Throwable t) {
958                LOG.error(
959                    Messages.get().getBundle().key(
960                        Messages.LOG_INSTANCE_SHUTDOWN_ERR_1,
961                        moduleAction.getClass().getName()),
962                    t);
963            }
964        }
965
966        if (CmsLog.INIT.isInfoEnabled()) {
967            CmsLog.INIT.info(
968                Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_NUM_MOD_CLASSES_1, new Integer(count)));
969        }
970
971        if (CmsLog.INIT.isInfoEnabled()) {
972            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_1, this.getClass().getName()));
973        }
974    }
975
976    /**
977     * Updates a already configured module with new values.<p>
978     *
979     * @param cms must be initialized with "Admin" permissions
980     * @param module the module to update
981     *
982     * @throws CmsRoleViolationException if the required module manager role permissions are not available
983     * @throws CmsConfigurationException if a module with this name is not available for updating
984     */
985    public synchronized void updateModule(CmsObject cms, CmsModule module)
986    throws CmsRoleViolationException, CmsConfigurationException {
987
988        // check for module manager role permissions
989        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
990
991        CmsModule oldModule = m_modules.get(module.getName());
992
993        if (oldModule == null) {
994            // module is not currently configured, no update possible
995            throw new CmsConfigurationException(Messages.get().container(Messages.ERR_OLD_MOD_ERR_1, module.getName()));
996        }
997
998        if (LOG.isInfoEnabled()) {
999            LOG.info(Messages.get().getBundle().key(Messages.LOG_MOD_UPDATE_1, module.getName()));
1000        }
1001
1002        // indicate that the version number was recently updated
1003        module.getVersion().setUpdated(true);
1004
1005        // initialize (freeze) the module
1006        module.initialize(cms);
1007
1008        // replace old version of module with new version
1009        m_modules.put(module.getName(), module);
1010
1011        try {
1012            I_CmsModuleAction moduleAction = oldModule.getActionInstance();
1013            // handle module action instance if initialized
1014            if (moduleAction != null) {
1015                moduleAction.moduleUpdate(module);
1016                // set the old action instance
1017                // the new action instance will be used after a system restart
1018                module.setActionInstance(moduleAction);
1019            }
1020        } catch (Throwable t) {
1021            LOG.error(Messages.get().getBundle().key(Messages.LOG_INSTANCE_UPDATE_ERR_1, module.getName()), t);
1022        }
1023
1024        // initialize the export points
1025        initModuleExportPoints();
1026
1027        // update the configuration
1028        updateModuleConfiguration();
1029
1030        // reinit the workplace CSS URIs
1031        if (!module.getParameters().isEmpty()) {
1032            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
1033        }
1034    }
1035
1036    /**
1037     * Updates the module configuration.<p>
1038     */
1039    public void updateModuleConfiguration() {
1040
1041        OpenCms.writeConfiguration(CmsModuleConfiguration.class);
1042    }
1043
1044    /**
1045     * Initializes the list of export points from all configured modules.<p>
1046     */
1047    private synchronized void initModuleExportPoints() {
1048
1049        Set<CmsExportPoint> exportPoints = new HashSet<CmsExportPoint>();
1050        Iterator<CmsModule> i = m_modules.values().iterator();
1051        while (i.hasNext()) {
1052            CmsModule module = i.next();
1053            List<CmsExportPoint> moduleExportPoints = module.getExportPoints();
1054            for (int j = 0; j < moduleExportPoints.size(); j++) {
1055                CmsExportPoint point = moduleExportPoints.get(j);
1056                if (exportPoints.contains(point)) {
1057                    if (LOG.isWarnEnabled()) {
1058                        LOG.warn(
1059                            Messages.get().getBundle().key(
1060                                Messages.LOG_DUPLICATE_EXPORT_POINT_2,
1061                                point,
1062                                module.getName()));
1063                    }
1064                } else {
1065                    exportPoints.add(point);
1066                    if (LOG.isDebugEnabled()) {
1067                        LOG.debug(
1068                            Messages.get().getBundle().key(Messages.LOG_ADD_EXPORT_POINT_2, point, module.getName()));
1069                    }
1070                }
1071            }
1072        }
1073        m_moduleExportPoints = Collections.unmodifiableSet(exportPoints);
1074    }
1075}