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