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