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
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            for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
348                if (m_importIds.contains(resData.getResource().getStructureId())
349                    && !OpenCms.getResourceManager().matchResourceType(
350                        resData.getTypeName(),
351                        resData.getResource().getTypeId())) {
352                    if (OpenCms.getResourceManager().hasResourceType(resData.getTypeName())) {
353                        try {
354                            CmsResource res = cms.readResource(resData.getResource().getStructureId());
355                            cms.chtype(res, OpenCms.getResourceManager().getResourceType(resData.getTypeName()));
356                        } catch (Exception e) {
357                            m_report.println(e);
358                        }
359                    }
360                }
361            }
362            cms.unlockProject(importProject.getUuid());
363            OpenCms.getPublishManager().publishProject(cms, m_report);
364            OpenCms.getPublishManager().waitWhileRunning();
365            CmsModuleImportExportHandler.reportEndImport(m_report);
366        } catch (Exception e) {
367            m_report.println(e);
368        } finally {
369            cleanUp();
370        }
371    }
372
373    /**
374     * Updates the access control list fr a resource.<p>
375     *
376     * @param cms the current cms context
377     * @param resData the resource data
378     * @param resource the existing resource
379     * @return the resource
380     *
381     * @throws CmsException if something goes wrong
382     */
383    public boolean updateAcls(CmsObject cms, CmsResourceImportData resData, CmsResource resource) throws CmsException {
384
385        boolean changed = false;
386        Map<CmsUUID, CmsAccessControlEntry> importAces = buildAceMap(resData.getAccessControlEntries());
387
388        String path = cms.getSitePath(resource);
389        List<CmsAccessControlEntry> existingAcl = cms.getAccessControlEntries(path, false);
390        Map<CmsUUID, CmsAccessControlEntry> existingAces = buildAceMap(existingAcl);
391        Set<CmsUUID> keys = new HashSet<>(existingAces.keySet());
392        keys.addAll(importAces.keySet());
393        for (CmsUUID key : keys) {
394            CmsAccessControlEntry existingEntry = existingAces.get(key);
395            CmsAccessControlEntry newEntry = importAces.get(key);
396            if ((existingEntry == null)
397                || (newEntry == null)
398                || !existingEntry.withNulledResource().equals(newEntry.withNulledResource())) {
399                cms.importAccessControlEntries(resource, resData.getAccessControlEntries());
400                changed = true;
401                break;
402            }
403        }
404        return changed;
405    }
406
407    /**
408     * Creates the project used to import module resources and sets it on the CmsObject.
409     *
410     * @param cms the CmsObject to set the project on
411     * @param module the module
412     * @return the created project
413     * @throws CmsException if something goes wrong
414     */
415    protected CmsProject createAndSetModuleImportProject(CmsObject cms, CmsModule module) throws CmsException {
416
417        CmsProject importProject = cms.createProject(
418            org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
419                org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_NAME_1,
420                new Object[] {module.getName()}),
421            org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key(
422                org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_DESC_1,
423                new Object[] {module.getName()}),
424            OpenCms.getDefaultUsers().getGroupAdministrators(),
425            OpenCms.getDefaultUsers().getGroupAdministrators(),
426            CmsProject.PROJECT_TYPE_TEMPORARY);
427        cms.getRequestContext().setCurrentProject(importProject);
428        cms.copyResourceToProject("/");
429        return importProject;
430    }
431
432    /**
433     * Deletes and publishes resources with ID conflicts.
434     *
435     * @param cms the CMS context to use
436     * @param module the module
437     * @param conflictingIds the conflicting ids
438     * @throws CmsException if something goes wrong
439     * @throws Exception if something goes wrong
440     */
441    protected void deleteConflictingResources(CmsObject cms, CmsModule module, Map<CmsUUID, CmsUUID> conflictingIds)
442    throws CmsException, Exception {
443
444        CmsProject conflictProject = cms.createProject(
445            "Deletion of conflicting resources for " + module.getName(),
446            "Deletion of conflicting resources for " + module.getName(),
447            OpenCms.getDefaultUsers().getGroupAdministrators(),
448            OpenCms.getDefaultUsers().getGroupAdministrators(),
449            CmsProject.PROJECT_TYPE_TEMPORARY);
450        CmsObject deleteCms = OpenCms.initCmsObject(cms);
451        deleteCms.getRequestContext().setCurrentProject(conflictProject);
452        for (CmsUUID vfsId : conflictingIds.values()) {
453            CmsResource toDelete = deleteCms.readResource(vfsId, CmsResourceFilter.ALL);
454            lock(deleteCms, toDelete);
455            deleteCms.deleteResource(toDelete, CmsResource.DELETE_PRESERVE_SIBLINGS);
456        }
457        OpenCms.getPublishManager().publishProject(deleteCms);
458        OpenCms.getPublishManager().waitWhileRunning();
459    }
460
461    /**
462     * Parses links for XMLContents etc.
463     *
464     * @param cms the CMS context to use
465     * @throws CmsException if something goes wrong
466     */
467    protected void parseLinks(CmsObject cms) throws CmsException {
468
469        List<CmsResource> linkParseables = new ArrayList<>();
470        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
471            CmsResource importRes = resData.getImportResource();
472            if ((importRes != null) && m_importIds.contains(importRes.getStructureId()) && isLinkParsable(importRes)) {
473                linkParseables.add(importRes);
474            }
475        }
476        m_report.println(Messages.get().container(Messages.RPT_START_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE);
477        CmsImportVersion10.parseLinks(cms, linkParseables, m_report);
478        m_report.println(Messages.get().container(Messages.RPT_END_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE);
479    }
480
481    /**
482     * Handles the file deletions.
483     *
484     * @param cms the CMS context to use
485     * @param toDelete the resources to delete
486     *
487     * @throws CmsException if something goes wrong
488     */
489    protected void processDeletions(CmsObject cms, List<CmsResource> toDelete) throws CmsException {
490
491        Collections.sort(toDelete, (a, b) -> b.getRootPath().compareTo(a.getRootPath()));
492        for (CmsResource deleteRes : toDelete) {
493            m_report.print(
494                org.opencms.importexport.Messages.get().container(org.opencms.importexport.Messages.RPT_DELFOLDER_0),
495                I_CmsReport.FORMAT_NOTE);
496            m_report.print(
497                org.opencms.report.Messages.get().container(
498                    org.opencms.report.Messages.RPT_ARGUMENT_1,
499                    deleteRes.getRootPath()));
500            CmsLock lock = cms.getLock(deleteRes);
501            if (lock.isUnlocked()) {
502                lock(cms, deleteRes);
503            }
504            cms.deleteResource(deleteRes, CmsResource.DELETE_PRESERVE_SIBLINGS);
505            m_report.println(
506                org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
507                I_CmsReport.FORMAT_OK);
508
509        }
510    }
511
512    /**
513     * Processes single resource from module import data.
514     *
515     * @param cms the CMS context to use
516     * @param resData the resource data from the module import
517     * @param index index of the current import resource
518     */
519    protected void processImportResource(CmsObject cms, CmsResourceImportData resData, int index) {
520
521        boolean changed = false;
522        m_report.print(
523            org.opencms.report.Messages.get().container(
524                org.opencms.report.Messages.RPT_ARGUMENT_1,
525                "( " + index + " / " + m_moduleData.getResourceData().size() + " ) "),
526            I_CmsReport.FORMAT_NOTE);
527        m_report.print(Messages.get().container(Messages.RPT_IMPORTING_0), I_CmsReport.FORMAT_NOTE);
528        m_report.print(
529            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resData.getPath()));
530        m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
531        try {
532            CmsResource oldRes = null;
533            try {
534                if (resData.hasStructureId()) {
535                    oldRes = cms.readResource(
536                        resData.getResource().getStructureId(),
537                        CmsResourceFilter.IGNORE_EXPIRATION);
538                } else {
539                    oldRes = cms.readResource(resData.getPath(), CmsResourceFilter.IGNORE_EXPIRATION);
540                }
541            } catch (CmsVfsResourceNotFoundException e) {
542                LOG.debug(e.getLocalizedMessage(), e);
543            }
544            CmsResource currentRes = oldRes;
545            if (oldRes != null) {
546                String oldPath = cms.getSitePath(oldRes);
547                String newPath = resData.getPath();
548                if (!CmsStringUtil.comparePaths(oldPath, resData.getPath())) {
549                    cms.moveResource(oldPath, newPath);
550                    changed = true;
551                    currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION);
552                }
553            }
554            boolean needsImport = true;
555            boolean reducedExport = !resData.hasDateLastModified();
556            byte[] content = resData.getContent();
557            if (oldRes != null) {
558                if (!resData.hasStructureId()) {
559                    needsImport = false;
560                } else if (oldRes.getState().isUnchanged()
561                    && !needToUpdateResourceFields(oldRes, resData.getResource(), reducedExport)) {
562
563                        // if resource is changed or new, we don't want to go into this code block
564                        // because even if the content / metaadata are the same, we still want the file to be published at the end,
565                        // so we import it to add it to the current working project
566
567                        if (oldRes.isFile() && (content != null)) {
568                            CmsFile file = cms.readFile(oldRes);
569                            if (Arrays.equals(file.getContents(), content)) {
570                                needsImport = false;
571                            } else {
572                                LOG.debug("Content mismatch for " + file.getRootPath());
573                            }
574                        } else {
575                            needsImport = false;
576                        }
577                    }
578            }
579            if (needsImport || (oldRes == null)) { // oldRes null check is redundant, we just do it to remove the warning in Eclipse
580                currentRes = cms.importResource(
581                    resData.getPath(),
582                    resData.getResource(),
583                    content,
584                    new ArrayList<CmsProperty>());
585                changed = true;
586                m_importIds.add(currentRes.getStructureId());
587            } else {
588                currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.ALL);
589                CmsLock lock = cms.getLock(currentRes);
590                if (lock.isUnlocked()) {
591                    lock(cms, currentRes);
592                }
593            }
594            resData.setImportResource(currentRes);
595            List<CmsProperty> propsToWrite = compareProperties(cms, resData, currentRes);
596            if (!propsToWrite.isEmpty()) {
597                cms.writePropertyObjects(currentRes, propsToWrite);
598                changed = true;
599            }
600            changed |= updateAcls(cms, resData, currentRes);
601            if (changed) {
602                m_report.println(
603                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
604                    I_CmsReport.FORMAT_OK);
605            } else {
606                m_report.println(
607                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0),
608                    I_CmsReport.FORMAT_NOTE);
609            }
610
611        } catch (Exception e) {
612            m_report.println(e);
613            LOG.error(e.getLocalizedMessage(), e);
614
615        }
616    }
617
618    /**
619     * Runs the module import script.
620     *
621     * @param cms the CMS context to use
622     * @param module the module for which to run the script
623     */
624    protected void runImportScript(CmsObject cms, CmsModule module) {
625
626        LOG.info("Executing import script for module " + module.getName());
627        m_report.println(
628            org.opencms.module.Messages.get().container(org.opencms.module.Messages.RPT_IMPORT_SCRIPT_HEADER_0),
629            I_CmsReport.FORMAT_HEADLINE);
630        String importScript = "echo on\n" + module.getImportScript();
631        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
632        PrintStream out = new PrintStream(buffer);
633        CmsShell shell = new CmsShell(cms, "${user}@${project}:${siteroot}|${uri}>", null, out, out);
634        shell.execute(importScript);
635        String outputString = buffer.toString();
636        LOG.info("Shell output for import script was: \n" + outputString);
637        m_report.println(
638            org.opencms.module.Messages.get().container(
639                org.opencms.module.Messages.RPT_IMPORT_SCRIPT_OUTPUT_1,
640                outputString));
641    }
642
643    /**
644     * Converts access control list to map form, with principal ids as keys.<p>
645     *
646     * @param acl an access control list
647     * @return the map with the access control entries
648     */
649    Map<CmsUUID, CmsAccessControlEntry> buildAceMap(Collection<CmsAccessControlEntry> acl) {
650
651        if (acl == null) {
652            acl = new ArrayList<>();
653        }
654        Map<CmsUUID, CmsAccessControlEntry> result = new HashMap<>();
655        for (CmsAccessControlEntry ace : acl) {
656            result.put(ace.getPrincipal(), ace);
657        }
658        return result;
659    }
660
661    /**
662     * Cleans up temp files.
663     */
664    private void cleanUp() {
665
666        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
667            resData.cleanUp();
668        }
669    }
670
671    /**
672     * Compares properties of an existing resource with those to be imported, and returns a list of properties that need to be updated.<p>
673     *
674     * @param cms the current CMS context
675     * @param resData  the resource import data
676     * @param existingResource the existing resource
677     * @return the list of properties that need to be updated
678     *
679     * @throws CmsException if something goes wrong
680     */
681    private List<CmsProperty> compareProperties(
682        CmsObject cms,
683        CmsResourceImportData resData,
684        CmsResource existingResource)
685    throws CmsException {
686
687        if (existingResource == null) {
688            return Collections.emptyList();
689        }
690
691        Map<String, CmsProperty> importProps = resData.getProperties();
692        Map<String, CmsProperty> existingProps = CmsProperty.getPropertyMap(
693            cms.readPropertyObjects(existingResource, false));
694        Map<String, CmsProperty> propsToWrite = new HashMap<>();
695        Set<String> keys = new HashSet<>();
696        keys.addAll(existingProps.keySet());
697        keys.addAll(importProps.keySet());
698
699        for (String key : keys) {
700            if (existingResource.isFile() && CmsPropertyDefinition.PROPERTY_IMAGE_SIZE.equals(key)) {
701                // Depending on the configuration of the image loader, an image is potentially resized when importing/creating it,
702                // and the image.size property is set to the size of the resized image. However, the property value in the import may
703                // be from a system with different image loader settings, and thus may not correspond to the actual size of the image
704                // in the current system anymore, leading to problems with image scaling later.
705                //
706                // To prevent this state, we skip setting the image.size property for module updates.
707                continue;
708            }
709            CmsProperty existingProp = existingProps.get(key);
710            CmsProperty importProp = importProps.get(key);
711            if (existingProp == null) {
712                propsToWrite.put(key, importProp);
713            } else if (importProp == null) {
714                propsToWrite.put(key, new CmsProperty(key, "", ""));
715            } else if (!existingProp.isIdentical(importProp)) {
716                propsToWrite.put(key, importProp);
717            }
718        }
719        return new ArrayList<>(propsToWrite.values());
720
721    }
722
723    /**
724     * Checks if a resource is link parseable.<P>
725     *
726     * @param importRes the resource to check
727     * @return true if the resource is link parseable
728     *
729     * @throws CmsException if something goes wrong
730     */
731    private boolean isLinkParsable(CmsResource importRes) throws CmsException {
732
733        int typeId = importRes.getTypeId();
734        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId);
735        return type instanceof I_CmsLinkParseable;
736
737    }
738
739    /**
740     * Locks a resource, or steals the lock if it's already locked.<p>
741     *
742     * @param cms the CMS context
743     * @param resource the resource to lock
744     * @throws CmsException if something goes wrong
745     */
746    private void lock(CmsObject cms, CmsResource resource) throws CmsException {
747
748        CmsLock lock = cms.getLock(resource);
749        if (lock.isUnlocked()) {
750            cms.lockResourceTemporary(resource);
751        } else {
752            cms.changeLock(resource);
753        }
754    }
755
756    /**
757     * Compares list of existing relations with list of relations to import and returns true if they are different.
758     *
759     * @param noContentRelations the existing relations which are not in-content relations
760     * @param newRelations the relations to import
761     *
762     * @return true if the relations need to be updated
763     */
764    private boolean needToUpdateRelations(List<CmsRelation> noContentRelations, Set<CmsRelation> newRelations) {
765
766        if (noContentRelations.size() != newRelations.size()) {
767            return true;
768        }
769
770        for (CmsRelation relation : noContentRelations) {
771            if (!(newRelations.contains(relation) || newRelations.contains(relation.withTargetId(null)))) {
772                return true;
773            }
774        }
775
776        return false;
777    }
778
779    /**
780     * Compares the relation (not defined in content) for a resource with those to be imported, and makes
781     * the necessary modifications.
782     *
783     * @param cms the CMS context
784     * @param importResource the resource
785     * @param relations the relations to be imported
786     *
787     * @throws CmsException if something goes wrong
788     */
789    private void updateRelations(CmsObject cms, CmsResource importResource, List<RelationData> relations)
790    throws CmsException {
791
792        Map<String, CmsRelationType> relTypes = new HashMap<>();
793        for (CmsRelationType relType : OpenCms.getResourceManager().getRelationTypes()) {
794            relTypes.put(relType.getName(), relType);
795        }
796        Set<CmsRelation> existingRelations = Sets.newHashSet(
797            cms.readRelations(CmsRelationFilter.relationsFromStructureId(importResource.getStructureId())));
798        List<CmsRelation> noContentRelations = existingRelations.stream().filter(
799            rel -> !rel.getType().isDefinedInContent()).collect(Collectors.toList());
800        Set<CmsRelation> newRelations = new HashSet<>();
801        for (RelationData rel : relations) {
802            if (!rel.getType().isDefinedInContent()) {
803                newRelations.add(
804                    new CmsRelation(
805                        importResource.getStructureId(),
806                        importResource.getRootPath(),
807                        rel.getTargetId(),
808                        rel.getTarget(),
809                        rel.getType()));
810            }
811        }
812
813        if (needToUpdateRelations(noContentRelations, newRelations)) {
814
815            CmsRelationFilter relFilter = CmsRelationFilter.TARGETS.filterNotDefinedInContent();
816            try {
817                cms.deleteRelationsFromResource(importResource, relFilter);
818            } catch (CmsException e) {
819                LOG.error(e.getLocalizedMessage(), e);
820                m_report.println(e);
821            }
822
823            for (CmsRelation newRel : newRelations) {
824                try {
825                    CmsResource targetResource;
826                    if (newRel.getTargetId() != null) {
827                        targetResource = cms.readResource(newRel.getTargetId(), CmsResourceFilter.IGNORE_EXPIRATION);
828                    } else {
829                        try (AutoCloseable ac = cms.tempChangeSiteRoot("")) {
830                            targetResource = cms.readResource(
831                                newRel.getTargetPath(),
832                                CmsResourceFilter.IGNORE_EXPIRATION);
833                        }
834                    }
835                    if (targetResource != null) {
836                        cms.addRelationToResource(importResource, targetResource, newRel.getType().getName());
837                    }
838                } catch (Exception e) {
839                    LOG.error(e.getLocalizedMessage(), e);
840                    m_report.println(e);
841                }
842            }
843        }
844    }
845
846}