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