001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.ade.containerpage;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsFormatterUtils;
032import org.opencms.ade.configuration.CmsResourceTypeConfig;
033import org.opencms.ade.containerpage.shared.CmsContainerElement;
034import org.opencms.ade.containerpage.shared.CmsContainerElement.ModelGroupState;
035import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
036import org.opencms.file.CmsObject;
037import org.opencms.file.CmsProperty;
038import org.opencms.file.CmsPropertyDefinition;
039import org.opencms.file.CmsResource;
040import org.opencms.file.CmsResourceFilter;
041import org.opencms.file.CmsUser;
042import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
043import org.opencms.file.types.I_CmsResourceType;
044import org.opencms.flex.CmsFlexController;
045import org.opencms.i18n.CmsEncoder;
046import org.opencms.lock.CmsLock;
047import org.opencms.main.CmsException;
048import org.opencms.main.CmsLog;
049import org.opencms.main.OpenCms;
050import org.opencms.util.CmsStringUtil;
051import org.opencms.util.CmsUUID;
052import org.opencms.xml.containerpage.CmsADESessionCache;
053import org.opencms.xml.containerpage.CmsContainerBean;
054import org.opencms.xml.containerpage.CmsContainerElementBean;
055import org.opencms.xml.containerpage.CmsContainerPageBean;
056import org.opencms.xml.containerpage.CmsXmlContainerPage;
057import org.opencms.xml.containerpage.CmsXmlContainerPageFactory;
058import org.opencms.xml.containerpage.I_CmsFormatterBean;
059
060import java.io.IOException;
061import java.util.ArrayList;
062import java.util.Collections;
063import java.util.HashMap;
064import java.util.HashSet;
065import java.util.List;
066import java.util.Locale;
067import java.util.Map;
068import java.util.Map.Entry;
069import java.util.Set;
070
071import org.apache.commons.logging.Log;
072
073import jakarta.servlet.http.HttpServletRequest;
074import jakarta.servlet.http.HttpServletResponse;
075
076/**
077 * Handles all model group specific tasks.<p>
078 */
079public class CmsModelGroupHelper {
080
081    /** The name of the container storing the groups base element. */
082    public static final String MODEL_GROUP_BASE_CONTAINER = "base_container";
083
084    /** Static reference to the log. */
085    private static final Log LOG = CmsLog.getLog(CmsModelGroupHelper.class);
086
087    /** Settings to keep when resetting. */
088    private static final String[] KEEP_SETTING_IDS = new String[] {
089        CmsContainerElement.MODEL_GROUP_STATE,
090        CmsContainerElement.ELEMENT_INSTANCE_ID,
091        CmsContainerElement.USE_AS_COPY_MODEL};
092
093    /** The current cms context. */
094    private CmsObject m_cms;
095
096    /** The session cache instance. */
097    private CmsADESessionCache m_sessionCache;
098
099    /** The configuration data of the current container page location. */
100    private CmsADEConfigData m_configData;
101
102    /** Indicating the edit model groups mode. */
103    private boolean m_isEditingModelGroups;
104
105    /**
106     * Constructor.<p>
107     *
108     * @param cms the current cms context
109     * @param configData the configuration data
110     * @param sessionCache the session cache
111     * @param isEditingModelGroups the edit model groups flag
112     */
113    public CmsModelGroupHelper(
114        CmsObject cms,
115        CmsADEConfigData configData,
116        CmsADESessionCache sessionCache,
117        boolean isEditingModelGroups) {
118
119        m_cms = cms;
120        m_sessionCache = sessionCache;
121        m_configData = configData;
122        m_isEditingModelGroups = isEditingModelGroups;
123    }
124
125    /**
126     * Creates a new model group resource.<p>
127     *
128     * @param cms the current cms context
129     * @param configData the configuration data
130     *
131     * @return the new resource
132     *
133     * @throws CmsException in case creating the resource fails
134     */
135    public static CmsResource createModelGroup(CmsObject cms, CmsADEConfigData configData) throws CmsException {
136
137        CmsResourceTypeConfig typeConfig = configData.getResourceType(
138            CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME);
139        return typeConfig.createNewElement(cms, configData.getBasePath());
140    }
141
142    /**
143     * Returns if the given resource is a model group resource.<p>
144     *
145     * @param resource the resource
146     *
147     * @return <code>true</code> if the given resource is a model group resource
148     */
149    public static boolean isModelGroupResource(CmsResource resource) {
150
151        return CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME.equals(
152            OpenCms.getResourceManager().getResourceType(resource).getTypeName());
153    }
154
155    /**
156     * Updates a model group resource to the changed data structure.<p>
157     * This step is necessary when updating from version 10.0.x to 10.5.x.<p>
158     *
159     * @param cms the cms context
160     * @param group the model group resource
161     * @param baseContainerName the new base container name
162     *
163     * @return <code>true</code> if the resource was updated
164     */
165    public static boolean updateModelGroupResource(CmsObject cms, CmsResource group, String baseContainerName) {
166
167        if (!isModelGroupResource(group)) {
168            // skip resources that are no model group
169            return false;
170        }
171        try {
172            CmsXmlContainerPage xmlContainerPage = CmsXmlContainerPageFactory.unmarshal(cms, group);
173            CmsContainerPageBean pageBean = xmlContainerPage.getContainerPage(cms);
174
175            CmsContainerBean baseContainer = pageBean.getContainers().get(MODEL_GROUP_BASE_CONTAINER);
176            boolean changedContent = false;
177            if ((baseContainer != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(baseContainerName)) {
178
179                List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
180                for (CmsContainerBean container : pageBean.getContainers().values()) {
181                    if (container.getName().equals(MODEL_GROUP_BASE_CONTAINER)) {
182                        CmsContainerBean replacer = new CmsContainerBean(
183                            baseContainerName,
184                            container.getType(),
185                            container.getParentInstanceId(),
186                            container.isRootContainer(),
187                            container.getElements());
188                        containers.add(replacer);
189                        changedContent = true;
190                    } else {
191                        containers.add(container);
192                    }
193                }
194                if (changedContent) {
195                    pageBean = new CmsContainerPageBean(containers);
196                }
197            }
198            if (changedContent) {
199                ensureLock(cms, group);
200
201                if (changedContent) {
202                    xmlContainerPage.save(cms, pageBean);
203                }
204                if (group.getName().endsWith(".xml")) {
205                    // renaming model groups so they will be rendered correctly by the browser
206                    String targetPath = cms.getSitePath(group);
207                    targetPath = targetPath.substring(0, targetPath.length() - 4) + ".html";
208                    cms.renameResource(cms.getSitePath(group), targetPath);
209                    group = cms.readResource(group.getStructureId());
210                }
211                tryUnlock(cms, group);
212                return true;
213            }
214            return false;
215
216        } catch (CmsException e) {
217            LOG.error(e.getLocalizedMessage(), e);
218            return false;
219        }
220
221    }
222
223    /**
224     * Updates model group resources to the changed data structure.<p>
225     * This step is necessary when updating from version 10.0.x to 10.5.x.<p>
226     *
227     * @param request the request
228     * @param response the response
229     * @param basePath the path to the model group, or the base path to search for model groups
230     * @param baseContainerName the new base container name
231     *
232     * @throws IOException in case writing to the response fails
233     */
234    @SuppressWarnings("resource")
235    public static void updateModelGroupResources(
236        HttpServletRequest request,
237        HttpServletResponse response,
238        String basePath,
239        String baseContainerName)
240    throws IOException {
241
242        if (CmsFlexController.isCmsRequest(request)) {
243
244            try {
245                CmsFlexController controller = CmsFlexController.getController(request);
246                CmsObject cms = controller.getCmsObject();
247                CmsResource base = cms.readResource(basePath);
248                List<CmsResource> resources;
249                I_CmsResourceType groupType = OpenCms.getResourceManager().getResourceType(
250                    CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME);
251                if (base.isFolder()) {
252                    resources = cms.readResources(
253                        basePath,
254                        CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(groupType));
255                } else if (OpenCms.getResourceManager().getResourceType(base).equals(groupType)) {
256                    resources = Collections.singletonList(base);
257                } else {
258                    resources = Collections.emptyList();
259                }
260
261                if (resources.isEmpty()) {
262                    response.getWriter().println(
263                        "No model group resources found at " + CmsEncoder.escapeXml(basePath) + "<br />");
264                } else {
265                    for (CmsResource group : resources) {
266                        boolean updated = updateModelGroupResource(cms, group, baseContainerName);
267                        response.getWriter().println(
268                            "Group '" + group.getRootPath() + "' was updated " + updated + "<br />");
269                    }
270                }
271            } catch (CmsException e) {
272                LOG.error(e.getLocalizedMessage(), e);
273                e.printStackTrace(response.getWriter());
274            }
275        }
276    }
277
278    /**
279     * Locks the given resource.<p>
280     *
281     * @param cms the cms context
282     * @param resource the resource to lock
283     *
284     * @throws CmsException in case locking fails
285     */
286    private static void ensureLock(CmsObject cms, CmsResource resource) throws CmsException {
287
288        CmsUser user = cms.getRequestContext().getCurrentUser();
289        CmsLock lock = cms.getLock(resource);
290        if (!lock.isOwnedBy(user)) {
291            cms.lockResourceTemporary(resource);
292        } else if (!lock.isOwnedInProjectBy(user, cms.getRequestContext().getCurrentProject())) {
293            cms.changeLock(resource);
294        }
295    }
296
297    /**
298     * Tries to unlock a resource.<p>
299     *
300     * @param cms the cms context
301     * @param resource the resource to unlock
302     */
303    private static void tryUnlock(CmsObject cms, CmsResource resource) {
304
305        try {
306            cms.unlockResource(resource);
307        } catch (CmsException e) {
308            LOG.debug("Unable to unlock " + resource.getRootPath(), e);
309        }
310    }
311
312    /**
313     * Adds the model group elements to the page.<p>
314     *
315     * @param elements the requested elements
316     * @param foundGroups list to add the found group element client ids to
317     * @param page the page
318     * @param alwaysCopy <code>true</code> to create element copies in case of non model groups and createNew is set
319     * @param locale the content locale
320     * @param createContextPath the context path to pass to CmsResourceTypeConfig#createNewElement
321     *
322     * @return the adjusted page
323     *
324     * @throws CmsException in case something goes wrong
325     */
326    public CmsContainerPageBean prepareforModelGroupContent(
327        Map<String, CmsContainerElementBean> elements,
328        List<String> foundGroups,
329        CmsContainerPageBean page,
330        boolean alwaysCopy,
331        Locale locale,
332        String createContextPath)
333    throws CmsException {
334
335        for (Entry<String, CmsContainerElementBean> entry : elements.entrySet()) {
336            CmsContainerElementBean element = entry.getValue();
337            CmsContainerPageBean modelPage = null;
338            String modelInstanceId = null;
339            boolean foundInstance = false;
340            if (CmsModelGroupHelper.isModelGroupResource(element.getResource())) {
341                modelPage = getContainerPageBean(element.getResource());
342                CmsContainerElementBean baseElement = getModelBaseElement(modelPage, element.getResource());
343                if (baseElement == null) {
344                    break;
345                }
346                String baseInstanceId = baseElement.getInstanceId();
347                String originalInstanceId = element.getInstanceId();
348                element = getModelReplacementElement(element, baseElement, true);
349                List<CmsContainerBean> modelContainers = readModelContainers(
350                    baseInstanceId,
351                    originalInstanceId,
352                    modelPage,
353                    baseElement.isCopyModel());
354                if (!m_isEditingModelGroups && baseElement.isCopyModel()) {
355                    modelContainers = createNewElementsForModelGroup(m_cms, modelContainers, locale, createContextPath);
356                }
357                modelContainers.addAll(page.getContainers().values());
358                page = new CmsContainerPageBean(modelContainers);
359                // update the entry element value, as the settings will have changed
360                entry.setValue(element);
361                if (m_sessionCache != null) {
362                    // also update the session cache
363                    m_sessionCache.setCacheContainerElement(element.editorHash(), element);
364                }
365            } else {
366                // here we need to make sure to remove the source container page setting and to set a new element instance id
367
368                Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
369                String source = settings.get(CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING);
370                settings.remove(CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING);
371                // TODO: Make sure source id is available for second call
372
373                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(source)) {
374                    try {
375                        CmsUUID sourceId = new CmsUUID(source);
376                        CmsResource sourcePage = m_cms.readResource(sourceId);
377                        if (CmsResourceTypeXmlContainerPage.isContainerPage(sourcePage)) {
378                            CmsXmlContainerPage xmlCnt = CmsXmlContainerPageFactory.unmarshal(
379                                m_cms,
380                                m_cms.readFile(sourcePage));
381                            modelPage = xmlCnt.getContainerPage(m_cms);
382                            modelInstanceId = element.getInstanceId();
383                        }
384
385                        settings.remove(CmsContainerElement.ELEMENT_INSTANCE_ID);
386
387                        boolean copyRoot = false;
388                        if (alwaysCopy && (modelInstanceId != null) && (modelPage != null)) {
389                            for (CmsContainerElementBean el : modelPage.getElements()) {
390                                if (modelInstanceId.equals(el.getInstanceId())) {
391                                    copyRoot = el.isCreateNew();
392                                    break;
393                                }
394                            }
395                        }
396
397                        if (copyRoot) {
398                            CmsObject cloneCms = OpenCms.initCmsObject(m_cms);
399                            cloneCms.getRequestContext().setLocale(locale);
400                            String typeName = OpenCms.getResourceManager().getResourceType(
401                                element.getResource()).getTypeName();
402                            CmsResourceTypeConfig typeConfig = m_configData.getResourceType(typeName);
403                            if (typeConfig == null) {
404                                throw new IllegalArgumentException(
405                                    "Can not copy template model element '"
406                                        + element.getResource().getRootPath()
407                                        + "' because the resource type '"
408                                        + typeName
409                                        + "' is not available in this sitemap.");
410                            }
411                            CmsResource newResource = typeConfig.createNewElement(
412                                cloneCms,
413                                element.getResource(),
414                                createContextPath);
415
416                            element = new CmsContainerElementBean(
417                                newResource.getStructureId(),
418                                element.getFormatterId(),
419                                settings,
420                                false);
421                        } else {
422                            element = CmsContainerElementBean.cloneWithSettings(element, settings);
423                        }
424                        if (modelPage != null) {
425                            Map<String, List<CmsContainerBean>> containerByParent = new HashMap<String, List<CmsContainerBean>>();
426
427                            for (CmsContainerBean container : modelPage.getContainers().values()) {
428                                if (container.getParentInstanceId() != null) {
429                                    if (!containerByParent.containsKey(container.getParentInstanceId())) {
430                                        containerByParent.put(
431                                            container.getParentInstanceId(),
432                                            new ArrayList<CmsContainerBean>());
433                                    }
434                                    containerByParent.get(container.getParentInstanceId()).add(container);
435                                }
436                                if (!foundInstance) {
437                                    for (CmsContainerElementBean child : container.getElements()) {
438                                        if (modelInstanceId == null) {
439                                            if (child.getId().equals(element.getId())) {
440                                                modelInstanceId = child.getInstanceId();
441                                                foundInstance = true;
442                                                // we also want to keep the settings of the model group
443                                                Map<String, String> setting = new HashMap<String, String>(
444                                                    child.getIndividualSettings());
445                                                setting.remove(CmsContainerElement.ELEMENT_INSTANCE_ID);
446                                                element = CmsContainerElementBean.cloneWithSettings(element, setting);
447                                                break;
448                                            }
449                                        } else {
450                                            if (modelInstanceId.equals(child.getInstanceId())) {
451                                                foundInstance = true;
452                                                break;
453                                            }
454                                        }
455                                    }
456                                }
457                            }
458                            if (foundInstance && containerByParent.containsKey(modelInstanceId)) {
459                                List<CmsContainerBean> modelContainers = collectModelStructure(
460                                    modelInstanceId,
461                                    element.getInstanceId(),
462                                    containerByParent,
463                                    true);
464                                if (alwaysCopy) {
465                                    modelContainers = createNewElementsForModelGroup(
466                                        m_cms,
467                                        modelContainers,
468                                        locale,
469                                        createContextPath);
470                                }
471                                foundGroups.add(element.editorHash());
472                                modelContainers.addAll(page.getContainers().values());
473                                page = new CmsContainerPageBean(modelContainers);
474                            }
475                        }
476
477                        // update the entry element value, as the settings will have changed
478                        entry.setValue(element);
479                        if (m_sessionCache != null) {
480                            // also update the session cache
481                            m_sessionCache.setCacheContainerElement(element.editorHash(), element);
482                        }
483                    } catch (Exception e) {
484                        LOG.info(e.getLocalizedMessage(), e);
485                    }
486
487                }
488            }
489
490        }
491        return page;
492    }
493
494    /**
495     * Reads the present model groups and merges their containers into the page.<p>
496     *
497     * @param page the container page
498     *
499     * @return the resulting container page
500     */
501    public CmsContainerPageBean readModelGroups(CmsContainerPageBean page) {
502
503        List<CmsContainerBean> resultContainers = new ArrayList<CmsContainerBean>();
504        for (CmsContainerBean container : page.getContainers().values()) {
505            boolean hasModels = false;
506            List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
507            for (CmsContainerElementBean element : container.getElements()) {
508                try {
509                    element.initResource(m_cms);
510                    if (isModelGroupResource(element.getResource())) {
511                        hasModels = true;
512                        CmsContainerPageBean modelGroupPage = getContainerPageBean(element.getResource());
513                        CmsContainerElementBean baseElement = getModelBaseElement(
514                            modelGroupPage,
515                            element.getResource());
516                        if (baseElement == null) {
517                            LOG.error(
518                                "Error rendering model group '"
519                                    + element.getResource().getRootPath()
520                                    + "', base element could not be determind.");
521                            continue;
522                        }
523                        String baseInstanceId = baseElement.getInstanceId();
524                        CmsContainerElementBean replaceElement = getModelReplacementElement(
525                            element,
526                            baseElement,
527                            false);
528                        if (m_sessionCache != null) {
529                            m_sessionCache.setCacheContainerElement(replaceElement.editorHash(), replaceElement);
530                        }
531                        elements.add(replaceElement);
532                        resultContainers.addAll(
533                            readModelContainers(
534                                baseInstanceId,
535                                element.getInstanceId(),
536                                modelGroupPage,
537                                baseElement.isCopyModel()));
538                    } else {
539                        elements.add(element);
540                    }
541                } catch (CmsException e) {
542                    LOG.info(e.getLocalizedMessage(), e);
543                }
544            }
545            if (hasModels) {
546                resultContainers.add(container.copyWithNewElements(elements));
547            } else {
548                resultContainers.add(container);
549            }
550        }
551        return new CmsContainerPageBean(resultContainers);
552    }
553
554    /**
555     * Removes the model group containers.<p>
556     *
557     * @param page the container page state
558     *
559     * @return the container page without the model group containers
560     */
561    public CmsContainerPageBean removeModelGroupContainers(CmsContainerPageBean page) {
562
563        Map<String, List<CmsContainerBean>> containersByParent = getContainerByParent(page);
564        Set<String> modelInstances = new HashSet<String>();
565        for (CmsContainerElementBean element : page.getElements()) {
566            if (element.getIndividualSettings().containsKey(CmsContainerElement.MODEL_GROUP_ID)) {
567                modelInstances.add(element.getInstanceId());
568            }
569        }
570
571        Set<String> descendingInstances = new HashSet<String>();
572        for (String modelInstance : modelInstances) {
573            descendingInstances.addAll(collectDescendingInstances(modelInstance, containersByParent));
574        }
575        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
576        for (CmsContainerBean container : page.getContainers().values()) {
577            if ((container.getParentInstanceId() == null)
578                || !descendingInstances.contains(container.getParentInstanceId())) {
579                // iterate the container elements to replace the model group elements
580                List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
581                for (CmsContainerElementBean element : container.getElements()) {
582                    if (modelInstances.contains(element.getInstanceId())) {
583                        CmsUUID modelId = new CmsUUID(
584                            element.getIndividualSettings().get(CmsContainerElement.MODEL_GROUP_ID));
585                        CmsContainerElementBean replacer = new CmsContainerElementBean(
586                            modelId,
587                            element.getFormatterId(),
588                            element.getIndividualSettings(),
589                            false);
590                        elements.add(replacer);
591                    } else {
592                        elements.add(element);
593                    }
594                }
595                containers.add(container.copyWithNewElements(elements));
596            }
597        }
598        return new CmsContainerPageBean(containers);
599    }
600
601    /**
602     * Saves the model groups of the given container page.<p>
603     *
604     * @param page the container page
605     * @param pageResource the model group resource
606     *
607     * @return the container page referencing the saved model groups
608     *
609     * @throws CmsException in case writing the page properties fails
610     */
611    public CmsContainerPageBean saveModelGroups(CmsContainerPageBean page, CmsResource pageResource)
612    throws CmsException {
613
614        CmsUUID modelElementId = null;
615        CmsContainerElementBean baseElement = null;
616        for (CmsContainerElementBean element : page.getElements()) {
617            if (element.isModelGroup()) {
618                modelElementId = element.getId();
619                baseElement = element;
620                break;
621
622            }
623        }
624        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
625        for (CmsContainerBean container : page.getContainers().values()) {
626            List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
627            boolean hasChanges = false;
628            for (CmsContainerElementBean element : container.getElements()) {
629                if (element.isModelGroup() && !element.getId().equals(modelElementId)) {
630                    // there should not be another model group element, remove the model group settings
631                    Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
632                    settings.remove(CmsContainerElement.MODEL_GROUP_ID);
633                    settings.remove(CmsContainerElement.MODEL_GROUP_STATE);
634                    elements.add(
635                        new CmsContainerElementBean(element.getId(), element.getFormatterId(), settings, false));
636                    hasChanges = true;
637                } else {
638                    elements.add(element);
639                }
640            }
641            if (hasChanges) {
642                containers.add(container.copyWithNewElements(elements));
643            } else {
644                containers.add(container);
645            }
646
647        }
648
649        List<CmsProperty> changedProps = new ArrayList<CmsProperty>();
650        if (baseElement != null) {
651            String val = Boolean.parseBoolean(
652                baseElement.getIndividualSettings().get(CmsContainerElement.USE_AS_COPY_MODEL))
653                ? CmsContainerElement.USE_AS_COPY_MODEL
654                : "";
655            changedProps.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_TEMPLATE_ELEMENTS, val, val));
656        }
657        m_cms.writePropertyObjects(pageResource, changedProps);
658
659        return new CmsContainerPageBean(containers);
660
661    }
662
663    /**
664     * Adjusts formatter settings and initializes a new instance id for the given container element.<p>
665     *
666     * @param element the container element
667     * @param originalContainer the original parent container name
668     * @param adjustedContainer the target container name
669     * @param setNewInstanceId <code>true</code> to set a new instance id
670     *
671     * @return the new element instance
672     */
673    private CmsContainerElementBean adjustSettings(
674        CmsContainerElementBean element,
675        String originalContainer,
676        String adjustedContainer,
677        boolean setNewInstanceId) {
678
679        Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
680        if (setNewInstanceId) {
681            settings.put(CmsContainerElement.ELEMENT_INSTANCE_ID, new CmsUUID().toString());
682        }
683        String formatterId = settings.remove(CmsFormatterConfig.getSettingsKeyForContainer(originalContainer));
684        settings.put(CmsFormatterConfig.getSettingsKeyForContainer(adjustedContainer), formatterId);
685        return CmsContainerElementBean.cloneWithSettings(element, settings);
686    }
687
688    /**
689     * Returns the descending instance id's to the given element instance.<p>
690     *
691     * @param instanceId the instance id
692     * @param containersByParent the container page containers by parent instance id
693     *
694     * @return the containers
695     */
696    private Set<String> collectDescendingInstances(
697        String instanceId,
698        Map<String, List<CmsContainerBean>> containersByParent) {
699
700        Set<String> descendingInstances = new HashSet<String>();
701        descendingInstances.add(instanceId);
702        if (containersByParent.containsKey(instanceId)) {
703            for (CmsContainerBean container : containersByParent.get(instanceId)) {
704                for (CmsContainerElementBean element : container.getElements()) {
705                    descendingInstances.addAll(collectDescendingInstances(element.getInstanceId(), containersByParent));
706                }
707            }
708        }
709        return descendingInstances;
710    }
711
712    /**
713     * Collects the model group structure.<p>
714     *
715     * @param modelInstanceId the model instance id
716     * @param replaceModelId the local instance id
717     * @param containerByParent the model group page containers by parent instance id
718     * @param isCopyGroup <code>true</code> in case of a copy group
719     *
720     * @return the collected containers
721     */
722    private List<CmsContainerBean> collectModelStructure(
723        String modelInstanceId,
724        String replaceModelId,
725        Map<String, List<CmsContainerBean>> containerByParent,
726        boolean isCopyGroup) {
727
728        List<CmsContainerBean> result = new ArrayList<CmsContainerBean>();
729
730        if (containerByParent.containsKey(modelInstanceId)) {
731            for (CmsContainerBean container : containerByParent.get(modelInstanceId)) {
732                String adjustedContainerName = replaceModelId + container.getName().substring(modelInstanceId.length());
733
734                List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
735                for (CmsContainerElementBean element : container.getElements()) {
736                    CmsContainerElementBean copyElement = adjustSettings(
737                        element,
738                        container.getName(),
739                        adjustedContainerName,
740                        isCopyGroup);
741                    if (m_sessionCache != null) {
742                        m_sessionCache.setCacheContainerElement(copyElement.editorHash(), copyElement);
743                    }
744                    elements.add(copyElement);
745                    result.addAll(
746                        collectModelStructure(
747                            element.getInstanceId(),
748                            copyElement.getInstanceId(),
749                            containerByParent,
750                            isCopyGroup));
751                }
752
753                result.add(
754                    new CmsContainerBean(
755                        adjustedContainerName,
756                        container.getType(),
757                        replaceModelId,
758                        container.isRootContainer(),
759                        container.getMaxElements(),
760                        elements));
761            }
762        }
763        return result;
764    }
765
766    /**
767     * Creates new resources for elements marked with create as new.<p>
768     *
769     * @param cms the cms context
770     * @param modelContainers the model containers
771     * @param locale the content locale
772     * @param createContext the context path to pass to CmsResourceTypeConfig#createNewElement
773     *
774     * @return the updated model containers
775     *
776     * @throws CmsException in case something goes wrong
777     */
778    private List<CmsContainerBean> createNewElementsForModelGroup(
779        CmsObject cms,
780        List<CmsContainerBean> modelContainers,
781        Locale locale,
782        String createContext)
783    throws CmsException {
784
785        Map<CmsUUID, CmsResource> newResources = new HashMap<CmsUUID, CmsResource>();
786        CmsObject cloneCms = OpenCms.initCmsObject(cms);
787        cloneCms.getRequestContext().setLocale(locale);
788        for (CmsContainerBean container : modelContainers) {
789            for (CmsContainerElementBean element : container.getElements()) {
790                if (element.isCreateNew() && !newResources.containsKey(element.getId())) {
791                    element.initResource(cms);
792                    String typeName = OpenCms.getResourceManager().getResourceType(element.getResource()).getTypeName();
793                    CmsResourceTypeConfig typeConfig = m_configData.getResourceType(typeName);
794                    if (typeConfig == null) {
795                        throw new IllegalArgumentException(
796                            "Can not copy template model element '"
797                                + element.getResource().getRootPath()
798                                + "' because the resource type '"
799                                + typeName
800                                + "' is not available in this sitemap.");
801                    }
802                    CmsResource newResource = typeConfig.createNewElement(
803                        cloneCms,
804                        element.getResource(),
805                        createContext);
806                    newResources.put(element.getId(), newResource);
807                }
808            }
809        }
810        if (!newResources.isEmpty()) {
811            List<CmsContainerBean> updatedContainers = new ArrayList<CmsContainerBean>();
812            for (CmsContainerBean container : modelContainers) {
813                List<CmsContainerElementBean> updatedElements = new ArrayList<CmsContainerElementBean>();
814                for (CmsContainerElementBean element : container.getElements()) {
815                    if (newResources.containsKey(element.getId())) {
816                        CmsContainerElementBean newBean = new CmsContainerElementBean(
817                            newResources.get(element.getId()).getStructureId(),
818                            element.getFormatterId(),
819                            element.getIndividualSettings(),
820                            false);
821                        updatedElements.add(newBean);
822                    } else {
823                        updatedElements.add(element);
824                    }
825                }
826                CmsContainerBean updatedContainer = container.copyWithNewElements(updatedElements);
827                updatedContainers.add(updatedContainer);
828            }
829            modelContainers = updatedContainers;
830        }
831        return modelContainers;
832    }
833
834    /**
835     * Collects the page containers by parent instance id.<p>
836     *
837     * @param page the page
838     *
839     * @return the containers by parent id
840     */
841    private Map<String, List<CmsContainerBean>> getContainerByParent(CmsContainerPageBean page) {
842
843        Map<String, List<CmsContainerBean>> containerByParent = new HashMap<String, List<CmsContainerBean>>();
844
845        for (CmsContainerBean container : page.getContainers().values()) {
846            if (container.getParentInstanceId() != null) {
847                if (!containerByParent.containsKey(container.getParentInstanceId())) {
848                    containerByParent.put(container.getParentInstanceId(), new ArrayList<CmsContainerBean>());
849                }
850                containerByParent.get(container.getParentInstanceId()).add(container);
851            }
852        }
853        return containerByParent;
854    }
855
856    /**
857     * Unmarshals the given resource.<p>
858     *
859     * @param resource the resource
860     *
861     * @return the container page bean
862     *
863     * @throws CmsException in case unmarshalling fails
864     */
865    private CmsContainerPageBean getContainerPageBean(CmsResource resource) throws CmsException {
866
867        CmsXmlContainerPage xmlCnt = CmsXmlContainerPageFactory.unmarshal(m_cms, m_cms.readFile(resource));
868        return xmlCnt.getContainerPage(m_cms);
869    }
870
871    /**
872     * Returns the model group base element.<p>
873     *
874     * @param modelGroupPage the model group page
875     * @param modelGroupResource the model group resource
876     *
877     * @return the base element
878     */
879    private CmsContainerElementBean getModelBaseElement(
880        CmsContainerPageBean modelGroupPage,
881        CmsResource modelGroupResource) {
882
883        CmsContainerElementBean result = null;
884        for (CmsContainerElementBean element : modelGroupPage.getElements()) {
885            if (CmsContainerElement.ModelGroupState.isModelGroup.name().equals(
886                element.getIndividualSettings().get(CmsContainerElement.MODEL_GROUP_STATE))) {
887                result = element;
888                break;
889            }
890        }
891        return result;
892    }
893
894    /**
895     * Returns the the element to be rendered as the model group base.<p>
896     *
897     * @param element the original element
898     * @param baseElement the model group base
899     * @param allowCopyModel if copy models are allowed
900     *
901     * @return the element
902     */
903    private CmsContainerElementBean getModelReplacementElement(
904        CmsContainerElementBean element,
905        CmsContainerElementBean baseElement,
906        boolean allowCopyModel) {
907
908        boolean resetSettings = false;
909        if (!baseElement.isCopyModel() && hasIncompatibleFormatters(baseElement, element)) {
910            I_CmsFormatterBean formatter = m_configData.findFormatter(element.getFormatterId());
911            resetSettings = (formatter == null)
912                || !formatter.getResourceTypeNames().contains(
913                    OpenCms.getResourceManager().getResourceType(baseElement.getResource()).getTypeName());
914        }
915        Map<String, String> settings;
916        if (resetSettings) {
917            settings = new HashMap<String, String>();
918            for (String id : KEEP_SETTING_IDS) {
919                if (element.getIndividualSettings().containsKey(id)) {
920                    settings.put(id, element.getIndividualSettings().get(id));
921                }
922            }
923            settings.put(CmsContainerElement.MODEL_GROUP_ID, element.getId().toString());
924            // transfer all other settings
925            for (Entry<String, String> settingEntry : baseElement.getIndividualSettings().entrySet()) {
926                if (!settings.containsKey(settingEntry.getKey())) {
927                    settings.put(settingEntry.getKey(), settingEntry.getValue());
928                }
929            }
930        } else {
931            settings = new HashMap<String, String>(element.getIndividualSettings());
932            if (!(baseElement.isCopyModel() && allowCopyModel)) {
933                // skip the model id in case of copy models
934                settings.put(CmsContainerElement.MODEL_GROUP_ID, element.getId().toString());
935                if (allowCopyModel) {
936                    // transfer all other settings
937                    for (Entry<String, String> settingEntry : baseElement.getIndividualSettings().entrySet()) {
938                        if (!settings.containsKey(settingEntry.getKey())) {
939                            settings.put(settingEntry.getKey(), settingEntry.getValue());
940                        }
941                    }
942                }
943
944            } else if (baseElement.isCopyModel()) {
945                settings.put(CmsContainerElement.MODEL_GROUP_STATE, ModelGroupState.wasModelGroup.name());
946            }
947        }
948        return CmsContainerElementBean.cloneWithSettings(baseElement, settings);
949    }
950
951    private boolean hasIncompatibleFormatters(CmsContainerElementBean baseElement, CmsContainerElementBean element) {
952
953        if ((baseElement == null) || (element == null)) {
954            return false;
955        }
956        if ((baseElement.getFormatterId() != null)
957            && (element.getFormatterId() != null)
958            && !baseElement.getFormatterId().equals(element.getFormatterId())) {
959            return true;
960        }
961        Set<String> baseFormatterKeys = CmsFormatterUtils.getAllFormatterKeys(m_configData, baseElement);
962        Set<String> elementFormatterKeys = CmsFormatterUtils.getAllFormatterKeys(m_configData, element);
963        boolean hasCommonKeys = baseFormatterKeys.stream().anyMatch(elementFormatterKeys::contains);
964        return !hasCommonKeys;
965    }
966
967    /**
968     * Returns the model containers.<p>
969     *
970     * @param modelInstanceId the model instance id
971     * @param localInstanceId the local instance id
972     * @param modelPage the model page bean
973     * @param isCopyGroup <code>true</code> in case of a copy group
974     *
975     * @return the model group containers
976     */
977    private List<CmsContainerBean> readModelContainers(
978        String modelInstanceId,
979        String localInstanceId,
980        CmsContainerPageBean modelPage,
981        boolean isCopyGroup) {
982
983        Map<String, List<CmsContainerBean>> containerByParent = getContainerByParent(modelPage);
984        List<CmsContainerBean> modelContainers;
985        if (containerByParent.containsKey(modelInstanceId)) {
986            modelContainers = collectModelStructure(modelInstanceId, localInstanceId, containerByParent, isCopyGroup);
987        } else {
988            modelContainers = new ArrayList<CmsContainerBean>();
989        }
990        return modelContainers;
991    }
992}