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.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 javax.servlet.http.HttpServletRequest;
072import javax.servlet.http.HttpServletResponse;
073
074import org.apache.commons.logging.Log;
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("No model group resources found at " + CmsEncoder.escapeXml(basePath) + "<br />");
263                } else {
264                    for (CmsResource group : resources) {
265                        boolean updated = updateModelGroupResource(cms, group, baseContainerName);
266                        response.getWriter().println(
267                            "Group '" + group.getRootPath() + "' was updated " + updated + "<br />");
268                    }
269                }
270            } catch (CmsException e) {
271                LOG.error(e.getLocalizedMessage(), e);
272                e.printStackTrace(response.getWriter());
273            }
274        }
275    }
276
277    /**
278     * Locks the given resource.<p>
279     *
280     * @param cms the cms context
281     * @param resource the resource to lock
282     *
283     * @throws CmsException in case locking fails
284     */
285    private static void ensureLock(CmsObject cms, CmsResource resource) throws CmsException {
286
287        CmsUser user = cms.getRequestContext().getCurrentUser();
288        CmsLock lock = cms.getLock(resource);
289        if (!lock.isOwnedBy(user)) {
290            cms.lockResourceTemporary(resource);
291        } else if (!lock.isOwnedInProjectBy(user, cms.getRequestContext().getCurrentProject())) {
292            cms.changeLock(resource);
293        }
294    }
295
296    /**
297     * Tries to unlock a resource.<p>
298     *
299     * @param cms the cms context
300     * @param resource the resource to unlock
301     */
302    private static void tryUnlock(CmsObject cms, CmsResource resource) {
303
304        try {
305            cms.unlockResource(resource);
306        } catch (CmsException e) {
307            LOG.debug("Unable to unlock " + resource.getRootPath(), e);
308        }
309    }
310
311    /**
312     * Adds the model group elements to the page.<p>
313     *
314     * @param elements the requested elements
315     * @param foundGroups list to add the found group element client ids to
316     * @param page the page
317     * @param alwaysCopy <code>true</code> to create element copies in case of non model groups and createNew is set
318     * @param locale the content locale
319     * @param createContextPath the context path to pass to CmsResourceTypeConfig#createNewElement
320     *
321     * @return the adjusted page
322     *
323     * @throws CmsException in case something goes wrong
324     */
325    public CmsContainerPageBean prepareforModelGroupContent(
326        Map<String, CmsContainerElementBean> elements,
327        List<String> foundGroups,
328        CmsContainerPageBean page,
329        boolean alwaysCopy,
330        Locale locale,
331        String createContextPath)
332    throws CmsException {
333
334        for (Entry<String, CmsContainerElementBean> entry : elements.entrySet()) {
335            CmsContainerElementBean element = entry.getValue();
336            CmsContainerPageBean modelPage = null;
337            String modelInstanceId = null;
338            boolean foundInstance = false;
339            if (CmsModelGroupHelper.isModelGroupResource(element.getResource())) {
340                modelPage = getContainerPageBean(element.getResource());
341                CmsContainerElementBean baseElement = getModelBaseElement(modelPage, element.getResource());
342                if (baseElement == null) {
343                    break;
344                }
345                String baseInstanceId = baseElement.getInstanceId();
346                String originalInstanceId = element.getInstanceId();
347                element = getModelReplacementElement(element, baseElement, true);
348                List<CmsContainerBean> modelContainers = readModelContainers(
349                    baseInstanceId,
350                    originalInstanceId,
351                    modelPage,
352                    baseElement.isCopyModel());
353                if (!m_isEditingModelGroups && baseElement.isCopyModel()) {
354                    modelContainers = createNewElementsForModelGroup(m_cms, modelContainers, locale, createContextPath);
355                }
356                modelContainers.addAll(page.getContainers().values());
357                page = new CmsContainerPageBean(modelContainers);
358                // update the entry element value, as the settings will have changed
359                entry.setValue(element);
360                if (m_sessionCache != null) {
361                    // also update the session cache
362                    m_sessionCache.setCacheContainerElement(element.editorHash(), element);
363                }
364            } else {
365                // here we need to make sure to remove the source container page setting and to set a new element instance id
366
367                Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
368                String source = settings.get(CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING);
369                settings.remove(CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING);
370                // TODO: Make sure source id is available for second call
371
372                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(source)) {
373                    try {
374                        CmsUUID sourceId = new CmsUUID(source);
375                        CmsResource sourcePage = m_cms.readResource(sourceId);
376                        if (CmsResourceTypeXmlContainerPage.isContainerPage(sourcePage)) {
377                            CmsXmlContainerPage xmlCnt = CmsXmlContainerPageFactory.unmarshal(
378                                m_cms,
379                                m_cms.readFile(sourcePage));
380                            modelPage = xmlCnt.getContainerPage(m_cms);
381                            modelInstanceId = element.getInstanceId();
382                        }
383
384                        settings.remove(CmsContainerElement.ELEMENT_INSTANCE_ID);
385
386                        boolean copyRoot = false;
387                        if (alwaysCopy && (modelInstanceId != null) && (modelPage != null)) {
388                            for (CmsContainerElementBean el : modelPage.getElements()) {
389                                if (modelInstanceId.equals(el.getInstanceId())) {
390                                    copyRoot = el.isCreateNew();
391                                    break;
392                                }
393                            }
394                        }
395
396                        if (copyRoot) {
397                            CmsObject cloneCms = OpenCms.initCmsObject(m_cms);
398                            cloneCms.getRequestContext().setLocale(locale);
399                            String typeName = OpenCms.getResourceManager().getResourceType(
400                                element.getResource()).getTypeName();
401                            CmsResourceTypeConfig typeConfig = m_configData.getResourceType(typeName);
402                            if (typeConfig == null) {
403                                throw new IllegalArgumentException(
404                                    "Can not copy template model element '"
405                                        + element.getResource().getRootPath()
406                                        + "' because the resource type '"
407                                        + typeName
408                                        + "' is not available in this sitemap.");
409                            }
410                            CmsResource newResource = typeConfig.createNewElement(
411                                cloneCms,
412                                element.getResource(),
413                                createContextPath);
414
415                            element = new CmsContainerElementBean(
416                                newResource.getStructureId(),
417                                element.getFormatterId(),
418                                settings,
419                                false);
420                        } else {
421                            element = CmsContainerElementBean.cloneWithSettings(element, settings);
422                        }
423                        if (modelPage != null) {
424                            Map<String, List<CmsContainerBean>> containerByParent = new HashMap<String, List<CmsContainerBean>>();
425
426                            for (CmsContainerBean container : modelPage.getContainers().values()) {
427                                if (container.getParentInstanceId() != null) {
428                                    if (!containerByParent.containsKey(container.getParentInstanceId())) {
429                                        containerByParent.put(
430                                            container.getParentInstanceId(),
431                                            new ArrayList<CmsContainerBean>());
432                                    }
433                                    containerByParent.get(container.getParentInstanceId()).add(container);
434                                }
435                                if (!foundInstance) {
436                                    for (CmsContainerElementBean child : container.getElements()) {
437                                        if (modelInstanceId == null) {
438                                            if (child.getId().equals(element.getId())) {
439                                                modelInstanceId = child.getInstanceId();
440                                                foundInstance = true;
441                                                // we also want to keep the settings of the model group
442                                                Map<String, String> setting = new HashMap<String, String>(
443                                                    child.getIndividualSettings());
444                                                setting.remove(CmsContainerElement.ELEMENT_INSTANCE_ID);
445                                                element = CmsContainerElementBean.cloneWithSettings(element, setting);
446                                                break;
447                                            }
448                                        } else {
449                                            if (modelInstanceId.equals(child.getInstanceId())) {
450                                                foundInstance = true;
451                                                break;
452                                            }
453                                        }
454                                    }
455                                }
456                            }
457                            if (foundInstance && containerByParent.containsKey(modelInstanceId)) {
458                                List<CmsContainerBean> modelContainers = collectModelStructure(
459                                    modelInstanceId,
460                                    element.getInstanceId(),
461                                    containerByParent,
462                                    true);
463                                if (alwaysCopy) {
464                                    modelContainers = createNewElementsForModelGroup(
465                                        m_cms,
466                                        modelContainers,
467                                        locale,
468                                        createContextPath);
469                                }
470                                foundGroups.add(element.editorHash());
471                                modelContainers.addAll(page.getContainers().values());
472                                page = new CmsContainerPageBean(modelContainers);
473                            }
474                        }
475
476                        // update the entry element value, as the settings will have changed
477                        entry.setValue(element);
478                        if (m_sessionCache != null) {
479                            // also update the session cache
480                            m_sessionCache.setCacheContainerElement(element.editorHash(), element);
481                        }
482                    } catch (Exception e) {
483                        LOG.info(e.getLocalizedMessage(), e);
484                    }
485
486                }
487            }
488
489        }
490        return page;
491    }
492
493    /**
494     * Reads the present model groups and merges their containers into the page.<p>
495     *
496     * @param page the container page
497     *
498     * @return the resulting container page
499     */
500    public CmsContainerPageBean readModelGroups(CmsContainerPageBean page) {
501
502        List<CmsContainerBean> resultContainers = new ArrayList<CmsContainerBean>();
503        for (CmsContainerBean container : page.getContainers().values()) {
504            boolean hasModels = false;
505            List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
506            for (CmsContainerElementBean element : container.getElements()) {
507                try {
508                    element.initResource(m_cms);
509                    if (isModelGroupResource(element.getResource())) {
510                        hasModels = true;
511                        CmsContainerPageBean modelGroupPage = getContainerPageBean(element.getResource());
512                        CmsContainerElementBean baseElement = getModelBaseElement(
513                            modelGroupPage,
514                            element.getResource());
515                        if (baseElement == null) {
516                            LOG.error(
517                                "Error rendering model group '"
518                                    + element.getResource().getRootPath()
519                                    + "', base element could not be determind.");
520                            continue;
521                        }
522                        String baseInstanceId = baseElement.getInstanceId();
523                        CmsContainerElementBean replaceElement = getModelReplacementElement(
524                            element,
525                            baseElement,
526                            false);
527                        if (m_sessionCache != null) {
528                            m_sessionCache.setCacheContainerElement(replaceElement.editorHash(), replaceElement);
529                        }
530                        elements.add(replaceElement);
531                        resultContainers.addAll(
532                            readModelContainers(
533                                baseInstanceId,
534                                element.getInstanceId(),
535                                modelGroupPage,
536                                baseElement.isCopyModel()));
537                    } else {
538                        elements.add(element);
539                    }
540                } catch (CmsException e) {
541                    LOG.info(e.getLocalizedMessage(), e);
542                }
543            }
544            if (hasModels) {
545                resultContainers.add(container.copyWithNewElements(elements));
546            } else {
547                resultContainers.add(container);
548            }
549        }
550        return new CmsContainerPageBean(resultContainers);
551    }
552
553    /**
554     * Removes the model group containers.<p>
555     *
556     * @param page the container page state
557     *
558     * @return the container page without the model group containers
559     */
560    public CmsContainerPageBean removeModelGroupContainers(CmsContainerPageBean page) {
561
562        Map<String, List<CmsContainerBean>> containersByParent = getContainerByParent(page);
563        Set<String> modelInstances = new HashSet<String>();
564        for (CmsContainerElementBean element : page.getElements()) {
565            if (element.getIndividualSettings().containsKey(CmsContainerElement.MODEL_GROUP_ID)) {
566                modelInstances.add(element.getInstanceId());
567            }
568        }
569
570        Set<String> descendingInstances = new HashSet<String>();
571        for (String modelInstance : modelInstances) {
572            descendingInstances.addAll(collectDescendingInstances(modelInstance, containersByParent));
573        }
574        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
575        for (CmsContainerBean container : page.getContainers().values()) {
576            if ((container.getParentInstanceId() == null)
577                || !descendingInstances.contains(container.getParentInstanceId())) {
578                // iterate the container elements to replace the model group elements
579                List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
580                for (CmsContainerElementBean element : container.getElements()) {
581                    if (modelInstances.contains(element.getInstanceId())) {
582                        CmsUUID modelId = new CmsUUID(
583                            element.getIndividualSettings().get(CmsContainerElement.MODEL_GROUP_ID));
584                        CmsContainerElementBean replacer = new CmsContainerElementBean(
585                            modelId,
586                            element.getFormatterId(),
587                            element.getIndividualSettings(),
588                            false);
589                        elements.add(replacer);
590                    } else {
591                        elements.add(element);
592                    }
593                }
594                containers.add(container.copyWithNewElements(elements));
595            }
596        }
597        return new CmsContainerPageBean(containers);
598    }
599
600    /**
601     * Saves the model groups of the given container page.<p>
602     *
603     * @param page the container page
604     * @param pageResource the model group resource
605     *
606     * @return the container page referencing the saved model groups
607     *
608     * @throws CmsException in case writing the page properties fails
609     */
610    public CmsContainerPageBean saveModelGroups(CmsContainerPageBean page, CmsResource pageResource)
611    throws CmsException {
612
613        CmsUUID modelElementId = null;
614        CmsContainerElementBean baseElement = null;
615        for (CmsContainerElementBean element : page.getElements()) {
616            if (element.isModelGroup()) {
617                modelElementId = element.getId();
618                baseElement = element;
619                break;
620
621            }
622        }
623        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
624        for (CmsContainerBean container : page.getContainers().values()) {
625            List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
626            boolean hasChanges = false;
627            for (CmsContainerElementBean element : container.getElements()) {
628                if (element.isModelGroup() && !element.getId().equals(modelElementId)) {
629                    // there should not be another model group element, remove the model group settings
630                    Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
631                    settings.remove(CmsContainerElement.MODEL_GROUP_ID);
632                    settings.remove(CmsContainerElement.MODEL_GROUP_STATE);
633                    elements.add(
634                        new CmsContainerElementBean(element.getId(), element.getFormatterId(), settings, false));
635                    hasChanges = true;
636                } else {
637                    elements.add(element);
638                }
639            }
640            if (hasChanges) {
641                containers.add(container.copyWithNewElements(elements));
642            } else {
643                containers.add(container);
644            }
645
646        }
647
648        List<CmsProperty> changedProps = new ArrayList<CmsProperty>();
649        if (baseElement != null) {
650            String val = Boolean.parseBoolean(
651                baseElement.getIndividualSettings().get(CmsContainerElement.USE_AS_COPY_MODEL))
652                ? CmsContainerElement.USE_AS_COPY_MODEL
653                : "";
654            changedProps.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_TEMPLATE_ELEMENTS, val, val));
655        }
656        m_cms.writePropertyObjects(pageResource, changedProps);
657
658        return new CmsContainerPageBean(containers);
659
660    }
661
662    /**
663     * Adjusts formatter settings and initializes a new instance id for the given container element.<p>
664     *
665     * @param element the container element
666     * @param originalContainer the original parent container name
667     * @param adjustedContainer the target container name
668     * @param setNewInstanceId <code>true</code> to set a new instance id
669     *
670     * @return the new element instance
671     */
672    private CmsContainerElementBean adjustSettings(
673        CmsContainerElementBean element,
674        String originalContainer,
675        String adjustedContainer,
676        boolean setNewInstanceId) {
677
678        Map<String, String> settings = new HashMap<String, String>(element.getIndividualSettings());
679        if (setNewInstanceId) {
680            settings.put(CmsContainerElement.ELEMENT_INSTANCE_ID, new CmsUUID().toString());
681        }
682        String formatterId = settings.remove(CmsFormatterConfig.getSettingsKeyForContainer(originalContainer));
683        settings.put(CmsFormatterConfig.getSettingsKeyForContainer(adjustedContainer), formatterId);
684        return CmsContainerElementBean.cloneWithSettings(element, settings);
685    }
686
687    /**
688     * Returns the descending instance id's to the given element instance.<p>
689     *
690     * @param instanceId the instance id
691     * @param containersByParent the container page containers by parent instance id
692     *
693     * @return the containers
694     */
695    private Set<String> collectDescendingInstances(
696        String instanceId,
697        Map<String, List<CmsContainerBean>> containersByParent) {
698
699        Set<String> descendingInstances = new HashSet<String>();
700        descendingInstances.add(instanceId);
701        if (containersByParent.containsKey(instanceId)) {
702            for (CmsContainerBean container : containersByParent.get(instanceId)) {
703                for (CmsContainerElementBean element : container.getElements()) {
704                    descendingInstances.addAll(collectDescendingInstances(element.getInstanceId(), containersByParent));
705                }
706            }
707        }
708        return descendingInstances;
709    }
710
711    /**
712     * Collects the model group structure.<p>
713     *
714     * @param modelInstanceId the model instance id
715     * @param replaceModelId the local instance id
716     * @param containerByParent the model group page containers by parent instance id
717     * @param isCopyGroup <code>true</code> in case of a copy group
718     *
719     * @return the collected containers
720     */
721    private List<CmsContainerBean> collectModelStructure(
722        String modelInstanceId,
723        String replaceModelId,
724        Map<String, List<CmsContainerBean>> containerByParent,
725        boolean isCopyGroup) {
726
727        List<CmsContainerBean> result = new ArrayList<CmsContainerBean>();
728
729        if (containerByParent.containsKey(modelInstanceId)) {
730            for (CmsContainerBean container : containerByParent.get(modelInstanceId)) {
731                String adjustedContainerName = replaceModelId + container.getName().substring(modelInstanceId.length());
732
733                List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
734                for (CmsContainerElementBean element : container.getElements()) {
735                    CmsContainerElementBean copyElement = adjustSettings(
736                        element,
737                        container.getName(),
738                        adjustedContainerName,
739                        isCopyGroup);
740                    if (m_sessionCache != null) {
741                        m_sessionCache.setCacheContainerElement(copyElement.editorHash(), copyElement);
742                    }
743                    elements.add(copyElement);
744                    result.addAll(
745                        collectModelStructure(
746                            element.getInstanceId(),
747                            copyElement.getInstanceId(),
748                            containerByParent,
749                            isCopyGroup));
750                }
751
752                result.add(
753                    new CmsContainerBean(
754                        adjustedContainerName,
755                        container.getType(),
756                        replaceModelId,
757                        container.isRootContainer(),
758                        container.getMaxElements(),
759                        elements));
760            }
761        }
762        return result;
763    }
764
765    /**
766     * Creates new resources for elements marked with create as new.<p>
767     *
768     * @param cms the cms context
769     * @param modelContainers the model containers
770     * @param locale the content locale
771     * @param createContext the context path to pass to CmsResourceTypeConfig#createNewElement
772     *
773     * @return the updated model containers
774     *
775     * @throws CmsException in case something goes wrong
776     */
777    private List<CmsContainerBean> createNewElementsForModelGroup(
778        CmsObject cms,
779        List<CmsContainerBean> modelContainers,
780        Locale locale,
781        String createContext)
782    throws CmsException {
783
784        Map<CmsUUID, CmsResource> newResources = new HashMap<CmsUUID, CmsResource>();
785        CmsObject cloneCms = OpenCms.initCmsObject(cms);
786        cloneCms.getRequestContext().setLocale(locale);
787        for (CmsContainerBean container : modelContainers) {
788            for (CmsContainerElementBean element : container.getElements()) {
789                if (element.isCreateNew() && !newResources.containsKey(element.getId())) {
790                    element.initResource(cms);
791                    String typeName = OpenCms.getResourceManager().getResourceType(element.getResource()).getTypeName();
792                    CmsResourceTypeConfig typeConfig = m_configData.getResourceType(typeName);
793                    if (typeConfig == null) {
794                        throw new IllegalArgumentException(
795                            "Can not copy template model element '"
796                                + element.getResource().getRootPath()
797                                + "' because the resource type '"
798                                + typeName
799                                + "' is not available in this sitemap.");
800                    }
801                    CmsResource newResource = typeConfig.createNewElement(
802                        cloneCms,
803                        element.getResource(),
804                        createContext);
805                    newResources.put(element.getId(), newResource);
806                }
807            }
808        }
809        if (!newResources.isEmpty()) {
810            List<CmsContainerBean> updatedContainers = new ArrayList<CmsContainerBean>();
811            for (CmsContainerBean container : modelContainers) {
812                List<CmsContainerElementBean> updatedElements = new ArrayList<CmsContainerElementBean>();
813                for (CmsContainerElementBean element : container.getElements()) {
814                    if (newResources.containsKey(element.getId())) {
815                        CmsContainerElementBean newBean = new CmsContainerElementBean(
816                            newResources.get(element.getId()).getStructureId(),
817                            element.getFormatterId(),
818                            element.getIndividualSettings(),
819                            false);
820                        updatedElements.add(newBean);
821                    } else {
822                        updatedElements.add(element);
823                    }
824                }
825                CmsContainerBean updatedContainer = container.copyWithNewElements(updatedElements);
826                updatedContainers.add(updatedContainer);
827            }
828            modelContainers = updatedContainers;
829        }
830        return modelContainers;
831    }
832
833    /**
834     * Collects the page containers by parent instance id.<p>
835     *
836     * @param page the page
837     *
838     * @return the containers by parent id
839     */
840    private Map<String, List<CmsContainerBean>> getContainerByParent(CmsContainerPageBean page) {
841
842        Map<String, List<CmsContainerBean>> containerByParent = new HashMap<String, List<CmsContainerBean>>();
843
844        for (CmsContainerBean container : page.getContainers().values()) {
845            if (container.getParentInstanceId() != null) {
846                if (!containerByParent.containsKey(container.getParentInstanceId())) {
847                    containerByParent.put(container.getParentInstanceId(), new ArrayList<CmsContainerBean>());
848                }
849                containerByParent.get(container.getParentInstanceId()).add(container);
850            }
851        }
852        return containerByParent;
853    }
854
855    /**
856     * Unmarshals the given resource.<p>
857     *
858     * @param resource the resource
859     *
860     * @return the container page bean
861     *
862     * @throws CmsException in case unmarshalling fails
863     */
864    private CmsContainerPageBean getContainerPageBean(CmsResource resource) throws CmsException {
865
866        CmsXmlContainerPage xmlCnt = CmsXmlContainerPageFactory.unmarshal(m_cms, m_cms.readFile(resource));
867        return xmlCnt.getContainerPage(m_cms);
868    }
869
870    /**
871     * Returns the model group base element.<p>
872     *
873     * @param modelGroupPage the model group page
874     * @param modelGroupResource the model group resource
875     *
876     * @return the base element
877     */
878    private CmsContainerElementBean getModelBaseElement(
879        CmsContainerPageBean modelGroupPage,
880        CmsResource modelGroupResource) {
881
882        CmsContainerElementBean result = null;
883        for (CmsContainerElementBean element : modelGroupPage.getElements()) {
884            if (CmsContainerElement.ModelGroupState.isModelGroup.name().equals(
885                element.getIndividualSettings().get(CmsContainerElement.MODEL_GROUP_STATE))) {
886                result = element;
887                break;
888            }
889        }
890        return result;
891    }
892
893    /**
894     * Returns the the element to be rendered as the model group base.<p>
895     *
896     * @param element the original element
897     * @param baseElement the model group base
898     * @param allowCopyModel if copy models are allowed
899     *
900     * @return the element
901     */
902    private CmsContainerElementBean getModelReplacementElement(
903        CmsContainerElementBean element,
904        CmsContainerElementBean baseElement,
905        boolean allowCopyModel) {
906
907        boolean resetSettings = false;
908        if (!baseElement.isCopyModel() && hasIncompatibleFormatters(baseElement, element)) {
909            I_CmsFormatterBean formatter = m_configData.findFormatter(element.getFormatterId());
910            resetSettings = (formatter == null)
911                || !formatter.getResourceTypeNames().contains(
912                    OpenCms.getResourceManager().getResourceType(baseElement.getResource()).getTypeName());
913        }
914        Map<String, String> settings;
915        if (resetSettings) {
916            settings = new HashMap<String, String>();
917            for (String id : KEEP_SETTING_IDS) {
918                if (element.getIndividualSettings().containsKey(id)) {
919                    settings.put(id, element.getIndividualSettings().get(id));
920                }
921            }
922            settings.put(CmsContainerElement.MODEL_GROUP_ID, element.getId().toString());
923            // transfer all other settings
924            for (Entry<String, String> settingEntry : baseElement.getIndividualSettings().entrySet()) {
925                if (!settings.containsKey(settingEntry.getKey())) {
926                    settings.put(settingEntry.getKey(), settingEntry.getValue());
927                }
928            }
929        } else {
930            settings = new HashMap<String, String>(element.getIndividualSettings());
931            if (!(baseElement.isCopyModel() && allowCopyModel)) {
932                // skip the model id in case of copy models
933                settings.put(CmsContainerElement.MODEL_GROUP_ID, element.getId().toString());
934                if (allowCopyModel) {
935                    // transfer all other settings
936                    for (Entry<String, String> settingEntry : baseElement.getIndividualSettings().entrySet()) {
937                        if (!settings.containsKey(settingEntry.getKey())) {
938                            settings.put(settingEntry.getKey(), settingEntry.getValue());
939                        }
940                    }
941                }
942
943            } else if (baseElement.isCopyModel()) {
944                settings.put(CmsContainerElement.MODEL_GROUP_STATE, ModelGroupState.wasModelGroup.name());
945            }
946        }
947        return CmsContainerElementBean.cloneWithSettings(baseElement, settings);
948    }
949
950    private boolean hasIncompatibleFormatters(CmsContainerElementBean baseElement, CmsContainerElementBean element) {
951
952        if ((baseElement == null) || (element == null)) {
953            return false;
954        }
955        if ((baseElement.getFormatterId() != null)
956            && (element.getFormatterId() != null)
957            && !baseElement.getFormatterId().equals(element.getFormatterId())) {
958            return true;
959        }
960        Set<String> baseFormatterKeys = CmsFormatterUtils.getAllFormatterKeys(m_configData, baseElement);
961        Set<String> elementFormatterKeys = CmsFormatterUtils.getAllFormatterKeys(m_configData, element);
962        boolean hasCommonKeys = baseFormatterKeys.stream().anyMatch(elementFormatterKeys::contains);
963        return !hasCommonKeys;
964    }
965
966    /**
967     * Returns the model containers.<p>
968     *
969     * @param modelInstanceId the model instance id
970     * @param localInstanceId the local instance id
971     * @param modelPage the model page bean
972     * @param isCopyGroup <code>true</code> in case of a copy group
973     *
974     * @return the model group containers
975     */
976    private List<CmsContainerBean> readModelContainers(
977        String modelInstanceId,
978        String localInstanceId,
979        CmsContainerPageBean modelPage,
980        boolean isCopyGroup) {
981
982        Map<String, List<CmsContainerBean>> containerByParent = getContainerByParent(modelPage);
983        List<CmsContainerBean> modelContainers;
984        if (containerByParent.containsKey(modelInstanceId)) {
985            modelContainers = collectModelStructure(modelInstanceId, localInstanceId, containerByParent, isCopyGroup);
986        } else {
987            modelContainers = new ArrayList<CmsContainerBean>();
988        }
989        return modelContainers;
990    }
991}