content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   * Lesser General Public License for more details.
   *
   * For further information about Alkacon Software, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://www.opencms.org
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package org.opencms.i18n.tools;
  
  import org.opencms.ade.configuration.CmsADEConfigData;
  import org.opencms.ade.configuration.CmsResourceTypeConfig;
  import org.opencms.file.CmsFile;
  import org.opencms.file.CmsObject;
  import org.opencms.file.CmsProperty;
  import org.opencms.file.CmsPropertyDefinition;
  import org.opencms.file.CmsResource;
  import org.opencms.file.CmsResourceFilter;
  import org.opencms.file.types.CmsResourceTypeFolder;
  import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
  import org.opencms.file.types.I_CmsResourceType;
  import org.opencms.i18n.CmsLocaleGroupService;
  import org.opencms.i18n.CmsLocaleGroupService.Status;
  import org.opencms.i18n.CmsLocaleManager;
  import org.opencms.i18n.CmsMessageContainer;
  import org.opencms.loader.I_CmsFileNameGenerator;
  import org.opencms.lock.CmsLockActionRecord;
  import org.opencms.lock.CmsLockActionRecord.LockChange;
  import org.opencms.lock.CmsLockUtil;
  import org.opencms.main.CmsException;
  import org.opencms.main.CmsLog;
  import org.opencms.main.OpenCms;
  import org.opencms.site.CmsSite;
  import org.opencms.ui.Messages;
  import org.opencms.util.CmsFileUtil;
  import org.opencms.util.CmsMacroResolver;
  import org.opencms.util.CmsStringUtil;
  import org.opencms.util.CmsUUID;
  import org.opencms.xml.CmsXmlException;
  import org.opencms.xml.containerpage.CmsContainerBean;
  import org.opencms.xml.containerpage.CmsContainerElementBean;
  import org.opencms.xml.containerpage.CmsContainerPageBean;
  import org.opencms.xml.containerpage.CmsXmlContainerPage;
  import org.opencms.xml.containerpage.CmsXmlContainerPageFactory;
  import org.opencms.xml.content.CmsXmlContent;
  import org.opencms.xml.content.CmsXmlContentFactory;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Set;
  
  import org.apache.commons.logging.Log;
  
  import com.google.common.collect.Lists;
  import com.google.common.collect.Maps;
  import com.google.common.collect.Sets;
  
  /**
   * Helper class for copying container pages including some of their elements.<p>
   */
  public class CmsContainerPageCopier {
  
      /**
       * Enum representing the element copy mode.<p>
       */
      public enum CopyMode {
          /** Choose between reuse / copy automatically depending on source / target locale and the configuration .*/
          automatic,
  
          /** Do not copy elements. */
          reuse,
  
          /** Automatically determine when to copy elements. */
          smartCopy,
  
          /** Like smartCopy, but also converts locales of copied elements. */
          smartCopyAndChangeLocale;
  
      }
  
      /**
       * Exception indicating that no custom replacement element was found
       * for a type which requires replacement.<p>
       */
      public static class NoCustomReplacementException extends Exception {
  
          /** Serial version id. */
          private static final long serialVersionUID = 1L;
  
          /** The resource for which no exception was found. */
          private CmsResource m_resource;
  
          /**
           * Creates a new instance.<p>
           *
           * @param resource the resource for which no replacement was found
           */
          public NoCustomReplacementException(CmsResource resource) {
  
              super();
              m_resource = resource;
          }
  
          /**
           * Gets the resource for which no replacement was found.<p>
           *
           * @return the resource
           */
          public CmsResource getResource() {
  
              return m_resource;
          }
      }
  
      /** The log instance used for this class. */
      private static final Log LOG = CmsLog.getLog(CmsContainerPageCopier.class);
  
      /** The CMS context used by this object. */
      private CmsObject m_cms;
  
      /** The CMS context used by this object, but with the site root set to "". */
      private CmsObject m_rootCms;
  
      /** The copied resource. */
      private CmsResource m_copiedFolderOrPage;
  
      /** The copy mode. */
      private CopyMode m_copyMode = CopyMode.smartCopyAndChangeLocale;
  
      /** Map of custom replacements. */
      private Map<CmsUUID, CmsUUID> m_customReplacements;
  
      /** Maps structure ids of original container elements to structure ids of their copies/replacements. */
      private Map<CmsUUID, CmsUUID> m_elementReplacements = Maps.newHashMap();
  
      /** The original page. */
      private CmsResource m_originalPage;
  
      /** The target folder. */
      private CmsResource m_targetFolder;
  
      /** Resource types which require custom replacements. */
      private Set<String> m_typesWithRequiredReplacements;
  
      /**
       * Creates a new instance.<p>
       *
       * @param cms the CMS context to use
       */
      public CmsContainerPageCopier(CmsObject cms) {
  
          m_cms = cms;
      }
  
      /**
       * Converts locales for the copied container element.<p>
       *
       * @param elementResource the copied container element resource
       * @param originalResource the original container element resource
       *
       * @throws CmsException if something goes wrong
       */
      public void adjustLocalesForElement(CmsResource elementResource, CmsResource originalResource) throws CmsException {
  
          if (m_copyMode != CopyMode.smartCopyAndChangeLocale) {
              return;
          }
  
          CmsFile file = m_cms.readFile(elementResource);
          Locale oldLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_originalPage);
          Locale newLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_targetFolder);
          CmsXmlContent content = CmsXmlContentFactory.unmarshal(m_cms, file);
          try {
              if (content.hasLocale(newLocale)) {
                  // check if the new locale was already present in the original resource, it may have been created automatically
                  CmsFile origFile = m_cms.readFile(originalResource);
                  CmsXmlContent origContent = CmsXmlContentFactory.unmarshal(m_cms, origFile);
                  if (!origContent.hasLocale(newLocale)) {
                      // the target locale was not present in the original, remove it to ensure the source locale is moved
                      content.removeLocale(newLocale);
                  }
              }
              content.moveLocale(oldLocale, newLocale);
              LOG.info("Replacing locale " + oldLocale + " -> " + newLocale + " for " + elementResource.getRootPath());
              file.setContents(content.marshal());
              m_cms.writeFile(file);
          } catch (CmsXmlException e) {
              LOG.info(
                  "NOT replacing locale for "
                      + elementResource.getRootPath()
                      + ": old="
                      + oldLocale
                      + ", new="
                      + newLocale
                      + ", contentLocales="
                      + content.getLocales());
          }
  
      }
  
      /**
       * Copies the given container page to the provided root path.
       * @param originalPage the page to copy
       * @param targetPageRootPath the root path of the copy target.
       * @throws CmsException thrown if something goes wrong.
       * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it.
       */
      public void copyPageOnly(CmsResource originalPage, String targetPageRootPath)
      throws CmsException, NoCustomReplacementException {
  
          if ((null == originalPage)
              || !OpenCms.getResourceManager().getResourceType(originalPage).getTypeName().equals(
                  CmsResourceTypeXmlContainerPage.getStaticTypeName())) {
              throw new CmsException(new CmsMessageContainer(Messages.get(), Messages.ERR_PAGECOPY_INVALID_PAGE_0));
          }
          m_originalPage = originalPage;
          CmsObject rootCms = getRootCms();
          rootCms.copyResource(originalPage.getRootPath(), targetPageRootPath);
          CmsResource copiedPage = rootCms.readResource(targetPageRootPath, CmsResourceFilter.IGNORE_EXPIRATION);
          m_targetFolder = rootCms.readResource(CmsResource.getFolderPath(copiedPage.getRootPath()));
          replaceElements(copiedPage);
          attachLocaleGroups(copiedPage);
          tryUnlock(copiedPage);
  
      }
  
      /**
       * Gets the copied folder or page.<p>
       *
       * @return the copied folder or page
       */
      public CmsResource getCopiedFolderOrPage() {
  
          return m_copiedFolderOrPage;
      }
  
      /**
       * Returns the target folder.<p>
       *
       * @return the target folder
       */
      public CmsResource getTargetFolder() {
  
          return m_targetFolder;
      }
  
      /**
       * Produces the replacement for a container page element to use in a copy of an existing container page.<p>
       *
       * @param targetPage the target container page
       * @param originalElement the original element
       * @return the replacement element for the copied page
       *
       * @throws CmsException if something goes wrong
       * @throws NoCustomReplacementException if a custom replacement is not found for a type which requires it
       */
      public CmsContainerElementBean replaceContainerElement(
          CmsResource targetPage,
          CmsContainerElementBean originalElement)
      throws CmsException, NoCustomReplacementException {
          // if (m_elementReplacements.containsKey(originalElement.getId()
  
          CmsObject targetCms = OpenCms.initCmsObject(m_cms);
  
          CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
          if (site != null) {
              targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
          }
  
          if (originalElement.getId() == null) {
              String rootPath = m_originalPage != null ? m_originalPage.getRootPath() : "???";
              LOG.warn("Skipping container element because of missing id in page: " + rootPath);
              return null;
          }
  
          if (m_elementReplacements.containsKey(originalElement.getId())) {
              return new CmsContainerElementBean(
                  m_elementReplacements.get(originalElement.getId()),
                  maybeReplaceFormatter(originalElement.getFormatterId()),
                  maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
                  originalElement.isCreateNew());
          } else {
              CmsResource originalResource = m_cms.readResource(
                  originalElement.getId(),
                  CmsResourceFilter.IGNORE_EXPIRATION);
              I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(originalResource);
              CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(m_cms, targetPage.getRootPath());
              CmsResourceTypeConfig typeConfig = config.getResourceType(type.getTypeName());
              if ((m_copyMode != CopyMode.reuse)
                  && (typeConfig != null)
                  && (originalElement.isCreateNew() || typeConfig.isCopyInModels())
                  && !type.getTypeName().equals(CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME)) {
                  // set the request context locale to the target content locale as this is used during content creation
                  Locale targetLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, m_targetFolder);
                  targetCms.getRequestContext().setLocale(targetLocale);
                  CmsResource resourceCopy = typeConfig.createNewElement(
                      targetCms,
                      originalResource,
                      CmsResource.getParentFolder(targetPage.getRootPath()));
                  CmsContainerElementBean copy = new CmsContainerElementBean(
                      resourceCopy.getStructureId(),
                      maybeReplaceFormatter(originalElement.getFormatterId()),
                      maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
                      originalElement.isCreateNew());
                  m_elementReplacements.put(originalElement.getId(), resourceCopy.getStructureId());
                  LOG.info(
                      "Copied container element " + originalResource.getRootPath() + " -> " + resourceCopy.getRootPath());
                  CmsLockActionRecord record = null;
                  try {
                      record = CmsLockUtil.ensureLock(m_cms, resourceCopy);
                      adjustLocalesForElement(resourceCopy, originalResource);
                  } finally {
                      if ((record != null) && (record.getChange() == LockChange.locked)) {
                          m_cms.unlockResource(resourceCopy);
                      }
                  }
                  return copy;
              } else if (m_customReplacements != null) {
                  CmsUUID replacementId = m_customReplacements.get(originalElement.getId());
                  if (replacementId != null) {
  
                      return new CmsContainerElementBean(
                          replacementId,
                          maybeReplaceFormatter(originalElement.getFormatterId()),
                          maybeReplaceFormatterInSettings(originalElement.getIndividualSettings()),
                          originalElement.isCreateNew());
                  } else {
                      if ((m_typesWithRequiredReplacements != null)
                          && m_typesWithRequiredReplacements.contains(type.getTypeName())) {
                          throw new NoCustomReplacementException(originalResource);
                      } else {
                          return originalElement;
                      }
  
                  }
              } else {
                  LOG.info("Reusing container element: " + originalResource.getRootPath());
                  return originalElement;
              }
          }
      }
  
      /**
       * Replaces the elements in the copied container page with copies, if appropriate based on the current copy mode.<p>
       *
       * @param containerPage the container page copy whose elements should be replaced with copies
       *
       * @throws CmsException if something goes wrong
       * @throws NoCustomReplacementException if a custom replacement element was not found for a type which requires it
       */
      public void replaceElements(CmsResource containerPage) throws CmsException, NoCustomReplacementException {
  
          CmsObject rootCms = getRootCms();
          CmsObject targetCms = OpenCms.initCmsObject(m_cms);
          targetCms.getRequestContext().setSiteRoot("");
          CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(m_targetFolder.getRootPath());
          if (site != null) {
              targetCms.getRequestContext().setSiteRoot(site.getSiteRoot());
          } else if (OpenCms.getSiteManager().startsWithShared(m_targetFolder.getRootPath())) {
              targetCms.getRequestContext().setSiteRoot(OpenCms.getSiteManager().getSharedFolder());
          }
  
          CmsProperty elementReplacementProp = rootCms.readPropertyObject(
              m_targetFolder,
              CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS,
              true);
          if ((elementReplacementProp != null) && (elementReplacementProp.getValue() != null)) {
              try {
                  CmsResource elementReplacementMap = targetCms.readResource(
                      elementReplacementProp.getValue(),
                      CmsResourceFilter.IGNORE_EXPIRATION);
                  OpenCms.getLocaleManager();
                  String encoding = CmsLocaleManager.getResourceEncoding(targetCms, elementReplacementMap);
                  CmsFile elementReplacementFile = targetCms.readFile(elementReplacementMap);
                  Properties props = new Properties();
                  props.load(
                      new InputStreamReader(new ByteArrayInputStream(elementReplacementFile.getContents()), encoding));
                  CmsMacroResolver resolver = new CmsMacroResolver();
                  resolver.addMacro("sourcesite", m_cms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
                  resolver.addMacro("targetsite", targetCms.getRequestContext().getSiteRoot().replaceAll("/+$", ""));
                  Map<CmsUUID, CmsUUID> customReplacements = Maps.newHashMap();
                  for (Map.Entry<Object, Object> entry : props.entrySet()) {
                      if ((entry.getKey() instanceof String) && (entry.getValue() instanceof String)) {
                          try {
                              String key = (String)entry.getKey();
                              if ("required".equals(key)) {
                                  m_typesWithRequiredReplacements = Sets.newHashSet(
                                      ((String)entry.getValue()).split(" *, *"));
                                  continue;
                              }
                              key = resolver.resolveMacros(key);
                              String value = (String)entry.getValue();
                              value = resolver.resolveMacros(value);
                              CmsResource keyRes = rootCms.readResource(key, CmsResourceFilter.IGNORE_EXPIRATION);
                              CmsResource valRes = rootCms.readResource(value, CmsResourceFilter.IGNORE_EXPIRATION);
                              customReplacements.put(keyRes.getStructureId(), valRes.getStructureId());
                          } catch (Exception e) {
                              LOG.error(e.getLocalizedMessage(), e);
                          }
                          m_customReplacements = customReplacements;
                      }
                  }
              } catch (CmsException e) {
                  LOG.warn(e.getLocalizedMessage(), e);
              } catch (IOException e) {
                  LOG.warn(e.getLocalizedMessage(), e);
              }
          }
  
          CmsXmlContainerPage pageXml = CmsXmlContainerPageFactory.unmarshal(m_cms, containerPage);
          CmsContainerPageBean page = pageXml.getContainerPage(m_cms);
          List<CmsContainerBean> newContainers = Lists.newArrayList();
          for (CmsContainerBean container : page.getContainers().values()) {
              List<CmsContainerElementBean> newElements = Lists.newArrayList();
              for (CmsContainerElementBean element : container.getElements()) {
                  CmsContainerElementBean newBean = replaceContainerElement(containerPage, element);
                  if (newBean != null) {
                      newElements.add(newBean);
                  }
              }
              CmsContainerBean newContainer = container.copyWithNewElements(newElements);
              newContainers.add(newContainer);
          }
          CmsContainerPageBean newPageBean = new CmsContainerPageBean(newContainers);
          pageXml.save(rootCms, newPageBean);
      }
  
      /**
       * Starts the page copying process.<p>
       *
       * @param source the source (can be either a container page, or a folder whose default file is a container page)
       * @param target the target folder
       *
       * @throws CmsException if soemthing goes wrong
       * @throws NoCustomReplacementException if a custom replacement element was not found
       */
      public void run(CmsResource source, CmsResource target) throws CmsException, NoCustomReplacementException {
  
          run(source, target, null);
      }
  
      /**
       * Starts the page copying process.<p>
       *
       * @param source the source (can be either a container page, or a folder whose default file is a container page)
       * @param target the target folder
       * @param targetName the name to give the new folder
       *
       * @throws CmsException if something goes wrong
       * @throws NoCustomReplacementException if a custom replacement element was not found
       */
      public void run(CmsResource source, CmsResource target, String targetName)
      throws CmsException, NoCustomReplacementException {
  
          LOG.info(
              "Starting page copy process: page='"
                  + source.getRootPath()
                  + "', targetFolder='"
                  + target.getRootPath()
                  + "'");
          CmsObject rootCms = getRootCms();
          if (m_copyMode == CopyMode.automatic) {
              Locale sourceLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, source);
              Locale targetLocale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, target);
              // if same locale, copy elements, otherwise use configured setting
              LOG.debug(
                  "copy mode automatic: source="
                      + sourceLocale
                      + " target="
                      + targetLocale
                      + " reuseConfig="
                      + OpenCms.getLocaleManager().shouldReuseElements()
                      + "");
              if (sourceLocale.equals(targetLocale)) {
                  m_copyMode = CopyMode.smartCopyAndChangeLocale;
              } else {
                  if (OpenCms.getLocaleManager().shouldReuseElements()) {
                      m_copyMode = CopyMode.reuse;
                  } else {
                      m_copyMode = CopyMode.smartCopyAndChangeLocale;
                  }
              }
          }
  
          if (source.isFolder()) {
              if (source.equals(target)) {
                  throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_SOURCE_IS_TARGET_0));
              }
              CmsResource page = m_cms.readDefaultFile(source, CmsResourceFilter.IGNORE_EXPIRATION);
              if ((page == null) || !CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
                  throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
              }
              List<CmsProperty> properties = Lists.newArrayList(m_cms.readPropertyObjects(source, false));
              Iterator<CmsProperty> iterator = properties.iterator();
              while (iterator.hasNext()) {
                  CmsProperty prop = iterator.next();
                  // copied folder may be root of a locale subtree, but since we may want to copy to a different locale,
                  // we don't want the locale property in the copy
                  if (prop.getName().equals(CmsPropertyDefinition.PROPERTY_LOCALE)
                      || prop.getName().equals(CmsPropertyDefinition.PROPERTY_ELEMENT_REPLACEMENTS)) {
                      iterator.remove();
                  }
              }
  
              I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
              String copyPath;
              if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(targetName)) {
                  copyPath = CmsStringUtil.joinPaths(target.getRootPath(), targetName);
                  if (rootCms.existsResource(copyPath)) {
                      CmsResource existingResource = rootCms.readResource(copyPath);
                      // only overwrite the existing resource if it's a folder, otherwise find the next non-existing 'numbered' target path
                      if (!existingResource.isFolder()) {
                          copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
                      }
                  }
              } else {
                  copyPath = CmsFileUtil.removeTrailingSeparator(
                      CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
                  copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
              }
              Double maxNavPosObj = readMaxNavPos(target);
              double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
              boolean hasNavpos = maxNavPosObj != null;
              CmsResource copiedFolder = null;
              CmsLockActionRecord lockRecord = null;
              if (rootCms.existsResource(copyPath)) {
                  copiedFolder = rootCms.readResource(copyPath);
                  lockRecord = CmsLockUtil.ensureLock(rootCms, copiedFolder);
                  rootCms.writePropertyObjects(copyPath, properties);
              } else {
                  copiedFolder = rootCms.createResource(
                      copyPath,
                      OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.RESOURCE_TYPE_NAME),
                      null,
                      properties);
              }
              if (hasNavpos) {
                  String newNavPosStr = "" + (maxNavpos + 10);
                  rootCms.writePropertyObject(
                      copiedFolder.getRootPath(),
                      new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
              }
              String pageCopyPath = CmsStringUtil.joinPaths(copiedFolder.getRootPath(), page.getName());
              m_originalPage = page;
              m_targetFolder = target;
              m_copiedFolderOrPage = copiedFolder;
              if (rootCms.existsResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION)) {
                  rootCms.deleteResource(pageCopyPath, CmsResource.DELETE_PRESERVE_SIBLINGS);
              }
              rootCms.copyResource(page.getRootPath(), pageCopyPath);
  
              CmsResource copiedPage = rootCms.readResource(pageCopyPath, CmsResourceFilter.IGNORE_EXPIRATION);
  
              replaceElements(copiedPage);
              attachLocaleGroups(copiedPage);
              if ((lockRecord == null) || (lockRecord.getChange() == LockChange.locked)) {
                  tryUnlock(copiedFolder);
              }
          } else {
              CmsResource page = source;
              if (!CmsResourceTypeXmlContainerPage.isContainerPage(page)) {
                  throw new CmsException(Messages.get().container(Messages.ERR_PAGECOPY_INVALID_PAGE_0));
              }
              I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
              String copyPath = CmsFileUtil.removeTrailingSeparator(
                  CmsStringUtil.joinPaths(target.getRootPath(), source.getName()));
              int lastDot = copyPath.lastIndexOf(".");
              int lastSlash = copyPath.lastIndexOf("/");
              if (lastDot > lastSlash) { // path has an extension
                  String macroPath = copyPath.substring(0, lastDot) + "%(number)" + copyPath.substring(lastDot);
                  copyPath = nameGen.getNewFileName(rootCms, macroPath, 4, true);
              } else {
                  copyPath = nameGen.getNewFileName(rootCms, copyPath + "%(number)", 4, true);
              }
              Double maxNavPosObj = readMaxNavPos(target);
              double maxNavpos = maxNavPosObj == null ? 0 : maxNavPosObj.doubleValue();
              boolean hasNavpos = maxNavPosObj != null;
              rootCms.copyResource(page.getRootPath(), copyPath);
              if (hasNavpos) {
                  String newNavPosStr = "" + (maxNavpos + 10);
                  rootCms.writePropertyObject(
                      copyPath,
                      new CmsProperty(CmsPropertyDefinition.PROPERTY_NAVPOS, newNavPosStr, null));
              }
              CmsResource copiedPage = rootCms.readResource(copyPath);
              m_originalPage = page;
              m_targetFolder = target;
              m_copiedFolderOrPage = copiedPage;
              replaceElements(copiedPage);
              attachLocaleGroups(copiedPage);
              tryUnlock(copiedPage);
  
          }
      }
  
      /**
       * Sets the copy mode.<p>
       *
       * @param copyMode the copy mode
       */
      public void setCopyMode(CopyMode copyMode) {
  
          m_copyMode = copyMode;
      }
  
      /**
       * Reads the max nav position from the contents of a folder.<p>
       *
       * @param target a folder
       * @return the maximal NavPos from the contents of the folder, or null if no resources with a valid NavPos were found in the folder
       *
       * @throws CmsException if something goes wrong
       */
      Double readMaxNavPos(CmsResource target) throws CmsException {
  
          List<CmsResource> existingResourcesInFolder = m_cms.readResources(
              target,
              CmsResourceFilter.IGNORE_EXPIRATION,
              false);
  
          double maxNavpos = 0.0;
          boolean hasNavpos = false;
          for (CmsResource existingResource : existingResourcesInFolder) {
              CmsProperty navpos = m_cms.readPropertyObject(
                  existingResource,
                  CmsPropertyDefinition.PROPERTY_NAVPOS,
                  false);
              if (navpos.getValue() != null) {
                  try {
                      double navposNum = Double.parseDouble(navpos.getValue());
                      hasNavpos = true;
                      maxNavpos = Math.max(navposNum, maxNavpos);
                  } catch (NumberFormatException e) {
                      // ignore
                  }
              }
          }
          if (hasNavpos) {
              return Double.valueOf(maxNavpos);
          } else {
              return null;
          }
      }
  
      /**
       * Attaches locale groups to the copied page.
       * @param copiedPage the copied page.
       * @throws CmsException thrown if the root cms cannot be retrieved.
       */
      private void attachLocaleGroups(CmsResource copiedPage) throws CmsException {
  
          CmsLocaleGroupService localeGroupService = getRootCms().getLocaleGroupService();
          if (Status.linkable == localeGroupService.checkLinkable(m_originalPage, copiedPage)) {
              try {
                  localeGroupService.attachLocaleGroupIndirect(m_originalPage, copiedPage);
              } catch (CmsException e) {
                  LOG.error(e.getLocalizedMessage(), e);
              }
          }
      }
  
      /**
       * Return the cms object with the site root set to "/".
       * @return the cms object with the site root set to "/".
       * @throws CmsException thrown if initializing the root cms object fails.
       */
      private CmsObject getRootCms() throws CmsException {
  
          if (null == m_rootCms) {
              m_rootCms = OpenCms.initCmsObject(m_cms);
              m_rootCms.getRequestContext().setSiteRoot("");
          }
          return m_rootCms;
      }
  
      /**
       * Uses the custom translation table to translate formatter id.<p>
       *
       * @param formatterId the formatter id
       * @return the formatter replacement
       */
      private CmsUUID maybeReplaceFormatter(CmsUUID formatterId) {
  
          if (m_customReplacements != null) {
              CmsUUID replacement = m_customReplacements.get(formatterId);
              if (replacement != null) {
                  return replacement;
              }
          }
          return formatterId;
      }
  
      /**
       * Replaces formatter id in element settings.<p>
       *
       * @param individualSettings the settings in which to replace the formatter id
       *
       * @return the map with the possible replaced ids
       */
      private Map<String, String> maybeReplaceFormatterInSettings(Map<String, String> individualSettings) {
  
          if (individualSettings == null) {
              return null;
          } else if (m_customReplacements == null) {
              return individualSettings;
          } else {
              LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();
              for (Map.Entry<String, String> entry : individualSettings.entrySet()) {
                  String value = entry.getValue();
                  if (CmsUUID.isValidUUID(value)) {
                      CmsUUID valueId = new CmsUUID(value);
                      if (m_customReplacements.containsKey(valueId)) {
                          value = "" + m_customReplacements.get(valueId);
                      }
                  }
                  result.put(entry.getKey(), value);
              }
              return result;
          }
      }
  
      /**
       * Tries to unlock the given resource.<p>
       *
       * @param resource the resource to unlock
       */
      private void tryUnlock(CmsResource resource) {
  
          try {
              m_cms.unlockResource(resource);
          } catch (CmsException e) {
              // usually not a problem
              LOG.debug("failed to unlock " + resource.getRootPath(), e);
          }
  
      }
  
  }
  