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            if (moduleResources.size() > 0) {
716                report.println(
717                    Messages.get().container(Messages.RPT_PUBLISH_PROJECT_BEGIN_0),
718                    I_CmsReport.FORMAT_HEADLINE);
719                // now unlock and publish the project
720                cms.unlockProject(deleteProject.getUuid());
721                OpenCms.getPublishManager().publishProject(cms, report);
722                OpenCms.getPublishManager().waitWhileRunning();
723                report.println(
724                    Messages.get().container(Messages.RPT_PUBLISH_PROJECT_END_0),
725                    I_CmsReport.FORMAT_HEADLINE);
726                report.println(Messages.get().container(Messages.RPT_DELETE_MODULE_END_0), I_CmsReport.FORMAT_HEADLINE);
727            }
728
729        } catch (CmsException e) {
730            throw new CmsConfigurationException(e.getMessageContainer(), e);
731        } finally {
732            cms.getRequestContext().setCurrentProject(previousProject);
733        }
734
735        // initialize the export points (removes export points from deleted module)
736        initModuleExportPoints();
737
738        // update the configuration
739        updateModuleConfiguration();
740
741        // reinit the manager is necessary
742        if (removeResourceTypes) {
743            OpenCms.getResourceManager().initialize(cms);
744        }
745
746        // reinit the workplace CSS URIs
747        if (!module.getParameters().isEmpty()) {
748            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
749        }
750    }
751
752    /**
753     * Deletes a module from the configuration.<p>
754     *
755     * @param cms must be initialized with "Admin" permissions
756     * @param moduleName the name of the module to delete
757     * @param replace indicates if the module is replaced (true) or finally deleted (false)
758     * @param report the report to print progress messages to
759     *
760     * @throws CmsRoleViolationException if the required module manager role permissions are not available
761     * @throws CmsConfigurationException if a module with this name is not available for deleting
762     * @throws CmsLockException if the module resources can not be locked
763     */
764    public synchronized void deleteModule(CmsObject cms, String moduleName, boolean replace, I_CmsReport report)
765    throws CmsRoleViolationException, CmsConfigurationException, CmsLockException {
766
767        deleteModule(cms, moduleName, replace, false, report);
768    }
769
770    /**
771     * Returns a list of installed modules.<p>
772     *
773     * @return a list of <code>{@link CmsModule}</code> objects
774     */
775    public List<CmsModule> getAllInstalledModules() {
776
777        return new ArrayList<CmsModule>(m_modules.values());
778    }
779
780    /**
781     * Returns the (immutable) list of configured module export points.<p>
782     *
783     * @return the (immutable) list of configured module export points
784     * @see CmsExportPoint
785     */
786    public Set<CmsExportPoint> getExportPoints() {
787
788        return m_moduleExportPoints;
789    }
790
791    /**
792     * Returns the importExportRepository.<p>
793     *
794     * @return the importExportRepository
795     */
796    public CmsModuleImportExportRepository getImportExportRepository() {
797
798        return m_importExportRepository;
799    }
800
801    /**
802     * Returns the module with the given module name,
803     * or <code>null</code> if no module with the given name is configured.<p>
804     *
805     * @param name the name of the module to return
806     * @return the module with the given module name
807     */
808    public CmsModule getModule(String name) {
809
810        return m_modules.get(name);
811    }
812
813    /**
814     * Returns the set of names of all the installed modules.<p>
815     *
816     * @return the set of names of all the installed modules
817     */
818    public Set<String> getModuleNames() {
819
820        synchronized (m_modules) {
821            return new HashSet<String>(m_modules.keySet());
822        }
823    }
824
825    /**
826     * Checks if this module manager has a module with the given name installed.<p>
827     *
828     * @param name the name of the module to check
829     * @return true if this module manager has a module with the given name installed
830     */
831    public boolean hasModule(String name) {
832
833        return m_modules.containsKey(name);
834    }
835
836    /**
837     * Initializes all module instance classes managed in this module manager.<p>
838     *
839     * @param cms an initialized CmsObject with "manage modules" role permissions
840     * @param configurationManager the initialized OpenCms configuration manager
841     *
842     * @throws CmsRoleViolationException if the provided OpenCms context does not have "manage modules" role permissions
843     */
844    public synchronized void initialize(CmsObject cms, CmsConfigurationManager configurationManager)
845    throws CmsRoleViolationException {
846
847        if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
848            // certain test cases won't have an OpenCms context
849            OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
850        }
851
852        Iterator<String> it;
853        int count = 0;
854        it = m_modules.keySet().iterator();
855        while (it.hasNext()) {
856            // get the module description
857            CmsModule module = m_modules.get(it.next());
858
859            if (module.getActionClass() != null) {
860                // create module instance class
861                I_CmsModuleAction moduleAction = module.getActionInstance();
862                if (module.getActionClass() != null) {
863                    try {
864                        moduleAction = (I_CmsModuleAction)Class.forName(module.getActionClass()).newInstance();
865                    } catch (Exception e) {
866                        CmsLog.INIT.info(
867                            Messages.get().getBundle().key(Messages.INIT_CREATE_INSTANCE_FAILED_1, module.getName()),
868                            e);
869                    }
870                }
871                if (moduleAction != null) {
872                    count++;
873                    module.setActionInstance(moduleAction);
874                    if (CmsLog.INIT.isInfoEnabled()) {
875                        CmsLog.INIT.info(
876                            Messages.get().getBundle().key(
877                                Messages.INIT_INITIALIZE_MOD_CLASS_1,
878                                moduleAction.getClass().getName()));
879                    }
880                    try {
881                        // create a copy of the adminCms so that each module instance does have
882                        // it's own context, a shared context might introduce side - effects
883                        CmsObject adminCmsCopy = OpenCms.initCmsObject(cms);
884                        // initialize the module
885                        moduleAction.initialize(adminCmsCopy, configurationManager, module);
886                    } catch (Throwable t) {
887                        LOG.error(
888                            Messages.get().getBundle().key(
889                                Messages.LOG_INSTANCE_INIT_ERR_1,
890                                moduleAction.getClass().getName()),
891                            t);
892                    }
893                }
894            }
895        }
896
897        // initialize the export points
898        initModuleExportPoints();
899        m_importExportRepository.initialize(cms);
900
901        if (CmsLog.INIT.isInfoEnabled()) {
902            CmsLog.INIT.info(
903                Messages.get().getBundle().key(Messages.INIT_NUM_CLASSES_INITIALIZED_1, new Integer(count)));
904        }
905    }
906
907    /**
908     * Replaces an existing module with the one read from an import ZIP file.<p>
909     *
910     * If there is not already a module with the same name installed, then the module will just be imported normally.
911     *
912     * @param cms the CMS context
913     * @param importFile the import file
914     * @param report the report
915     *
916     * @return the module replacement status
917     * @throws CmsException if something goes wrong
918     */
919    public CmsReplaceModuleInfo replaceModule(CmsObject cms, String importFile, I_CmsReport report)
920    throws CmsException {
921
922        CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(importFile);
923
924        boolean hasModule = hasModule(module.getName());
925        boolean usedNewUpdate = false;
926        if (hasModule) {
927            Optional<CmsModuleUpdater> optModuleUpdater;
928            if (m_moduleUpdateEnabled) {
929                optModuleUpdater = CmsModuleUpdater.create(cms, importFile, report);
930            } else {
931                optModuleUpdater = Optional.empty();
932            }
933            if (optModuleUpdater.isPresent()) {
934                usedNewUpdate = true;
935                optModuleUpdater.get().run();
936            } else {
937                deleteModule(cms, module.getName(), true, report);
938                CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
939                OpenCms.getImportExportManager().importData(cms, report, params);
940            }
941
942        } else {
943            CmsImportParameters params = new CmsImportParameters(importFile, "/", true);
944            OpenCms.getImportExportManager().importData(cms, report, params);
945        }
946        return new CmsReplaceModuleInfo(module, usedNewUpdate);
947    }
948
949    /**
950     * Enables / disables incremental module updates, for testing purposes.
951     *
952     * @param enabled if incremental module updating should be enabled
953     */
954    public void setModuleUpdateEnabled(boolean enabled) {
955
956        m_moduleUpdateEnabled = enabled;
957    }
958
959    /**
960     * Shuts down all module instance classes managed in this module manager.<p>
961     */
962    public synchronized void shutDown() {
963
964        int count = 0;
965        Iterator<String> it = getModuleNames().iterator();
966        while (it.hasNext()) {
967            String moduleName = it.next();
968            // get the module
969            CmsModule module = m_modules.get(moduleName);
970            if (module == null) {
971                continue;
972            }
973            // get the module action instance
974            I_CmsModuleAction moduleAction = module.getActionInstance();
975            if (moduleAction == null) {
976                continue;
977            }
978
979            count++;
980            if (CmsLog.INIT.isInfoEnabled()) {
981                CmsLog.INIT.info(
982                    Messages.get().getBundle().key(
983                        Messages.INIT_SHUTDOWN_MOD_CLASS_1,
984                        moduleAction.getClass().getName()));
985            }
986            try {
987                // shut down the module
988                moduleAction.shutDown(module);
989            } catch (Throwable t) {
990                LOG.error(
991                    Messages.get().getBundle().key(
992                        Messages.LOG_INSTANCE_SHUTDOWN_ERR_1,
993                        moduleAction.getClass().getName()),
994                    t);
995            }
996        }
997
998        if (CmsLog.INIT.isInfoEnabled()) {
999            CmsLog.INIT.info(
1000                Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_NUM_MOD_CLASSES_1, new Integer(count)));
1001        }
1002
1003        if (CmsLog.INIT.isInfoEnabled()) {
1004            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_1, this.getClass().getName()));
1005        }
1006    }
1007
1008    /**
1009     * Updates a already configured module with new values.<p>
1010     *
1011     * @param cms must be initialized with "Admin" permissions
1012     * @param module the module to update
1013     *
1014     * @throws CmsRoleViolationException if the required module manager role permissions are not available
1015     * @throws CmsConfigurationException if a module with this name is not available for updating
1016     */
1017    public synchronized void updateModule(CmsObject cms, CmsModule module)
1018    throws CmsRoleViolationException, CmsConfigurationException {
1019
1020        // check for module manager role permissions
1021        OpenCms.getRoleManager().checkRole(cms, CmsRole.DATABASE_MANAGER);
1022
1023        CmsModule oldModule = m_modules.get(module.getName());
1024
1025        if (oldModule == null) {
1026            // module is not currently configured, no update possible
1027            throw new CmsConfigurationException(Messages.get().container(Messages.ERR_OLD_MOD_ERR_1, module.getName()));
1028        }
1029
1030        if (LOG.isInfoEnabled()) {
1031            LOG.info(Messages.get().getBundle().key(Messages.LOG_MOD_UPDATE_1, module.getName()));
1032        }
1033
1034        // indicate that the version number was recently updated
1035        module.getVersion().setUpdated(true);
1036
1037        // initialize (freeze) the module
1038        module.initialize(cms);
1039
1040        // replace old version of module with new version
1041        m_modules.put(module.getName(), module);
1042
1043        try {
1044            I_CmsModuleAction moduleAction = oldModule.getActionInstance();
1045            // handle module action instance if initialized
1046            if (moduleAction != null) {
1047                moduleAction.moduleUpdate(module);
1048                // set the old action instance
1049                // the new action instance will be used after a system restart
1050                module.setActionInstance(moduleAction);
1051            }
1052        } catch (Throwable t) {
1053            LOG.error(Messages.get().getBundle().key(Messages.LOG_INSTANCE_UPDATE_ERR_1, module.getName()), t);
1054        }
1055
1056        // initialize the export points
1057        initModuleExportPoints();
1058
1059        // update the configuration
1060        updateModuleConfiguration();
1061
1062        // reinit the workplace CSS URIs
1063        if (!module.getParameters().isEmpty()) {
1064            OpenCms.getWorkplaceAppManager().initWorkplaceCssUris(this);
1065        }
1066    }
1067
1068    /**
1069     * Updates the module configuration.<p>
1070     */
1071    public void updateModuleConfiguration() {
1072
1073        OpenCms.writeConfiguration(CmsModuleConfiguration.class);
1074    }
1075
1076    /**
1077     * Initializes the list of export points from all configured modules.<p>
1078     */
1079    private synchronized void initModuleExportPoints() {
1080
1081        Set<CmsExportPoint> exportPoints = new HashSet<CmsExportPoint>();
1082        Iterator<CmsModule> i = m_modules.values().iterator();
1083        while (i.hasNext()) {
1084            CmsModule module = i.next();
1085            List<CmsExportPoint> moduleExportPoints = module.getExportPoints();
1086            for (int j = 0; j < moduleExportPoints.size(); j++) {
1087                CmsExportPoint point = moduleExportPoints.get(j);
1088                if (exportPoints.contains(point)) {
1089                    if (LOG.isWarnEnabled()) {
1090                        LOG.warn(
1091                            Messages.get().getBundle().key(
1092                                Messages.LOG_DUPLICATE_EXPORT_POINT_2,
1093                                point,
1094                                module.getName()));
1095                    }
1096                } else {
1097                    exportPoints.add(point);
1098                    if (LOG.isDebugEnabled()) {
1099                        LOG.debug(
1100                            Messages.get().getBundle().key(Messages.LOG_ADD_EXPORT_POINT_2, point, module.getName()));
1101                    }
1102                }
1103            }
1104        }
1105        m_moduleExportPoints = Collections.unmodifiableSet(exportPoints);
1106    }
1107}