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
185     * @throws CmsException if something goes wrong
186     */
187    public void adjustLocalesForElement(CmsResource elementResource) throws CmsException {
188
189        if (m_copyMode != CopyMode.smartCopyAndChangeLocale) {
190            return;
191        }
192
193        CmsFile file = m_cms.readFile(elementResource);
194        Locale oldLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_originalPage);
195        Locale newLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_targetFolder);
196        CmsXmlContent content = CmsXmlContentFactory.unmarshal(m_cms, file);
197        try {
198            content.moveLocale(oldLocale, newLocale);
199            LOG.info("Replacing locale " + oldLocale + " -> " + newLocale + " for " + elementResource.getRootPath());
200            file.setContents(content.marshal());
201            m_cms.writeFile(file);
202        } catch (CmsXmlException e) {
203            LOG.info(
204                "NOT replacing locale for "
205                    + elementResource.getRootPath()
206                    + ": old="
207                    + oldLocale
208                    + ", new="
209                    + newLocale
210                    + ", contentLocales="
211                    + content.getLocales());
212        }
213
214    }
215
216    /**
217     * Copies the given container page to the provided root path.
218     * @param originalPage the page to copy
219     * @param targetPageRootPath the root path of the copy target.
220     * @throws CmsException thrown if something goes wrong.
221     * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it.
222     */
223    public void copyPageOnly(CmsResource originalPage, String targetPageRootPath)
224    throws CmsException, NoCustomReplacementException {
225
226        if ((null == originalPage)
227            || !OpenCms.getResourceManager().getResourceType(originalPage).getTypeName().equals(
228                CmsResourceTypeXmlContainerPage.getStaticTypeName())) {
229            throw new CmsException(new CmsMessageContainer(Messages.get(), Messages.ERR_PAGECOPY_INVALID_PAGE_0));
230        }
231        m_originalPage = originalPage;
232        CmsObject rootCms = getRootCms();
233        rootCms.copyResource(originalPage.getRootPath(), targetPageRootPath);
234        CmsResource copiedPage = rootCms.readResource(targetPageRootPath, CmsResourceFilter.IGNORE_EXPIRATION);
235        m_targetFolder = rootCms.readResource(CmsResource.getFolderPath(copiedPage.getRootPath()));
236        replaceElements(copiedPage);
237        attachLocaleGroups(copiedPage);
238        tryUnlock(copiedPage);
239
240    }
241
242    /**
243     * Gets the copied folder or page.<p>
244     *
245     * @return the copied folder or page
246     */
247    public CmsResource getCopiedFolderOrPage() {
248
249        return m_copiedFolderOrPage;
250    }
251
252    /**
253     * Returns the target folder.<p>
254     *
255     * @return the target folder
256     */
257    public CmsResource getTargetFolder() {
258
259        return m_targetFolder;
260    }
261
262    /**
263     * Produces the replacement for a container page element to use in a copy of an existing container page.<p>
264     *
265     * @param targetPage the target container page
266     * @param originalElement the original element
267     * @return the replacement element for the copied page
268     *
269     * @throws CmsException if something goes wrong
270     * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it
271     */
272    public CmsContainerElementBean replaceContainerElement(
273        CmsResource targetPage,
274        CmsContainerElementBean originalElement)
275    throws CmsException, NoCustomReplacementException {
276        // if (m_elementReplacements.containsKey(originalElement.getId()
277
278        CmsObject targetCms = OpenCms.initCmsObject(m_cms);
279
280        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
281        if (site != null) {
282            targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
283        }
284
285        if ((originalElement.getFormatterId() == null) || (originalElement.getId() == null)) {
286            String rootPath = m_originalPage != null ? m_originalPage.getRootPath() : "???";
287            LOG.warn("Skipping container element because of missing id in page: " + rootPath);
288            return null;
289        }
290
291        if (m_elementReplacements.containsKey(originalElement.getId())) {
292            return new CmsContainerElementBean(
293                m_elementReplacements.get(originalElement.getId()),
294                maybeReplaceFormatter(originalElement.getFormatterId()),
295                maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
296                originalElement.isCreateNew());
297        } else {
298            CmsResource originalResource = m_cms.readResource(
299                originalElement.getId(),
300                CmsResourceFilter.IGNORE_EXPIRATION);
301            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(originalResource);
302            CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(m_cms, targetPage.getRootPath());
303            CmsResourceTypeConfig typeConfig = config.getResourceType(type.getTypeName());
304            if ((m_copyMode != CopyMode.reuse)
305                && (typeConfig != null)
306                && (originalElement.isCreateNew() || typeConfig.isCopyInModels())
307                && !type.getTypeName().equals(CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME)) {
308                CmsResource resourceCopy = typeConfig.createNewElement(
309                    targetCms,
310                    originalResource,
311                    targetPage.getRootPath());
312                CmsContainerElementBean copy = new CmsContainerElementBean(
313                    resourceCopy.getStructureId(),
314                    maybeReplaceFormatter(originalElement.getFormatterId()),
315                    maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
316                    originalElement.isCreateNew());
317                m_elementReplacements.put(originalElement.getId(), resourceCopy.getStructureId());
318                LOG.info(
319                    "Copied container element " + originalResource.getRootPath() + " -> " + resourceCopy.getRootPath());
320                CmsLockActionRecord record = null;
321                try {
322                    record = CmsLockUtil.ensureLock(m_cms, resourceCopy);
323                    adjustLocalesForElement(resourceCopy);
324                } finally {
325                    if ((record != null) && (record.getChange() == LockChange.locked)) {
326                        m_cms.unlockResource(resourceCopy);
327                    }
328                }
329                return copy;
330            } else if (m_customReplacements != null) {
331                CmsUUID replacementId = m_customReplacements.get(originalElement.getId());
332                if (replacementId != null) {
333
334                    return new CmsContainerElementBean(
335                        replacementId,
336                        maybeReplaceFormatter(originalElement.getFormatterId()),
337                        maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
338                        originalElement.isCreateNew());
339                } else {
340                    if ((m_typesWithRequiredReplacements != null)
341                        && m_typesWithRequiredReplacements.contains(type.getTypeName())) {
342                        throw new NoCustomReplacementException(originalResource);
343                    } else {
344                        return originalElement;
345                    }
346
347                }
348            } else {
349                LOG.info("Reusing container element: " + originalResource.getRootPath());
350                return originalElement;
351            }
352        }
353    }
354
355    /**
356     * Replaces the elements in the copied container page with copies, if appropriate based on the current copy mode.<p>
357     *
358     * @param containerPage the container page copy whose elements should be replaced with copies
359     *
360     * @throws CmsException if something goes wrong
361     * @throws NoCustomReplacementException if a custom replacement element was not found for a type which requires it
362     */
363    public void replaceElements(CmsResource containerPage) throws CmsException, NoCustomReplacementException {
364
365        CmsObject rootCms = getRootCms();
366        CmsObject targetCms = OpenCms.initCmsObject(m_cms);
367        targetCms.getRequestContext().setSiteRoot("");
368        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
369        if (site != null) {
370            targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
371        } else if (OpenCms.getSiteManager().startsWithShared(m_targetFolder.getRootPath())) {
372            targetCms.getRequestContext().setSiteRoot(OpenCms.getSiteManager().getSharedFolder());
373        }
374
375        CmsProperty elementReplacementProp = rootCms.readPropertyObject(
376            m_targetFolder,
377            CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS,
378            true);
379        if ((elementReplacementProp != null) && (elementReplacementProp.getValue() != null)) {
380            try {
381                CmsResource elementReplacementMap = targetCms.readResource(
382                    elementReplacementProp.getValue(),
383                    CmsResourceFilter.IGNORE_EXPIRATION);
384                OpenCms.getLocaleManager();
385                String encoding = CmsLocaleManager.getResourceEncoding(targetCms, elementReplacementMap);
386                CmsFile elementReplacementFile = targetCms.readFile(elementReplacementMap);
387                Properties props = new Properties();
388                props.load(
389                    new InputStreamReader(new ByteArrayInputStream(elementReplacementFile.getContents()), encoding));
390                CmsMacroResolver resolver = new CmsMacroResolver();
391                resolver.addMacro("sourcesite", m_cms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
392                resolver.addMacro("targetsite", targetCms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
393                Map<CmsUUID, CmsUUID> customReplacements = Maps.newHashMap();
394                for (Map.Entry<Object, Object> entry : props.entrySet()) {
395                    if ((entry.getKey() instanceof String) && (entry.getValue() instanceof String)) {
396                        try {
397                            String key = (String)entry.getKey();
398                            if ("required".equals(key)) {
399                                m_typesWithRequiredReplacements = Sets.newHashSet(
400                                    ((String)entry.getValue()).split(" *, *"));
401                                continue;
402                            }
403                            key = resolver.resolveMacros(key);
404                            String value = (String)entry.getValue();
405                            value = resolver.resolveMacros(value);
406                            CmsResource keyRes = rootCms.readResource(key, CmsResourceFilter.IGNORE_EXPIRATION);
407                            CmsResource valRes = rootCms.readResource(value, CmsResourceFilter.IGNORE_EXPIRATION);
408                            customReplacements.put(keyRes.getStructureId(), valRes.getStructureId());
409                        } catch (Exception e) {
410                            LOG.error(e.getLocalizedMessage(), e);
411                        }
412                        m_customReplacements = customReplacements;
413                    }
414                }
415            } catch (CmsException e) {
416                LOG.warn(e.getLocalizedMessage(), e);
417            } catch (IOException e) {
418                LOG.warn(e.getLocalizedMessage(), e);
419            }
420        }
421
422        CmsXmlContainerPage pageXml = CmsXmlContainerPageFactory.unmarshal(m_cms, containerPage);
423        CmsContainerPageBean page = pageXml.getContainerPage(m_cms);
424        List<CmsContainerBean> newContainers = Lists.newArrayList();
425        for (CmsContainerBean container : page.getContainers().values()) {
426            List<CmsContainerElementBean> newElements = Lists.newArrayList();
427            for (CmsContainerElementBean element : container.getElements()) {
428                CmsContainerElementBean newBean = replaceContainerElement(containerPage, element);
429                if (newBean != null) {
430                    newElements.add(newBean);
431                }
432            }
433            CmsContainerBean newContainer = new CmsContainerBean(
434                container.getName(),
435                container.getType(),
436                container.getParentInstanceId(),
437                container.isRootContainer(),
438                newElements);
439            newContainers.add(newContainer);
440        }
441        CmsContainerPageBean newPageBean = new CmsContainerPageBean(newContainers);
442        pageXml.save(rootCms, newPageBean);
443    }
444
445    /**
446     * Starts the page copying process.<p>
447     *
448     * @param source the source (can be either a container page, or a folder whose default file is a container page)
449     * @param target the target folder
450     *
451     * @throws CmsException if soemthing goes wrong
452     * @throws NoCustomReplacementException if a custom replacement element was not found
453     */
454    public void run(CmsResource source, CmsResource target) throws CmsException, NoCustomReplacementException {
455
456        run(source, target, null);
457    }
458
459    /**
460     * Starts the page copying process.<p>
461     *
462     * @param source the source (can be either a container page, or a folder whose default file is a container page)
463     * @param target the target folder
464     * @param targetName the name to give the new folder
465     *
466     * @throws CmsException if something goes wrong
467     * @throws NoCustomReplacementException if a custom replacement element was not found
468     */
469    public void run(CmsResource source, CmsResource target, String targetName)
470    throws CmsException, NoCustomReplacementException {
471
472        LOG.info(
473            "Starting page copy process: page='"
474                + source.getRootPath()
475                + "', targetFolder='"
476                + target.getRootPath()
477                + "'");
478        CmsObject rootCms = getRootCms();
479        if (m_copyMode == CopyMode.automatic) {
480            Locale sourceLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, source);
481            Locale targetLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, target);
482            // if same locale, copy elements, otherwise use configured setting
483            LOG.debug(
484                "copy mode automatic: source="
485                    + sourceLocale
486                    + " target="
487                    + targetLocale
488                    + " reuseConfig="
489                    + OpenCms.getLocaleManager().shouldReuseElements()
490                    + "");
491            if (sourceLocale.equals(targetLocale)) {
492                m_copyMode = CopyMode.smartCopyAndChangeLocale;
493            } else {
494                if (OpenCms.getLocaleManager().shouldReuseElements()) {
495                    m_copyMode = CopyMode.reuse;
496                } else {
497                    m_copyMode = CopyMode.smartCopyAndChangeLocale;
498                }
499            }
500        }
501
502        if (source.isFolder()) {
503            if (source.equals(target)) {
504                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_SOURCE_IS_TARGET_0));
505            }
506            CmsResource page = m_cms.readDefaultFile(source, CmsResourceFilter.IGNORE_EXPIRATION);
507            if ((page == null) || !CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
508                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
509            }
510            List<CmsProperty> properties = Lists.newArrayList(m_cms.readPropertyObjects(source, false));
511            Iterator<CmsProperty> iterator = properties.iterator();
512            while (iterator.hasNext()) {
513                CmsProperty prop = iterator.next();
514                // copied folder may be root of a locale subtree, but since we may want to copy to a different locale,
515                // we don't want the locale property in the copy
516                if (prop.getName().equals(CmsPropertyDefinition.PROPERTY_LOCALE)
517                    || prop.getName().equals(CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS)) {
518                    iterator.remove();
519                }
520            }
521
522            I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
523            String copyPath;
524            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(targetName)) {
525                copyPath = CmsStringUtil.joinPaths(target.getRootPath(), targetName);
526                if (rootCms.existsResource(copyPath)) {
527                    CmsResource existingResource = rootCms.readResource(copyPath);
528                    // only overwrite the existing resource if it's a folder, otherwise find the next non-existing 'numbered' target path
529                    if (!existingResource.isFolder()) {
530                        copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
531                    }
532                }
533            } else {
534                copyPath = CmsFileUtil.removeTrailingSeparator(
535                    CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
536                copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
537            }
538            Double maxNavPosObj = readMaxNavPos(target);
539            double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
540            boolean hasNavpos = maxNavPosObj != null;
541            CmsResource copiedFolder = null;
542            CmsLockActionRecord lockRecord = null;
543            if (rootCms.existsResource(copyPath)) {
544                copiedFolder = rootCms.readResource(copyPath);
545                lockRecord = CmsLockUtil.ensureLock(rootCms, copiedFolder);
546                rootCms.writePropertyObjects(copyPath, properties);
547            } else {
548                copiedFolder = rootCms.createResource(
549                    copyPath,
550                    OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.RESOURCE_TYPE_NAME),
551                    null,
552                    properties);
553            }
554            if (hasNavpos) {
555                String newNavPosStr = "" + (maxNavpos + 10);
556                rootCms.writePropertyObject(
557                    copiedFolder.getRootPath(),
558                    new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
559            }
560            String pageCopyPath = CmsStringUtil.joinPaths(copiedFolder.getRootPath(), page.getName());
561            m_originalPage = page;
562            m_targetFolder = target;
563            m_copiedFolderOrPage = copiedFolder;
564            if (rootCms.existsResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION)) {
565                rootCms.deleteResource(pageCopyPath, CmsResource.DELETE_PRESERVE_SIBLINGS);
566            }
567            rootCms.copyResource(page.getRootPath(), pageCopyPath);
568
569            CmsResource copiedPage = rootCms.readResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION);
570
571            replaceElements(copiedPage);
572            attachLocaleGroups(copiedPage);
573            if ((lockRecord == null) || (lockRecord.getChange() == LockChange.locked)) {
574                tryUnlock(copiedFolder);
575            }
576        } else {
577            CmsResource page = source;
578            if (!CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
579                throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
580            }
581            I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
582            String copyPath = CmsFileUtil.removeTrailingSeparator(
583                CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
584            int lastDot = copyPath.lastIndexOf(".");
585            int lastSlash = copyPath.lastIndexOf("/");
586            if (lastDot > lastSlash) { // path has an extension
587                String macroPath = copyPath.substring(0, lastDot) + "%(number)" + copyPath.substring(lastDot);
588                copyPath = nameGen.getNewFileName(rootCms, macroPath, 4, true);
589            } else {
590                copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
591            }
592            Double maxNavPosObj = readMaxNavPos(target);
593            double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
594            boolean hasNavpos = maxNavPosObj != null;
595            rootCms.copyResource(page.getRootPath(), copyPath);
596            if (hasNavpos) {
597                String newNavPosStr = "" + (maxNavpos + 10);
598                rootCms.writePropertyObject(
599                    copyPath,
600                    new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
601            }
602            CmsResource copiedPage = rootCms.readResource(copyPath);
603            m_originalPage = page;
604            m_targetFolder = target;
605            m_copiedFolderOrPage = copiedPage;
606            replaceElements(copiedPage);
607            attachLocaleGroups(copiedPage);
608            tryUnlock(copiedPage);
609
610        }
611    }
612
613    /**
614     * Sets the copy mode.<p>
615     *
616     * @param copyMode the copy mode
617     */
618    public void setCopyMode(CopyMode copyMode) {
619
620        m_copyMode = copyMode;
621    }
622
623    /**
624     * Reads the max nav position from the contents of a folder.<p>
625     *
626     * @param target a folder
627     * @return the maximal NavPos from the contents of the folder, or null if no resources with a valid NavPos were found in the folder
628     *
629     * @throws CmsException if something goes wrong
630     */
631    Double readMaxNavPos(CmsResource target) throws CmsException {
632
633        List<CmsResource> existingResourcesInFolder = m_cms.readResources(
634            target,
635            CmsResourceFilter.IGNORE_EXPIRATION,
636            false);
637
638        double maxNavpos = 0.0;
639        boolean hasNavpos = false;
640        for (CmsResource existingResource : existingResourcesInFolder) {
641            CmsProperty navpos = m_cms.readPropertyObject(
642                existingResource,
643                CmsPropertyDefinition.PROPERTY_NAVPOS,
644                false);
645            if (navpos.getValue() != null) {
646                try {
647                    double navposNum = Double.parseDouble(navpos.getValue());
648                    hasNavpos = true;
649                    maxNavpos = Math.max(navposNum, maxNavpos);
650                } catch (NumberFormatException e) {
651                    // ignore
652                }
653            }
654        }
655        if (hasNavpos) {
656            return Double.valueOf(maxNavpos);
657        } else {
658            return null;
659        }
660    }
661
662    /**
663     * Attaches locale groups to the copied page.
664     * @param copiedPage the copied page.
665     * @throws CmsException thrown if the root cms cannot be retrieved.
666     */
667    private void attachLocaleGroups(CmsResource copiedPage) throws CmsException {
668
669        CmsLocaleGroupService localeGroupService = getRootCms().getLocaleGroupService();
670        if (Status.linkable == localeGroupService.checkLinkable(m_originalPage, copiedPage)) {
671            try {
672                localeGroupService.attachLocaleGroupIndirect(m_originalPage, copiedPage);
673            } catch (CmsException e) {
674                LOG.error(e.getLocalizedMessage(), e);
675            }
676        }
677    }
678
679    /**
680     * Return the cms object with the site root set to "/".
681     * @return the cms object with the site root set to "/".
682     * @throws CmsException thrown if initializing the root cms object fails.
683     */
684    private CmsObject getRootCms() throws CmsException {
685
686        if (null == m_rootCms) {
687            m_rootCms = OpenCms.initCmsObject(m_cms);
688            m_rootCms.getRequestContext().setSiteRoot("");
689        }
690        return m_rootCms;
691    }
692
693    /**
694     * Uses the custom translation table to translate formatter id.<p>
695     *
696     * @param formatterId the formatter id
697     * @return the formatter replacement
698     */
699    private CmsUUID maybeReplaceFormatter(CmsUUID formatterId) {
700
701        if (m_customReplacements != null) {
702            CmsUUID replacement = m_customReplacements.get(formatterId);
703            if (replacement != null) {
704                return replacement;
705            }
706        }
707        return formatterId;
708    }
709
710    /**
711     * Replaces formatter id in element settings.<p>
712     *
713     * @param individualSettings the settings in which to replace the formatter id
714     *
715     * @return the map with the possible replaced ids
716     */
717    private Map<String, String> maybeReplaceFormatterInSettings(Map<String, String> individualSettings) {
718
719        if (individualSettings == null) {
720            return null;
721        } else if (m_customReplacements == null) {
722            return individualSettings;
723        } else {
724            LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();
725            for (Map.Entry<String, String> entry : individualSettings.entrySet()) {
726                String value = entry.getValue();
727                if (CmsUUID.isValidUUID(value)) {
728                    CmsUUID valueId = new CmsUUID(value);
729                    if (m_customReplacements.containsKey(valueId)) {
730                        value = "" + m_customReplacements.get(valueId);
731                    }
732                }
733                result.put(entry.getKey(), value);
734            }
735            return result;
736        }
737    }
738
739    /**
740     * Tries to unlock the given resource.<p>
741     *
742     * @param resource the resource to unlock
743     */
744    private void tryUnlock(CmsResource resource) {
745
746        try {
747            m_cms.unlockResource(resource);
748        } catch (CmsException e) {
749            // usually not a problem
750            LOG.debug("failed to unlock " + resource.getRootPath(), e);
751        }
752
753    }
754
755}