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, 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.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProject;
033import org.opencms.file.CmsProperty;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.CmsVfsResourceNotFoundException;
037import org.opencms.file.types.I_CmsResourceType;
038import org.opencms.importexport.CmsImportParameters;
039import org.opencms.importexport.CmsImportResourceDataReader;
040import org.opencms.importexport.CmsImportVersion10;
041import org.opencms.importexport.CmsImportVersion10.RelationData;
042import org.opencms.importexport.Messages;
043import org.opencms.lock.CmsLock;
044import org.opencms.main.CmsException;
045import org.opencms.main.CmsLog;
046import org.opencms.main.CmsShell;
047import org.opencms.main.OpenCms;
048import org.opencms.relations.CmsRelation;
049import org.opencms.relations.CmsRelationFilter;
050import org.opencms.relations.CmsRelationType;
051import org.opencms.relations.I_CmsLinkParseable;
052import org.opencms.report.I_CmsReport;
053import org.opencms.security.CmsAccessControlEntry;
054import org.opencms.util.CmsFileUtil;
055import org.opencms.util.CmsStringUtil;
056import org.opencms.util.CmsUUID;
057
058import java.io.ByteArrayOutputStream;
059import java.io.PrintStream;
060import java.util.ArrayList;
061import java.util.Arrays;
062import java.util.Collection;
063import java.util.Collections;
064import java.util.HashMap;
065import java.util.HashSet;
066import java.util.List;
067import java.util.Map;
068import java.util.Optional;
069import java.util.Set;
070import java.util.stream.Collectors;
071
072import org.apache.commons.logging.Log;
073
074import com.google.common.base.Objects;
075import com.google.common.collect.Sets;
076
077/**
078 * Class used for updating modules.<p>
079 *
080 * This class updates modules in a smarter way than simply deleting and importing them again: The resources in the import
081 * ZIP file are compared to the resources in the currently installed module and only makes changes when necessary. The reason
082 * for this is that deletions of resources can be slow in some very large OpenCms installations, and the classic way of updating modules
083 * (delete/import) can take a long time because of this.
084 */
085public class CmsModuleUpdater {
086
087    /** The logger instance for this class. */
088    private static final Log LOG = CmsLog.getLog(CmsModuleUpdater.class);
089
090    /** Structure ids of imported resources.*/
091    private Set<CmsUUID> m_importIds = new HashSet<CmsUUID>();
092
093    /** The module data read from the ZIP. */
094    private CmsModuleImportData m_moduleData;
095
096    /** The report to write to. */
097    private I_CmsReport m_report;
098
099    /**
100     * Creates a new instance.<p>
101     *
102     * @param moduleData the module import data
103     * @param report the report to write to
104     */
105    public CmsModuleUpdater(CmsModuleImportData moduleData, I_CmsReport report) {
106
107        m_moduleData = moduleData;
108        m_report = report;
109    }
110
111    /**
112     * Checks whether the module resources and sites of the two module versions are suitable for updating.<p>
113     *
114     * @param installedModule the installed module
115     * @param newModule the module to import
116     *
117     * @return true if the module resources are compatible
118     */
119    public static boolean checkCompatibleModuleResources(CmsModule installedModule, CmsModule newModule) {
120
121        if (!(installedModule.hasOnlySystemAndSharedResources() && newModule.hasOnlySystemAndSharedResources())) {
122            String oldSite = installedModule.getSite();
123            String newSite = newModule.getSite();
124            if (!((oldSite != null) && (newSite != null) && CmsStringUtil.comparePaths(oldSite, newSite))) {
125                return false;
126            }
127
128        }
129        for (String oldModRes : installedModule.getResources()) {
130            for (String newModRes : newModule.getResources()) {
131                if (CmsStringUtil.isProperPrefixPath(oldModRes, newModRes)) {
132                    return false;
133                }
134            }
135        }
136        return true;
137
138    }
139
140    /**
141     * Tries to create a new updater instance.<p>
142     *
143     * If the module is deemed non-updatable, an empty result is returned.<p>
144     *
145     * @param cms the current CMS context
146     * @param importFile the import file path
147     * @param report the report to write to
148     * @return an optional module updater
149     *
150     * @throws CmsException if something goes wrong
151     */
152    public static Optional<CmsModuleUpdater> create(CmsObject cms, String importFile, I_CmsReport report)
153    throws CmsException {
154
155        CmsModuleImportData moduleData = readModuleData(cms, importFile, report);
156        if (moduleData.checkUpdatable(cms)) {
157            return Optional.of(new CmsModuleUpdater(moduleData, report));
158        } else {
159            return Optional.empty();
160        }
161    }
162
163    /**
164     * Check if a resource needs to be updated because of its direct fields.<p>
165     *
166     * @param existingRes the existing resource
167     * @param newRes the new resource
168     * @param reduced true if we are in reduced export mode
169     *
170     * @return true if we need to update the resource based on its direct fields
171     */
172    public static boolean needToUpdateResourceFields(CmsResource existingRes, CmsResource newRes, boolean reduced) {
173
174        boolean result = false;
175        result |= existingRes.getTypeId() != newRes.getTypeId();
176        result |= differentDates(existingRes.getDateCreated(), newRes.getDateCreated()); // Export format date is not precise to millisecond
177        result |= differentDates(existingRes.getDateReleased(), newRes.getDateReleased());
178        result |= differentDates(existingRes.getDateExpired(), newRes.getDateExpired());
179        result |= existingRes.getFlags() != newRes.getFlags();
180        if (!reduced) {
181            result |= !Objects.equal(existingRes.getUserCreated(), newRes.getUserCreated());
182            result |= !Objects.equal(existingRes.getUserLastModified(), newRes.getUserLastModified());
183            result |= existingRes.getDateLastModified() != newRes.getDateLastModified();
184        }
185        return result;
186    }
187
188    /**
189     * Normalizes the path.<p>
190     *
191     * @param pathComponents the path components
192     *
193     * @return the normalized path
194     */
195    public static String normalizePath(String... pathComponents) {
196
197        return CmsFileUtil.removeTrailingSeparator(CmsStringUtil.joinPaths(pathComponents));
198    }
199
200    /**
201     * Reads the module data from an import zip file.<p>
202     *
203     * @param cms the CMS context
204     * @param importFile the import file
205     * @param report the report to write to
206     * @return the module data
207     * @throws CmsException if something goes wrong
208     */
209    public static CmsModuleImportData readModuleData(CmsObject cms, String importFile, I_CmsReport report)
210    throws CmsException {
211
212        CmsModuleImportData result = new CmsModuleImportData();
213        CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(importFile);
214        cms = OpenCms.initCmsObject(cms);
215
216        String importSite = module.getImportSite();
217        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(importSite)) {
218            cms.getRequestContext().setSiteRoot(importSite);
219        } else {
220            String siteToSet = cms.getRequestContext().getSiteRoot();
221            if ("".equals(siteToSet)) {
222                siteToSet = "/";
223            }
224            module.setSite(siteToSet);
225        }
226        result.setModule(module);
227        result.setCms(cms);
228        CmsImportResourceDataReader importer = new CmsImportResourceDataReader(result);
229        CmsImportParameters params = new CmsImportParameters(importFile, "/", false);
230        importer.importData(cms, report, params); // This only reads the module data into Java objects
231        return result;
232
233    }
234
235    /**
236     * Checks that two longs representing dates differ by more than 1000 (milliseconds).<p>
237     *
238     * @param d1 the first date
239     * @param d2 the second date
240     *
241     * @return true if the dates differ by more than 1000 milliseconds
242     */
243    static boolean differentDates(long d1, long d2) {
244
245        return 1000 < Math.abs(d2 - d1);
246    }
247
248    /**
249     * Gets all resources in the module.<p>
250     *
251     * @param cms the current CMS context
252     * @param module the module
253     * @return the resources in the module
254     * @throws CmsException if something goes wrong
255     */
256    private static Set<CmsResource> getAllResourcesInModule(CmsObject cms, CmsModule module) throws CmsException {
257
258        Set<CmsResource> result = new HashSet<>();
259        for (CmsResource resource : CmsModule.calculateModuleResources(cms, module)) {
260            result.add(resource);
261            if (resource.isFolder()) {
262                result.addAll(cms.readResources(resource, CmsResourceFilter.ALL, true));
263            }
264        }
265        return result;
266    }
267
268    /**
269     * Update relations for all imported resources.<p>
270     *
271     * @param cms the current CMS context
272     * @throws CmsException if something goes wrong
273     */
274    public void importRelations(CmsObject cms) throws CmsException {
275
276        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
277            if (!resData.getRelations().isEmpty()) {
278                CmsResource importResource = resData.getImportResource();
279                if (importResource != null) {
280                    importResource = cms.readResource(importResource.getStructureId(), CmsResourceFilter.ALL);
281                    updateRelations(cms, importResource, resData.getRelations());
282                }
283            }
284        }
285
286    }
287
288    /**
289     * Performs the module update.<p>
290     */
291    public void run() {
292
293        try {
294            CmsObject cms = m_moduleData.getCms();
295            CmsModule module = m_moduleData.getModule();
296            CmsModule oldModule = OpenCms.getModuleManager().getModule(module.getName());
297            Map<CmsUUID, CmsUUID> conflictingIds = m_moduleData.getConflictingIds();
298            if (!conflictingIds.isEmpty()) {
299                deleteConflictingResources(cms, module, conflictingIds);
300            }
301            CmsProject importProject = createAndSetModuleImportProject(cms, module);
302            CmsModuleImportExportHandler.reportBeginImport(m_report, module.getName());
303
304            Map<CmsUUID, CmsResourceImportData> importResourcesById = new HashMap<>();
305            for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
306                importResourcesById.put(resData.getResource().getStructureId(), resData);
307            }
308            Set<CmsResource> oldModuleResources = getAllResourcesInModule(cms, oldModule);
309            List<CmsResource> toDelete = new ArrayList<>();
310            Set<String> immutables = OpenCms.getImportExportManager().getImmutableResources().stream().flatMap(
311                path -> Arrays.asList(
312                    CmsFileUtil.removeTrailingSeparator(path),
313                    CmsFileUtil.addTrailingSeparator(path)).stream()).collect(Collectors.toSet());
314            for (CmsResource oldRes : oldModuleResources) {
315                if (immutables.contains(oldRes.getRootPath())) {
316                    continue;
317                }
318                CmsResourceImportData newRes = importResourcesById.get(oldRes.getStructureId());
319                if (newRes == null) {
320                    toDelete.add(oldRes);
321                }
322            }
323            int index = 0;
324            for (CmsResourceImportData resData1 : m_moduleData.getResourceData()) {
325                index += 1;
326                processImportResource(cms, resData1, index);
327            }
328            processDeletions(cms, toDelete);
329            parseLinks(cms);
330
331            importRelations(cms);
332            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(module.getImportScript())) {
333                runImportScript(cms, module);
334            }
335            cms.unlockProject(importProject.getUuid());
336            OpenCms.getModuleManager().updateModule(cms, module);
337            module.setCheckpointTime(System.currentTimeMillis());
338            // reinitialize the resource manager with additional module resource types if necessary
339            if (module.getResourceTypes() != Collections.EMPTY_LIST) {
340                OpenCms.getResourceManager().initialize(cms);
341            }
342            // reinitialize the workplace manager with additional module explorer types if necessary
343            if (module.getExplorerTypes() != Collections.EMPTY_LIST) {
344                OpenCms.getWorkplaceManager().addExplorerTypeSettings(module);
345            }
346            OpenCms.getPublishManager().publishProject(cms, m_report);
347            OpenCms.getPublishManager().waitWhileRunning();
348            CmsModuleImportExportHandler.reportEndImport(m_report);
349        } catch (Exception e) {
350            m_report.println(e);
351        } finally {
352            cleanUp();
353        }
354    }
355
356    /**
357     * Updates the access control list fr a resource.<p>
358     *
359     * @param cms the current cms context
360     * @param resData the resource data
361     * @param resource the existing resource
362     * @return the resource
363     *
364     * @throws CmsException if something goes wrong
365     */
366    public boolean updateAcls(CmsObject cms, CmsResourceImportData resData, CmsResource resource) throws CmsException {
367
368        boolean changed = false;
369        Map<CmsUUID, CmsAccessControlEntry> importAces = buildAceMap(resData.getAccessControlEntries());
370
371        String path = cms.getSitePath(resource);
372        List<CmsAccessControlEntry> existingAcl = cms.getAccessControlEntries(path, false);
373        Map<CmsUUID, CmsAccessControlEntry> existingAces = buildAceMap(existingAcl);
374        Set<CmsUUID> keys = new HashSet<>(existingAces.keySet());
375        keys.addAll(importAces.keySet());
376        for (CmsUUID key : keys) {
377            CmsAccessControlEntry existingEntry = existingAces.get(key);
378            CmsAccessControlEntry newEntry = importAces.get(key);
379            if ((existingEntry == null)
380                || (newEntry == null)
381                || !existingEntry.withNulledResource().equals(newEntry.withNulledResource())) {
382                cms.importAccessControlEntries(resource, resData.getAccessControlEntries());
383                changed = true;
384                break;
385            }
386        }
387        return changed;
388    }
389
390    /**
391     * Creates the project used to import module resources and sets it on the CmsObject.
392     *
393     * @param cms the CmsObject to set the project on
394     * @param module the module
395     * @return the created project
396     * @throws CmsException if something goes wrong
397     */
398    protected CmsProject createAndSetModuleImportProject(CmsObject cms, CmsModule module) throws CmsException {
399
400        CmsProject importProject = cms.createProject(
401            org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
402                org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_NAME_1,
403                new Object[] {module.getName()}),
404            org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
405                org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_DESC_1,
406                new Object[] {module.getName()}),
407            OpenCms.getDefaultUsers().getGroupAdministrators(),
408            OpenCms.getDefaultUsers().getGroupAdministrators(),
409            CmsProject.PROJECT_TYPE_TEMPORARY);
410        cms.getRequestContext().setCurrentProject(importProject);
411        cms.copyResourceToProject("/");
412        return importProject;
413    }
414
415    /**
416     * Deletes and publishes resources with ID conflicts.
417     *
418     * @param cms the CMS context to use
419     * @param module the module
420     * @param conflictingIds the conflicting ids
421     * @throws CmsException if something goes wrong
422     * @throws Exception if something goes wrong
423     */
424    protected void deleteConflictingResources(CmsObject cms, CmsModule module, Map<CmsUUID, CmsUUID> conflictingIds)
425    throws CmsException, Exception {
426
427        CmsProject conflictProject = cms.createProject(
428            "Deletion of conflicting resources for " + module.getName(),
429            "Deletion of conflicting resources for " + module.getName(),
430            OpenCms.getDefaultUsers().getGroupAdministrators(),
431            OpenCms.getDefaultUsers().getGroupAdministrators(),
432            CmsProject.PROJECT_TYPE_TEMPORARY);
433        CmsObject deleteCms = OpenCms.initCmsObject(cms);
434        deleteCms.getRequestContext().setCurrentProject(conflictProject);
435        for (CmsUUID vfsId : conflictingIds.values()) {
436            CmsResource toDelete = deleteCms.readResource(vfsId, CmsResourceFilter.ALL);
437            lock(deleteCms, toDelete);
438            deleteCms.deleteResource(toDelete, CmsResource.DELETE_PRESERVE_SIBLINGS);
439        }
440        OpenCms.getPublishManager().publishProject(deleteCms);
441        OpenCms.getPublishManager().waitWhileRunning();
442    }
443
444    /**
445     * Parses links for XMLContents etc.
446     *
447     * @param cms the CMS context to use
448     * @throws CmsException if something goes wrong
449     */
450    protected void parseLinks(CmsObject cms) throws CmsException {
451
452        List<CmsResource> linkParseables = new ArrayList<>();
453        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
454            CmsResource importRes = resData.getImportResource();
455            if ((importRes != null) && m_importIds.contains(importRes.getStructureId()) && isLinkParsable(importRes)) {
456                linkParseables.add(importRes);
457            }
458        }
459        m_report.println(Messages.get().container(Messages.RPT_START_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE);
460        CmsImportVersion10.parseLinks(cms, linkParseables, m_report);
461        m_report.println(Messages.get().container(Messages.RPT_END_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE);
462    }
463
464    /**
465     * Handles the file deletions.
466     *
467     * @param cms the CMS context to use
468     * @param toDelete the resources to delete
469     *
470     * @throws CmsException if something goes wrong
471     */
472    protected void processDeletions(CmsObject cms, List<CmsResource> toDelete) throws CmsException {
473
474        Collections.sort(toDelete, (a, b) -> b.getRootPath().compareTo(a.getRootPath()));
475        for (CmsResource deleteRes : toDelete) {
476            m_report.print(
477                org.opencms.importexport.Messages.get().container(org.opencms.importexport.Messages.RPT_DELFOLDER_0),
478                I_CmsReport.FORMAT_NOTE);
479            m_report.print(
480                org.opencms.report.Messages.get().container(
481                    org.opencms.report.Messages.RPT_ARGUMENT_1,
482                    deleteRes.getRootPath()));
483            CmsLock lock = cms.getLock(deleteRes);
484            if (lock.isUnlocked()) {
485                lock(cms, deleteRes);
486            }
487            cms.deleteResource(deleteRes, CmsResource.DELETE_PRESERVE_SIBLINGS);
488            m_report.println(
489                org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
490                I_CmsReport.FORMAT_OK);
491
492        }
493    }
494
495    /**
496     * Processes single resource from module import data.
497     *
498     * @param cms the CMS context to use
499     * @param resData the resource data from the module import
500     * @param index index of the current import resource
501     */
502    protected void processImportResource(CmsObject cms, CmsResourceImportData resData, int index) {
503
504        boolean changed = false;
505        m_report.print(
506            org.opencms.report.Messages.get().container(
507                org.opencms.report.Messages.RPT_ARGUMENT_1,
508                "( " + index + " / " + m_moduleData.getResourceData().size() + " ) "),
509            I_CmsReport.FORMAT_NOTE);
510        m_report.print(Messages.get().container(Messages.RPT_IMPORTING_0), I_CmsReport.FORMAT_NOTE);
511        m_report.print(
512            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resData.getPath()));
513        m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
514        try {
515            CmsResource oldRes = null;
516            try {
517                if (resData.hasStructureId()) {
518                    oldRes = cms.readResource(
519                        resData.getResource().getStructureId(),
520                        CmsResourceFilter.IGNORE_EXPIRATION);
521                } else {
522                    oldRes = cms.readResource(resData.getPath(), CmsResourceFilter.IGNORE_EXPIRATION);
523                }
524            } catch (CmsVfsResourceNotFoundException e) {
525                LOG.debug(e.getLocalizedMessage(), e);
526            }
527            CmsResource currentRes = oldRes;
528            if (oldRes != null) {
529                String oldPath = cms.getSitePath(oldRes);
530                String newPath = resData.getPath();
531                if (!CmsStringUtil.comparePaths(oldPath, resData.getPath())) {
532                    cms.moveResource(oldPath, newPath);
533                    changed = true;
534                    currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION);
535                }
536            }
537            boolean needsImport = true;
538            boolean reducedExport = !resData.hasDateLastModified();
539            byte[] content = resData.getContent();
540            if (oldRes != null) {
541                if (!resData.hasStructureId()) {
542                    needsImport = false;
543                } else if (oldRes.getState().isUnchanged()
544                    && !needToUpdateResourceFields(oldRes, resData.getResource(), reducedExport)) {
545
546                    // if resource is changed or new, we don't want to go into this code block
547                    // because even if the content / metaadata are the same, we still want the file to be published at the end,
548                    // so we import it to add it to the current working project
549
550                    if (oldRes.isFile() && (content != null)) {
551                        CmsFile file = cms.readFile(oldRes);
552                        if (Arrays.equals(file.getContents(), content)) {
553                            needsImport = false;
554                        } else {
555                            LOG.debug("Content mismatch for " + file.getRootPath());
556                        }
557                    } else {
558                        needsImport = false;
559                    }
560                }
561            }
562            if (needsImport || (oldRes == null)) { // oldRes null check is redundant, we just do it to remove the warning in Eclipse
563                currentRes = cms.importResource(
564                    resData.getPath(),
565                    resData.getResource(),
566                    content,
567                    new ArrayList<CmsProperty>());
568                changed = true;
569                m_importIds.add(currentRes.getStructureId());
570            } else {
571                currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.ALL);
572                CmsLock lock = cms.getLock(currentRes);
573                if (lock.isUnlocked()) {
574                    lock(cms, currentRes);
575                }
576            }
577            resData.setImportResource(currentRes);
578            List<CmsProperty> propsToWrite = compareProperties(cms, resData, currentRes);
579            if (!propsToWrite.isEmpty()) {
580                cms.writePropertyObjects(currentRes, propsToWrite);
581                changed = true;
582            }
583            changed |= updateAcls(cms, resData, currentRes);
584            if (changed) {
585                m_report.println(
586                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
587                    I_CmsReport.FORMAT_OK);
588            } else {
589                m_report.println(
590                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0),
591                    I_CmsReport.FORMAT_NOTE);
592            }
593
594        } catch (Exception e) {
595            m_report.println(e);
596            LOG.error(e.getLocalizedMessage(), e);
597
598        }
599    }
600
601    /**
602     * Runs the module import script.
603     *
604     * @param cms the CMS context to use
605     * @param module the module for which to run the script
606     */
607    protected void runImportScript(CmsObject cms, CmsModule module) {
608
609        LOG.info("Executing import script for module " + module.getName());
610        m_report.println(
611            org.opencms.module.Messages.get().container(org.opencms.module.Messages.RPT_IMPORT_SCRIPT_HEADER_0),
612            I_CmsReport.FORMAT_HEADLINE);
613        String importScript = "echo on\n" + module.getImportScript();
614        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
615        PrintStream out = new PrintStream(buffer);
616        CmsShell shell = new CmsShell(cms, "${user}@${project}:${siteroot}|${uri}>", null, out, out);
617        shell.execute(importScript);
618        String outputString = buffer.toString();
619        LOG.info("Shell output for import script was: \n" + outputString);
620        m_report.println(
621            org.opencms.module.Messages.get().container(
622                org.opencms.module.Messages.RPT_IMPORT_SCRIPT_OUTPUT_1,
623                outputString));
624    }
625
626    /**
627     * Converts access control list to map form, with principal ids as keys.<p>
628     *
629     * @param acl an access control list
630     * @return the map with the access control entries
631     */
632    Map<CmsUUID, CmsAccessControlEntry> buildAceMap(Collection<CmsAccessControlEntry> acl) {
633
634        if (acl == null) {
635            acl = new ArrayList<>();
636        }
637        Map<CmsUUID, CmsAccessControlEntry> result = new HashMap<>();
638        for (CmsAccessControlEntry ace : acl) {
639            result.put(ace.getPrincipal(), ace);
640        }
641        return result;
642    }
643
644    /**
645     * Cleans up temp files.
646     */
647    private void cleanUp() {
648
649        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
650            resData.cleanUp();
651        }
652    }
653
654    /**
655     * Compares properties of an existing resource with those to be imported, and returns a list of properties that need to be updated.<p>
656     *
657     * @param cms the current CMS context
658     * @param resData  the resource import data
659     * @param existingResource the existing resource
660     * @return the list of properties that need to be updated
661     *
662     * @throws CmsException if something goes wrong
663     */
664    private List<CmsProperty> compareProperties(
665        CmsObject cms,
666        CmsResourceImportData resData,
667        CmsResource existingResource)
668    throws CmsException {
669
670        if (existingResource == null) {
671            return Collections.emptyList();
672        }
673
674        Map<String, CmsProperty> importProps = resData.getProperties();
675        Map<String, CmsProperty> existingProps = CmsProperty.getPropertyMap(
676            cms.readPropertyObjects(existingResource, false));
677        Map<String, CmsProperty> propsToWrite = new HashMap<>();
678        Set<String> keys = new HashSet<>();
679        keys.addAll(existingProps.keySet());
680        keys.addAll(importProps.keySet());
681
682        for (String key : keys) {
683            CmsProperty existingProp = existingProps.get(key);
684            CmsProperty importProp = importProps.get(key);
685            if (existingProp == null) {
686                propsToWrite.put(key, importProp);
687            } else if (importProp == null) {
688                propsToWrite.put(key, new CmsProperty(key, "", ""));
689            } else if (!existingProp.isIdentical(importProp)) {
690                propsToWrite.put(key, importProp);
691            }
692        }
693        return new ArrayList<>(propsToWrite.values());
694
695    }
696
697    /**
698     * Checks if a resource is link parseable.<P>
699     *
700     * @param importRes the resource to check
701     * @return true if the resource is link parseable
702     *
703     * @throws CmsException if something goes wrong
704     */
705    private boolean isLinkParsable(CmsResource importRes) throws CmsException {
706
707        int typeId = importRes.getTypeId();
708        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId);
709        return type instanceof I_CmsLinkParseable;
710
711    }
712
713    /**
714     * Locks a resource, or steals the lock if it's already locked.<p>
715     *
716     * @param cms the CMS context
717     * @param resource the resource to lock
718     * @throws CmsException if something goes wrong
719     */
720    private void lock(CmsObject cms, CmsResource resource) throws CmsException {
721
722        CmsLock lock = cms.getLock(resource);
723        if (lock.isUnlocked()) {
724            cms.lockResourceTemporary(resource);
725        } else {
726            cms.changeLock(resource);
727        }
728    }
729
730    /**
731     * Compares the relation (not defined in content) for a resource with those to be imported, and makes
732     * the necessary modifications.
733     *
734     * @param cms the CMS context
735     * @param importResource the resource
736     * @param relations the relations to be imported
737     *
738     * @throws CmsException if something goes wrong
739     */
740    private void updateRelations(CmsObject cms, CmsResource importResource, List<RelationData> relations)
741    throws CmsException {
742
743        Map<String, CmsRelationType> relTypes = new HashMap<>();
744        for (CmsRelationType relType : OpenCms.getResourceManager().getRelationTypes()) {
745            relTypes.put(relType.getName(), relType);
746        }
747        Set<CmsRelation> existingRelations = Sets.newHashSet(
748            cms.readRelations(CmsRelationFilter.relationsFromStructureId(importResource.getStructureId())));
749        List<CmsRelation> noContentRelations = existingRelations.stream().filter(
750            rel -> !rel.getType().isDefinedInContent()).collect(Collectors.toList());
751        Set<CmsRelation> newRelations = new HashSet<>();
752        for (RelationData rel : relations) {
753            if (!rel.getType().isDefinedInContent()) {
754                newRelations.add(
755                    new CmsRelation(
756                        importResource.getStructureId(),
757                        importResource.getRootPath(),
758                        rel.getTargetId(),
759                        rel.getTarget(),
760                        rel.getType()));
761            }
762        }
763        if (!newRelations.equals(noContentRelations)) {
764
765            CmsRelationFilter relFilter = CmsRelationFilter.TARGETS.filterNotDefinedInContent();
766
767            cms.deleteRelationsFromResource(importResource, relFilter);
768            for (CmsRelation newRel : newRelations) {
769                cms.addRelationToResource(
770                    importResource,
771                    cms.readResource(newRel.getTargetId(), CmsResourceFilter.IGNORE_EXPIRATION),
772                    newRel.getType().getName());
773
774            }
775        }
776    }
777
778}