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.i18n.tools;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsResourceTypeConfig;
032import org.opencms.file.CmsFile;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProperty;
035import org.opencms.file.CmsPropertyDefinition;
036import org.opencms.file.CmsResource;
037import org.opencms.file.CmsResourceFilter;
038import org.opencms.file.types.CmsResourceTypeFolder;
039import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
040import org.opencms.file.types.I_CmsResourceType;
041import org.opencms.i18n.CmsLocaleGroupService;
042import org.opencms.i18n.CmsLocaleGroupService.Status;
043import org.opencms.i18n.CmsLocaleManager;
044import org.opencms.i18n.CmsMessageContainer;
045import org.opencms.loader.I_CmsFileNameGenerator;
046import org.opencms.lock.CmsLockActionRecord;
047import org.opencms.lock.CmsLockActionRecord.LockChange;
048import org.opencms.lock.CmsLockUtil;
049import org.opencms.main.CmsException;
050import org.opencms.main.CmsLog;
051import org.opencms.main.OpenCms;
052import org.opencms.site.CmsSite;
053import org.opencms.ui.Messages;
054import org.opencms.util.CmsFileUtil;
055import org.opencms.util.CmsMacroResolver;
056import org.opencms.util.CmsStringUtil;
057import org.opencms.util.CmsUUID;
058import org.opencms.xml.CmsXmlException;
059import org.opencms.xml.containerpage.CmsContainerBean;
060import org.opencms.xml.containerpage.CmsContainerElementBean;
061import org.opencms.xml.containerpage.CmsContainerPageBean;
062import org.opencms.xml.containerpage.CmsXmlContainerPage;
063import org.opencms.xml.containerpage.CmsXmlContainerPageFactory;
064import org.opencms.xml.content.CmsXmlContent;
065import org.opencms.xml.content.CmsXmlContentFactory;
066
067import java.io.ByteArrayInputStream;
068import java.io.IOException;
069import java.io.InputStreamReader;
070import java.util.Iterator;
071import java.util.LinkedHashMap;
072import java.util.List;
073import java.util.Locale;
074import java.util.Map;
075import java.util.Properties;
076import java.util.Set;
077
078import org.apache.commons.logging.Log;
079
080import com.google.common.collect.Lists;
081import com.google.common.collect.Maps;
082import com.google.common.collect.Sets;
083
084/**
085 * Helper class for copying container pages including some of their elements.<p>
086 */
087public class CmsContainerPageCopier {
088
089    /**
090     * Enum representing the element copy mode.<p>
091     */
092    public enum CopyMode {
093        /** Choose between reuse / copy automatically depending on source / target locale and the configuration .*/
094        automatic,
095
096        /** Do not copy elements. */
097        reuse,
098
099        /** Automatically determine when to copy elements. */
100        smartCopy,
101
102        /** Like smartCopy, but also converts locales of copied elements. */
103        smartCopyAndChangeLocale;
104
105    }
106
107    /**
108     * Exception indicating that no custom replacement element was found
109     * for a type which requires replacement.<p>
110     */
111    public static class NoCustomReplacementException extends Exception {
112
113        /** Serial version id. */
114        private static final long serialVersionUID = 1L;
115
116        /** The resource for which no exception was found. */
117        private CmsResource m_resource;
118
119        /**
120         * Creates a new instance.<p>
121         *
122         * @param resource the resource for which no replacement was found
123         */
124        public NoCustomReplacementException(CmsResource resource) {
125
126            super();
127            m_resource = resource;
128        }
129
130        /**
131         * Gets the resource for which no replacement was found.<p>
132         *
133         * @return the resource
134         */
135        public CmsResource getResource() {
136
137            return m_resource;
138        }
139    }
140
141    /** The log instance used for this class. */
142    private static final Log LOG = CmsLog.getLog(CmsContainerPageCopier.class);
143
144    /** The CMS context used by this object. */
145    private CmsObject m_cms;
146
147    /** The CMS context used by this object, but with the site root set to "". */
148    private CmsObject m_rootCms;
149
150    /** The copied resource. */
151    private CmsResource m_copiedFolderOrPage;
152
153    /** The copy mode. */
154    private CopyMode m_copyMode = CopyMode.smartCopyAndChangeLocale;
155
156    /** Map of custom replacements. */
157    private Map<CmsUUID, CmsUUID> m_customReplacements;
158
159    /** Maps structure ids of original container elements to structure ids of their copies/replacements. */
160    private Map<CmsUUID, CmsUUID> m_elementReplacements = Maps.newHashMap();
161
162    /** The original page. */
163    private CmsResource m_originalPage;
164
165    /** The target folder. */
166    private CmsResource m_targetFolder;
167
168    /** Resource types which require custom replacements. */
169    private Set<String> m_typesWithRequiredReplacements;
170
171    /**
172     * Creates a new instance.<p>
173     *
174     * @param cms the CMS context to use
175     */
176    public CmsContainerPageCopier(CmsObject cms) {
177
178        m_cms = cms;
179    }
180
181    /**
182     * Converts locales for the copied container element.<p>
183     *
184     * @param elementResource the copied container element resource
185     * @param originalResource the original container element resource
186     *
187     * @throws CmsException if something goes wrong
188     */
189    public void adjustLocalesForElement(CmsResource elementResource, CmsResource originalResource) throws CmsException {
190
191        if (m_copyMode != CopyMode.smartCopyAndChangeLocale) {
192            return;
193        }
194
195        CmsFile file = m_cms.readFile(elementResource);
196        Locale oldLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_originalPage);
197        Locale newLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_targetFolder);
198        CmsXmlContent content = CmsXmlContentFactory.unmarshal(m_cms, file);
199        try {
200            if (content.hasLocale(newLocale)) {
201                // check if the new locale was already present in the original resource, it may have been created automatically
202                CmsFile origFile = m_cms.readFile(originalResource);
203                CmsXmlContent origContent = CmsXmlContentFactory.unmarshal(m_cms, origFile);
204                if (!origContent.hasLocale(newLocale)) {
205                    // the target locale was not present in the original, remove it to ensure the source locale is moved
206                    content.removeLocale(newLocale);
207                }
208            }
209            content.moveLocale(oldLocale, newLocale);
210            LOG.info("Replacing locale " + oldLocale + " -> " + newLocale + " for " + elementResource.getRootPath());
211            file.setContents(content.marshal());
212            m_cms.writeFile(file);
213        } catch (CmsXmlException e) {
214            LOG.info(
215                "NOT replacing locale for "
216                    + elementResource.getRootPath()
217                    + ": old="
218                    + oldLocale
219                    + ", new="
220                    + newLocale
221                    + ", contentLocales="
222                    + content.getLocales());
223        }
224
225    }
226
227    /**
228     * Copies the given container page to the provided root path.
229     * @param originalPage the page to copy
230     * @param targetPageRootPath the root path of the copy target.
231     * @throws CmsException thrown if something goes wrong.
232     * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it.
233     */
234    public void copyPageOnly(CmsResource originalPage, String targetPageRootPath)
235    throws CmsException, NoCustomReplacementException {
236
237        if ((null == originalPage)
238            || !OpenCms.getResourceManager().getResourceType(originalPage).getTypeName().equals(
239                CmsResourceTypeXmlContainerPage.getStaticTypeName())) {
240            throw new CmsException(new CmsMessageContainer(Messages.get(), Messages.ERR_PAGECOPY_INVALID_PAGE_0));
241        }
242        m_originalPage = originalPage;
243        CmsObject rootCms = getRootCms();
244        rootCms.copyResource(originalPage.getRootPath(), targetPageRootPath);
245        CmsResource copiedPage = rootCms.readResource(targetPageRootPath, CmsResourceFilter.IGNORE_EXPIRATION);
246        m_targetFolder = rootCms.readResource(CmsResource.getFolderPath(copiedPage.getRootPath()));
247        replaceElements(copiedPage);
248        attachLocaleGroups(copiedPage);
249        tryUnlock(copiedPage);
250
251    }
252
253    /**
254     * Gets the copied folder or page.<p>
255     *
256     * @return the copied folder or page
257     */
258    public CmsResource getCopiedFolderOrPage() {
259
260        return m_copiedFolderOrPage;
261    }
262
263    /**
264     * Returns the target folder.<p>
265     *
266     * @return the target folder
267     */
268    public CmsResource getTargetFolder() {
269
270        return m_targetFolder;
271    }
272
273    /**
274     * Produces the replacement for a container page element to use in a copy of an existing container page.<p>
275     *
276     * @param targetPage the target container page
277     * @param originalElement the original element
278     * @return the replacement element for the copied page
279     *
280     * @throws CmsException if something goes wrong
281     * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it
282     */
283    public CmsContainerElementBean replaceContainerElement(
284        CmsResource targetPage,
285        CmsContainerElementBean originalElement)
286    throws CmsException, NoCustomReplacementException {
287        // if (m_elementReplacements.containsKey(originalElement.getId()
288
289        CmsObject targetCms = OpenCms.initCmsObject(m_cms);
290
291        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
292        if (site != null) {
293            targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
294        }
295
296        if (originalElement.getId() == null) {
297            String rootPath = m_originalPage != null ? m_originalPage.getRootPath() : "???";
298            LOG.warn("Skipping container element because of missing id in page: " + rootPath);
299            return null;
300        }
301
302        if (m_elementReplacements.containsKey(originalElement.getId())) {
303            return new CmsContainerElementBean(
304                m_elementReplacements.get(originalElement.getId()),
305                maybeReplaceFormatter(originalElement.getFormatterId()),
306                maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
307                originalElement.isCreateNew());
308        } else {
309            CmsResource originalResource = m_cms.readResource(
310                originalElement.getId(),
311                CmsResourceFilter.IGNORE_EXPIRATION);
312            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(originalResource);
313            CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(m_cms, targetPage.getRootPath());
314            CmsResourceTypeConfig typeConfig = config.getResourceType(type.getTypeName());
315            if ((m_copyMode != CopyMode.reuse)
316                && (typeConfig != null)
317                && (originalElement.isCreateNew() || typeConfig.isCopyInModels())
318                && !type.getTypeName().equals(CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME)) {
319                // set the request context locale to the target content locale as this is used during content creation
320                Locale targetLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_targetFolder);
321                targetCms.getRequestContext().setLocale(targetLocale);
322                CmsResource resourceCopy = typeConfig.createNewElement(
323                    targetCms,
324                    originalResource,
325                    CmsResource.getParentFolder(targetPage.getRootPath()));
326                CmsContainerElementBean copy = new CmsContainerElementBean(
327                    resourceCopy.getStructureId(),
328                    maybeReplaceFormatter(originalElement.getFormatterId()),
329                    maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
330                    originalElement.isCreateNew());
331                m_elementReplacements.put(originalElement.getId(), resourceCopy.getStructureId());
332                LOG.info(
333                    "Copied container element " + originalResource.getRootPath() + " -> " + resourceCopy.getRootPath());
334                CmsLockActionRecord record = null;
335                try {
336                    record = CmsLockUtil.ensureLock(m_cms, resourceCopy);
337                    adjustLocalesForElement(resourceCopy, originalResource);
338                } finally {
339                    if ((record != null) && (record.getChange() == LockChange.locked)) {
340                        m_cms.unlockResource(resourceCopy);
341                    }
342                }
343                return copy;
344            } else if (m_customReplacements != null) {
345                CmsUUID replacementId = m_customReplacements.get(originalElement.getId());
346                if (replacementId != null) {
347
348                    return new CmsContainerElementBean(
349                        replacementId,
350                        maybeReplaceFormatter(originalElement.getFormatterId()),
351                        maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
352                        originalElement.isCreateNew());
353                } else {
354                    if ((m_typesWithRequiredReplacements != null)
355                        && m_typesWithRequiredReplacements.contains(type.getTypeName())) {
356                        throw new NoCustomReplacementException(originalResource);
357                    } else {
358                        return originalElement;
359                    }
360
361                }
362            } else {
363                LOG.info("Reusing container element: " + originalResource.getRootPath());
364                return originalElement;
365            }
366        }
367    }
368
369    /**
370     * Replaces the elements in the copied container page with copies, if appropriate based on the current copy mode.<p>
371     *
372     * @param containerPage the container page copy whose elements should be replaced with copies
373     *
374     * @throws CmsException if something goes wrong
375     * @throws NoCustomReplacementException if a custom replacement element was not found for a type which requires it
376     */
377    public void replaceElements(CmsResource containerPage) throws CmsException, NoCustomReplacementException {
378
379        CmsObject rootCms = getRootCms();
380        CmsObject targetCms = OpenCms.initCmsObject(m_cms);
381        targetCms.getRequestContext().setSiteRoot("");
382        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
383        if (site != null) {
384            targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
385        } else if (OpenCms.getSiteManager().startsWithShared(m_targetFolder.getRootPath())) {
386            targetCms.getRequestContext().setSiteRoot(OpenCms.getSiteManager().getSharedFolder());
387        }
388
389        CmsProperty elementReplacementProp = rootCms.readPropertyObject(
390            m_targetFolder,
391            CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS,
392            true);
393        if ((elementReplacementProp != null) && (elementReplacementProp.getValue() != null)) {
394            try {
395                CmsResource elementReplacementMap = targetCms.readResource(
396                    elementReplacementProp.getValue(),
397                    CmsResourceFilter.IGNORE_EXPIRATION);
398                OpenCms.getLocaleManager();
399                String encoding = CmsLocaleManager.getResourceEncoding(targetCms, elementReplacementMap);
400                CmsFile elementReplacementFile = targetCms.readFile(elementReplacementMap);
401                Properties props = new Properties();
402                props.load(
403                    new InputStreamReader(new ByteArrayInputStream(elementReplacementFile.getContents()), encoding));
404                CmsMacroResolver resolver = new CmsMacroResolver();
405                resolver.addMacro("sourcesite", m_cms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
406                resolver.addMacro("targetsite", targetCms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
407                Map<CmsUUID, CmsUUID> customReplacements = Maps.newHashMap();
408                for (Map.Entry<Object, Object> entry : props.entrySet()) {
409                    if ((entry.getKey() instanceof String) && (entry.getValue() instanceof String)) {
410                        try {
411                            String key = (String)entry.getKey();
412                            if ("required".equals(key)) {
413                                m_typesWithRequiredReplacements = Sets.newHashSet(
414                                    ((String)entry.getValue()).split(" *, *"));
415                                continue;
416                            }
417                            key = resolver.resolveMacros(key);
418                            String value = (String)entry.getValue();
419                            value = resolver.resolveMacros(value);
420                            CmsResource keyRes = rootCms.readResource(key, CmsResourceFilter.IGNORE_EXPIRATION);
421                            CmsResource valRes = rootCms.readResource(value, CmsResourceFilter.IGNORE_EXPIRATION);
422                            customReplacements.put(keyRes.getStructureId(), valRes.getStructureId());
423                        } catch (Exception e) {
424                            LOG.error(e.getLocalizedMessage(), e);
425                        }
426                        m_customReplacements = customReplacements;
427                    }
428                }
429            } catch (CmsException e) {
430                LOG.warn(e.getLocalizedMessage(), e);
431            } catch (IOException e) {
432                LOG.warn(e.getLocalizedMessage(), e);
433            }
434        }
435
436        CmsXmlContainerPage pageXml = CmsXmlContainerPageFactory.unmarshal(m_cms, containerPage);
437        CmsContainerPageBean page = pageXml.getContainerPage(m_cms);
438        List<CmsContainerBean> newContainers = Lists.newArrayList();
439        for (CmsContainerBean container : page.getContainers().values()) {
440            List<CmsContainerElementBean> newElements = Lists.newArrayList();
441            for (CmsContainerElementBean element : container.getElements()) {
442                CmsContainerElementBean newBean = replaceContainerElement(containerPage, element);
443                if (newBean != null) {
444                    newElements.add(newBean);
445                }
446            }
447            CmsContainerBean newContainer = container.copyWithNewElements(newElements);
448            newContainers.add(newContainer);
449        }
450        CmsContainerPageBean newPageBean = new CmsContainerPageBean(newContainers);
451        pageXml.save(rootCms, newPageBean);
452    }
453
454    /**
455     * Starts the page copying process.<p>
456     *
457     * @param source the source (can be either a container page, or a folder whose default file is a container page)
458     * @param target the target folder
459     *
460     * @throws CmsException if soemthing goes wrong
461     * @throws NoCustomReplacementException if a custom replacement element was not found
462     */
463    public void run(CmsResource source, CmsResource target) throws CmsException, NoCustomReplacementException {
464
465        run(source, target, null);
466    }
467
468    /**
469     * Starts the page copying process.<p>
470     *
471     * @param source the source (can be either a container page, or a folder whose default file is a container page)
472     * @param target the target folder
473     * @param targetName the name to give the new folder
474     *
475     * @throws CmsException if something goes wrong
476     * @throws NoCustomReplacementException if a custom replacement element was not found
477     */
478    public void run(CmsResource source, CmsResource target, String targetName)
479    throws CmsException, NoCustomReplacementException {
480
481        LOG.info(
482            "Starting page copy process: page='"
483                + source.getRootPath()
484                + "', targetFolder='"
485                + target.getRootPath()
486                + "'");
487        CmsObject rootCms = getRootCms();
488        if (m_copyMode == CopyMode.automatic) {
489            Locale sourceLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, source);
490            Locale targetLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, target);
491            // if same locale, copy elements, otherwise use configured setting
492            LOG.debug(
493                "copy mode automatic: source="
494                    + sourceLocale
495                    + " target="
496                    + targetLocale
497                    + " reuseConfig="
498                    + OpenCms.getLocaleManager().shouldReuseElements()
499                    + "");
500            if (sourceLocale.equals(targetLocale)) {
501                m_copyMode = CopyMode.smartCopyAndChangeLocale;
502            } else {
503                if (OpenCms.getLocaleManager().shouldReuseElements()) {
504                    m_copyMode = CopyMode.reuse;
505                } else {
506                    m_copyMode = CopyMode.smartCopyAndChangeLocale;
507                }
508            }
509        }
510
511        if (source.isFolder()) {
512            if (source.equals(target)) {
513                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_SOURCE_IS_TARGET_0));
514            }
515            CmsResource page = m_cms.readDefaultFile(source, CmsResourceFilter.IGNORE_EXPIRATION);
516            if ((page == null) || !CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
517                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
518            }
519            List<CmsProperty> properties = Lists.newArrayList(m_cms.readPropertyObjects(source, false));
520            Iterator<CmsProperty> iterator = properties.iterator();
521            while (iterator.hasNext()) {
522                CmsProperty prop = iterator.next();
523                // copied folder may be root of a locale subtree, but since we may want to copy to a different locale,
524                // we don't want the locale property in the copy
525                if (prop.getName().equals(CmsPropertyDefinition.PROPERTY_LOCALE)
526                    || prop.getName().equals(CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS)) {
527                    iterator.remove();
528                }
529            }
530
531            I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
532            String copyPath;
533            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(targetName)) {
534                copyPath = CmsStringUtil.joinPaths(target.getRootPath(), targetName);
535                if (rootCms.existsResource(copyPath)) {
536                    CmsResource existingResource = rootCms.readResource(copyPath);
537                    // only overwrite the existing resource if it's a folder, otherwise find the next non-existing 'numbered' target path
538                    if (!existingResource.isFolder()) {
539                        copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
540                    }
541                }
542            } else {
543                copyPath = CmsFileUtil.removeTrailingSeparator(
544                    CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
545                copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
546            }
547            Double maxNavPosObj = readMaxNavPos(target);
548            double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
549            boolean hasNavpos = maxNavPosObj != null;
550            CmsResource copiedFolder = null;
551            CmsLockActionRecord lockRecord = null;
552            if (rootCms.existsResource(copyPath)) {
553                copiedFolder = rootCms.readResource(copyPath);
554                lockRecord = CmsLockUtil.ensureLock(rootCms, copiedFolder);
555                rootCms.writePropertyObjects(copyPath, properties);
556            } else {
557                copiedFolder = rootCms.createResource(
558                    copyPath,
559                    OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.RESOURCE_TYPE_NAME),
560                    null,
561                    properties);
562            }
563            if (hasNavpos) {
564                String newNavPosStr = "" + (maxNavpos + 10);
565                rootCms.writePropertyObject(
566                    copiedFolder.getRootPath(),
567                    new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
568            }
569            String pageCopyPath = CmsStringUtil.joinPaths(copiedFolder.getRootPath(), page.getName());
570            m_originalPage = page;
571            m_targetFolder = target;
572            m_copiedFolderOrPage = copiedFolder;
573            if (rootCms.existsResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION)) {
574                rootCms.deleteResource(pageCopyPath, CmsResource.DELETE_PRESERVE_SIBLINGS);
575            }
576            rootCms.copyResource(page.getRootPath(), pageCopyPath);
577
578            CmsResource copiedPage = rootCms.readResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION);
579
580            replaceElements(copiedPage);
581            attachLocaleGroups(copiedPage);
582            if ((lockRecord == null) || (lockRecord.getChange() == LockChange.locked)) {
583                tryUnlock(copiedFolder);
584            }
585        } else {
586            CmsResource page = source;
587            if (!CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
588                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
589            }
590            I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
591            String copyPath = CmsFileUtil.removeTrailingSeparator(
592                CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
593            int lastDot = copyPath.lastIndexOf(".");
594            int lastSlash = copyPath.lastIndexOf("/");
595            if (lastDot > lastSlash) { // path has an extension
596                String macroPath = copyPath.substring(0, lastDot) + "%(number)" + copyPath.substring(lastDot);
597                copyPath = nameGen.getNewFileName(rootCms, macroPath, 4, true);
598            } else {
599                copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
600            }
601            Double maxNavPosObj = readMaxNavPos(target);
602            double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
603            boolean hasNavpos = maxNavPosObj != null;
604            rootCms.copyResource(page.getRootPath(), copyPath);
605            if (hasNavpos) {
606                String newNavPosStr = "" + (maxNavpos + 10);
607                rootCms.writePropertyObject(
608                    copyPath,
609                    new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
610            }
611            CmsResource copiedPage = rootCms.readResource(copyPath);
612            m_originalPage = page;
613            m_targetFolder = target;
614            m_copiedFolderOrPage = copiedPage;
615            replaceElements(copiedPage);
616            attachLocaleGroups(copiedPage);
617            tryUnlock(copiedPage);
618
619        }
620    }
621
622    /**
623     * Sets the copy mode.<p>
624     *
625     * @param copyMode the copy mode
626     */
627    public void setCopyMode(CopyMode copyMode) {
628
629        m_copyMode = copyMode;
630    }
631
632    /**
633     * Reads the max nav position from the contents of a folder.<p>
634     *
635     * @param target a folder
636     * @return the maximal NavPos from the contents of the folder, or null if no resources with a valid NavPos were found in the folder
637     *
638     * @throws CmsException if something goes wrong
639     */
640    Double readMaxNavPos(CmsResource target) throws CmsException {
641
642        List<CmsResource> existingResourcesInFolder = m_cms.readResources(
643            target,
644            CmsResourceFilter.IGNORE_EXPIRATION,
645            false);
646
647        double maxNavpos = 0.0;
648        boolean hasNavpos = false;
649        for (CmsResource existingResource : existingResourcesInFolder) {
650            CmsProperty navpos = m_cms.readPropertyObject(
651                existingResource,
652                CmsPropertyDefinition.PROPERTY_NAVPOS,
653                false);
654            if (navpos.getValue() != null) {
655                try {
656                    double navposNum = Double.parseDouble(navpos.getValue());
657                    hasNavpos = true;
658                    maxNavpos = Math.max(navposNum, maxNavpos);
659                } catch (NumberFormatException e) {
660                    // ignore
661                }
662            }
663        }
664        if (hasNavpos) {
665            return Double.valueOf(maxNavpos);
666        } else {
667            return null;
668        }
669    }
670
671    /**
672     * Attaches locale groups to the copied page.
673     * @param copiedPage the copied page.
674     * @throws CmsException thrown if the root cms cannot be retrieved.
675     */
676    private void attachLocaleGroups(CmsResource copiedPage) throws CmsException {
677
678        CmsLocaleGroupService localeGroupService = getRootCms().getLocaleGroupService();
679        if (Status.linkable == localeGroupService.checkLinkable(m_originalPage, copiedPage)) {
680            try {
681                localeGroupService.attachLocaleGroupIndirect(m_originalPage, copiedPage);
682            } catch (CmsException e) {
683                LOG.error(e.getLocalizedMessage(), e);
684            }
685        }
686    }
687
688    /**
689     * Return the cms object with the site root set to "/".
690     * @return the cms object with the site root set to "/".
691     * @throws CmsException thrown if initializing the root cms object fails.
692     */
693    private CmsObject getRootCms() throws CmsException {
694
695        if (null == m_rootCms) {
696            m_rootCms = OpenCms.initCmsObject(m_cms);
697            m_rootCms.getRequestContext().setSiteRoot("");
698        }
699        return m_rootCms;
700    }
701
702    /**
703     * Uses the custom translation table to translate formatter id.<p>
704     *
705     * @param formatterId the formatter id
706     * @return the formatter replacement
707     */
708    private CmsUUID maybeReplaceFormatter(CmsUUID formatterId) {
709
710        if (m_customReplacements != null) {
711            CmsUUID replacement = m_customReplacements.get(formatterId);
712            if (replacement != null) {
713                return replacement;
714            }
715        }
716        return formatterId;
717    }
718
719    /**
720     * Replaces formatter id in element settings.<p>
721     *
722     * @param individualSettings the settings in which to replace the formatter id
723     *
724     * @return the map with the possible replaced ids
725     */
726    private Map<String, String> maybeReplaceFormatterInSettings(Map<String, String> individualSettings) {
727
728        if (individualSettings == null) {
729            return null;
730        } else if (m_customReplacements == null) {
731            return individualSettings;
732        } else {
733            LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();
734            for (Map.Entry<String, String> entry : individualSettings.entrySet()) {
735                String value = entry.getValue();
736                if (CmsUUID.isValidUUID(value)) {
737                    CmsUUID valueId = new CmsUUID(value);
738                    if (m_customReplacements.containsKey(valueId)) {
739                        value = "" + m_customReplacements.get(valueId);
740                    }
741                }
742                result.put(entry.getKey(), value);
743            }
744            return result;
745        }
746    }
747
748    /**
749     * Tries to unlock the given resource.<p>
750     *
751     * @param resource the resource to unlock
752     */
753    private void tryUnlock(CmsResource resource) {
754
755        try {
756            m_cms.unlockResource(resource);
757        } catch (CmsException e) {
758            // usually not a problem
759            LOG.debug("failed to unlock " + resource.getRootPath(), e);
760        }
761
762    }
763
764}