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                    m_report,
583                    resData.getResource(),
584                    content,
585                    new ArrayList<CmsProperty>());
586                changed = true;
587                m_importIds.add(currentRes.getStructureId());
588            } else {
589                currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.ALL);
590                CmsLock lock = cms.getLock(currentRes);
591                if (lock.isUnlocked()) {
592                    lock(cms, currentRes);
593                }
594            }
595            resData.setImportResource(currentRes);
596            List<CmsProperty> propsToWrite = compareProperties(cms, resData, currentRes);
597            if (!propsToWrite.isEmpty()) {
598                cms.writePropertyObjects(currentRes, propsToWrite);
599                changed = true;
600            }
601            changed |= updateAcls(cms, resData, currentRes);
602            if (changed) {
603                m_report.println(
604                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
605                    I_CmsReport.FORMAT_OK);
606            } else {
607                m_report.println(
608                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0),
609                    I_CmsReport.FORMAT_NOTE);
610            }
611
612        } catch (Exception e) {
613            m_report.println(e);
614            LOG.error(e.getLocalizedMessage(), e);
615
616        }
617    }
618
619    /**
620     * Runs the module import script.
621     *
622     * @param cms the CMS context to use
623     * @param module the module for which to run the script
624     */
625    protected void runImportScript(CmsObject cms, CmsModule module) {
626
627        LOG.info("Executing import script for module " + module.getName());
628        m_report.println(
629            org.opencms.module.Messages.get().container(org.opencms.module.Messages.RPT_IMPORT_SCRIPT_HEADER_0),
630            I_CmsReport.FORMAT_HEADLINE);
631        String importScript = "echo on\n" + module.getImportScript();
632        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
633        PrintStream out = new PrintStream(buffer);
634        CmsShell shell = new CmsShell(cms, "${user}@${project}:${siteroot}|${uri}>", null, out, out);
635        shell.execute(importScript);
636        String outputString = buffer.toString();
637        LOG.info("Shell output for import script was: \n" + outputString);
638        m_report.println(
639            org.opencms.module.Messages.get().container(
640                org.opencms.module.Messages.RPT_IMPORT_SCRIPT_OUTPUT_1,
641                outputString));
642    }
643
644    /**
645     * Converts access control list to map form, with principal ids as keys.<p>
646     *
647     * @param acl an access control list
648     * @return the map with the access control entries
649     */
650    Map<CmsUUID, CmsAccessControlEntry> buildAceMap(Collection<CmsAccessControlEntry> acl) {
651
652        if (acl == null) {
653            acl = new ArrayList<>();
654        }
655        Map<CmsUUID, CmsAccessControlEntry> result = new HashMap<>();
656        for (CmsAccessControlEntry ace : acl) {
657            result.put(ace.getPrincipal(), ace);
658        }
659        return result;
660    }
661
662    /**
663     * Cleans up temp files.
664     */
665    private void cleanUp() {
666
667        for (CmsResourceImportData resData : m_moduleData.getResourceData()) {
668            resData.cleanUp();
669        }
670    }
671
672    /**
673     * Compares properties of an existing resource with those to be imported, and returns a list of properties that need to be updated.<p>
674     *
675     * @param cms the current CMS context
676     * @param resData  the resource import data
677     * @param existingResource the existing resource
678     * @return the list of properties that need to be updated
679     *
680     * @throws CmsException if something goes wrong
681     */
682    private List<CmsProperty> compareProperties(
683        CmsObject cms,
684        CmsResourceImportData resData,
685        CmsResource existingResource)
686    throws CmsException {
687
688        if (existingResource == null) {
689            return Collections.emptyList();
690        }
691
692        Map<String, CmsProperty> importProps = resData.getProperties();
693        Map<String, CmsProperty> existingProps = CmsProperty.getPropertyMap(
694            cms.readPropertyObjects(existingResource, false));
695        Map<String, CmsProperty> propsToWrite = new HashMap<>();
696        Set<String> keys = new HashSet<>();
697        keys.addAll(existingProps.keySet());
698        keys.addAll(importProps.keySet());
699
700        for (String key : keys) {
701            if (existingResource.isFile() && CmsPropertyDefinition.PROPERTY_IMAGE_SIZE.equals(key)) {
702                // Depending on the configuration of the image loader, an image is potentially resized when importing/creating it,
703                // and the image.size property is set to the size of the resized image. However, the property value in the import may
704                // be from a system with different image loader settings, and thus may not correspond to the actual size of the image
705                // in the current system anymore, leading to problems with image scaling later.
706                //
707                // To prevent this state, we skip setting the image.size property for module updates.
708                continue;
709            }
710            CmsProperty existingProp = existingProps.get(key);
711            CmsProperty importProp = importProps.get(key);
712            if (existingProp == null) {
713                propsToWrite.put(key, importProp);
714            } else if (importProp == null) {
715                propsToWrite.put(key, new CmsProperty(key, "", ""));
716            } else if (!existingProp.isIdentical(importProp)) {
717                propsToWrite.put(key, importProp);
718            }
719        }
720        return new ArrayList<>(propsToWrite.values());
721
722    }
723
724    /**
725     * Checks if a resource is link parseable.<P>
726     *
727     * @param importRes the resource to check
728     * @return true if the resource is link parseable
729     *
730     * @throws CmsException if something goes wrong
731     */
732    private boolean isLinkParsable(CmsResource importRes) throws CmsException {
733
734        int typeId = importRes.getTypeId();
735        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId);
736        return type instanceof I_CmsLinkParseable;
737
738    }
739
740    /**
741     * Locks a resource, or steals the lock if it's already locked.<p>
742     *
743     * @param cms the CMS context
744     * @param resource the resource to lock
745     * @throws CmsException if something goes wrong
746     */
747    private void lock(CmsObject cms, CmsResource resource) throws CmsException {
748
749        CmsLock lock = cms.getLock(resource);
750        if (lock.isUnlocked()) {
751            cms.lockResourceTemporary(resource);
752        } else {
753            cms.changeLock(resource);
754        }
755    }
756
757    /**
758     * Compares list of existing relations with list of relations to import and returns true if they are different.
759     *
760     * @param noContentRelations the existing relations which are not in-content relations
761     * @param newRelations the relations to import
762     *
763     * @return true if the relations need to be updated
764     */
765    private boolean needToUpdateRelations(List<CmsRelation> noContentRelations, Set<CmsRelation> newRelations) {
766
767        if (noContentRelations.size() != newRelations.size()) {
768            return true;
769        }
770
771        for (CmsRelation relation : noContentRelations) {
772            if (!(newRelations.contains(relation) || newRelations.contains(relation.withTargetId(null)))) {
773                return true;
774            }
775        }
776
777        return false;
778    }
779
780    /**
781     * Compares the relation (not defined in content) for a resource with those to be imported, and makes
782     * the necessary modifications.
783     *
784     * @param cms the CMS context
785     * @param importResource the resource
786     * @param relations the relations to be imported
787     *
788     * @throws CmsException if something goes wrong
789     */
790    private void updateRelations(CmsObject cms, CmsResource importResource, List<RelationData> relations)
791    throws CmsException {
792
793        Map<String, CmsRelationType> relTypes = new HashMap<>();
794        for (CmsRelationType relType : OpenCms.getResourceManager().getRelationTypes()) {
795            relTypes.put(relType.getName(), relType);
796        }
797        Set<CmsRelation> existingRelations = Sets.newHashSet(
798            cms.readRelations(CmsRelationFilter.relationsFromStructureId(importResource.getStructureId())));
799        List<CmsRelation> noContentRelations = existingRelations.stream().filter(
800            rel -> !rel.getType().isDefinedInContent()).collect(Collectors.toList());
801        Set<CmsRelation> newRelations = new HashSet<>();
802        for (RelationData rel : relations) {
803            if (!rel.getType().isDefinedInContent()) {
804                newRelations.add(
805                    new CmsRelation(
806                        importResource.getStructureId(),
807                        importResource.getRootPath(),
808                        rel.getTargetId(),
809                        rel.getTarget(),
810                        rel.getType()));
811            }
812        }
813
814        if (needToUpdateRelations(noContentRelations, newRelations)) {
815
816            CmsRelationFilter relFilter = CmsRelationFilter.TARGETS.filterNotDefinedInContent();
817            try {
818                cms.deleteRelationsFromResource(importResource, relFilter);
819            } catch (CmsException e) {
820                LOG.error(e.getLocalizedMessage(), e);
821                m_report.println(e);
822            }
823
824            for (CmsRelation newRel : newRelations) {
825                try {
826                    CmsResource targetResource;
827                    if (newRel.getTargetId() != null) {
828                        targetResource = cms.readResource(newRel.getTargetId(), CmsResourceFilter.IGNORE_EXPIRATION);
829                    } else {
830                        try (AutoCloseable ac = cms.tempChangeSiteRoot("")) {
831                            targetResource = cms.readResource(
832                                newRel.getTargetPath(),
833                                CmsResourceFilter.IGNORE_EXPIRATION);
834                        }
835                    }
836                    if (targetResource != null) {
837                        cms.addRelationToResource(importResource, targetResource, newRel.getType().getName());
838                    }
839                } catch (Exception e) {
840                    LOG.error(e.getLocalizedMessage(), e);
841                    m_report.println(e);
842                }
843            }
844        }
845    }
846
847}