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