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.ui.editors.messagebundle;
  
  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.CmsResource.CmsResourceDeleteMode;
  import org.opencms.file.CmsResourceFilter;
  import org.opencms.file.CmsVfsResourceNotFoundException;
  import org.opencms.i18n.CmsLocaleManager;
  import org.opencms.i18n.CmsMessageContainer;
  import org.opencms.i18n.CmsMessageException;
  import org.opencms.i18n.CmsMessages;
  import org.opencms.loader.CmsLoaderException;
  import org.opencms.lock.CmsLockUtil.LockedFile;
  import org.opencms.main.CmsException;
  import org.opencms.main.CmsIllegalArgumentException;
  import org.opencms.main.CmsLog;
  import org.opencms.main.OpenCms;
  import org.opencms.security.CmsPermissionSet;
  import org.opencms.ui.A_CmsDialogContext;
  import org.opencms.ui.I_CmsDialogContext;
  import org.opencms.ui.I_CmsDialogContext.ContextType;
  import org.opencms.ui.actions.CmsDirectPublishDialogAction;
  import org.opencms.ui.contextmenu.CmsContextMenu;
  import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItem;
  import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickEvent;
  import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickListener;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.BundleType;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.Descriptor;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EditMode;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EditorState;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EntryChangeEvent;
  import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.TableProperty;
  import org.opencms.util.CmsFileUtil;
  import org.opencms.util.CmsStringUtil;
  import org.opencms.util.CmsUUID;
  import org.opencms.xml.CmsXmlException;
  import org.opencms.xml.content.CmsXmlContent;
  import org.opencms.xml.content.CmsXmlContentFactory;
  import org.opencms.xml.content.CmsXmlContentValueSequence;
  import org.opencms.xml.types.I_CmsXmlContentValue;
  
  import java.io.BufferedWriter;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.OutputStreamWriter;
  import java.io.UnsupportedEncodingException;
  import java.io.Writer;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Properties;
  import java.util.Set;
  
  import org.apache.commons.logging.Log;
  
  import com.vaadin.ui.UI;
  import com.vaadin.v7.data.Item;
  import com.vaadin.v7.data.Property;
  import com.vaadin.v7.data.util.DefaultItemSorter;
  import com.vaadin.v7.data.util.IndexedContainer;
  
  /**
   * The class contains the logic behind the message translation editor.
   * In particular it reads / writes the involved files and provides the contents as {@link IndexedContainer}.
   */
  public class CmsMessageBundleEditorModel {
  
      /** Comparator that compares strings case insensitive. */
      public static final class CmsCaseInsensitiveStringComparator implements Comparator<Object> {
  
          /** Single instance of the comparator. */
          private static CmsCaseInsensitiveStringComparator m_instance = new CmsCaseInsensitiveStringComparator();
  
          /**
           * Hide the default constructor.
           */
          private CmsCaseInsensitiveStringComparator() {
  
              // Hide constructor
          }
  
          /**
           * Returns the comparator instance.
           * @return the comparator
           */
          public static CmsCaseInsensitiveStringComparator getInstance() {
  
              // is never null, because it's directly initialized on class load.
              return m_instance;
          }
  
          /**
           * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
           */
          @SuppressWarnings("unchecked")
          public int compare(Object o1, Object o2) {
  
              int r = 0;
              // Normal non-null comparison
              if ((o1 != null) && (o2 != null)) {
                  if ((o1 instanceof String) && (o2 instanceof String)) {
                      String string1 = (String)o1;
                      String string2 = (String)o2;
                      r = String.CASE_INSENSITIVE_ORDER.compare(string1, string2);
                      if (r == 0) {
                          r = string1.compareTo(string2);
                      }
                  } else {
                      // Assume the objects can be cast to Comparable, throw
                      // ClassCastException otherwise.
                      r = ((Comparable<Object>)o1).compareTo(o2);
                  }
              } else if (o1 == o2) {
                  // Objects are equal if both are null
                  r = 0;
              } else {
                  if (o1 == null) {
                      r = -1; // null is less than non-null
                  } else {
                      r = 1; // non-null is greater than null
                  }
              }
  
              return r;
          }
      }
  
      /** Wrapper for the configurable messages for the column headers of the message bundle editor. */
      public static final class ConfigurableMessages {
  
          /** The messages from the default message bundle. */
          CmsMessages m_defaultMessages;
          /** The messages from a configured message bundle, overwriting the ones from the default bundle. */
          CmsMessages m_configuredMessages;
  
          /**
           * Default constructor.
           * @param defaultMessages the default messages.
           * @param locale the locale in which the messages are requested.
           * @param configuredBundle the base name of the configured message bundle (can be <code>null</code>).
           */
          public ConfigurableMessages(CmsMessages defaultMessages, Locale locale, String configuredBundle) {
  
              m_defaultMessages = defaultMessages;
              if (null != configuredBundle) {
                  CmsMessages bundle = new CmsMessages(configuredBundle, locale);
                  if (null != bundle.getResourceBundle()) {
                      m_configuredMessages = bundle;
                  }
              }
          }
  
          /**
           * Returns the localized column header.
           * @param column the column's property (name).
           * @return the localized columen header.
           */
          public String getColumnHeader(TableProperty column) {
  
              switch (column) {
                  case DEFAULT:
                      return getMessage(Messages.GUI_COLUMN_HEADER_DEFAULT_0);
                  case DESCRIPTION:
                      return getMessage(Messages.GUI_COLUMN_HEADER_DESCRIPTION_0);
                  case KEY:
                      return getMessage(Messages.GUI_COLUMN_HEADER_KEY_0);
                  case OPTIONS:
                      return "";
                  case TRANSLATION:
                      return getMessage(Messages.GUI_COLUMN_HEADER_TRANSLATION_0);
                  default:
                      throw new IllegalArgumentException();
              }
          }
  
          /**
           * Returns the message for the key, either from the configured bundle, or - if not found - from the default bundle.
           *
           * @param key message key.
           * @return the message for the key.
           */
          private String getMessage(String key) {
  
              if (null != m_configuredMessages) {
                  try {
                      return m_configuredMessages.getString(key);
                  } catch (CmsMessageException e) {
                      // do nothing - use default messages
                  }
              }
              try {
                  return m_defaultMessages.getString(key);
              } catch (CmsMessageException e) {
                  return "???" + key + "???";
              }
          }
  
      }
  
      /** Extension of {@link Properties} to allow saving with keys alphabetically ordered and without time stamp as first comment.
       *
       * NOTE: Can't handle comments. They are just discarded.
       * NOTE: Most of the class is just a plain copy of the private methods of {@link Properties}, so be aware that adjustments may be necessary if the {@link Properties} implementation changes.
       * NOTE: The solution was taken to guarantee correct escaping when storing properties.
       *
       */
      public static final class SortedProperties extends Properties {
  
          /** Serialization id to implement Serializable. */
          private static final long serialVersionUID = 8814525892788043348L;
  
          /**
           * NOTE: This is a one-to-one copy from {@link java.util.Properties} with HEX_DIGIT renamed to HEX_DIGIT.
           * A table of hex digits.
           */
          private static final char[] HEX_DIGIT = {
              '0',
              '1',
              '2',
              '3',
              '4',
              '5',
              '6',
              '7',
              '8',
              '9',
              'A',
              'B',
              'C',
              'D',
              'E',
              'F'};
  
          /**
           * Default constructor.
           */
          public SortedProperties() {
  
              super();
          }
  
          /**
           * NOTE: This is a one-to-one copy from {@link java.util.Properties}
           * Convert a nibble to a hex character.
           * @param   nibble  the nibble to convert.
           * @return the character as Hex
           */
          private static char toHex(int nibble) {
  
              return HEX_DIGIT[(nibble & 0xF)];
          }
  
          /**
           * Override to omit the date comment.
           * @see java.util.Properties#store(java.io.OutputStream, java.lang.String)
           */
          @Override
          public void store(OutputStream out, String comments) throws IOException {
  
              store0(new BufferedWriter(new OutputStreamWriter(out, "8859_1")), true);
          }
  
          /**
           * Override to omit the date comment.
           * @see java.util.Properties#store(java.io.Writer, java.lang.String)
           */
          @Override
          public void store(Writer writer, String comments) throws IOException {
  
              store0((writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer), false);
          }
  
          /**
           * NOTE: This is a one-to-one copy of the private method from {@link java.util.Properties}
           * Converts unicodes to encoded &#92;uxxxx and escapes
           * special characters with a preceding slash.
           *
           * @param theString string to convert
           * @param escapeSpace flag, indicating if spaces should be escaped
           * @param escapeUnicode flag, indicating if unicode signs should be escaped
           * @return the converted string
           */
          private String saveConvert(String theString, boolean escapeSpace, boolean escapeUnicode) {
  
              int len = theString.length();
              int bufLen = len * 2;
              if (bufLen < 0) {
                  bufLen = Integer.MAX_VALUE;
              }
              StringBuffer outBuffer = new StringBuffer(bufLen);
  
              for (int x = 0; x < len; x++) {
                  char aChar = theString.charAt(x);
                  // Handle common case first, selecting largest block that
                  // avoids the specials below
                  if ((aChar > 61) && (aChar < 127)) {
                      if (aChar == '\\') {
                          outBuffer.append('\\');
                          outBuffer.append('\\');
                          continue;
                      }
                      outBuffer.append(aChar);
                      continue;
                  }
                  switch (aChar) {
                      case ' ':
                          if ((x == 0) || escapeSpace) {
                              outBuffer.append('\\');
                          }
                          outBuffer.append(' ');
                          break;
                      case '\t':
                          outBuffer.append('\\');
                          outBuffer.append('t');
                          break;
                      case '\n':
                          outBuffer.append('\\');
                          outBuffer.append('n');
                          break;
                      case '\r':
                          outBuffer.append('\\');
                          outBuffer.append('r');
                          break;
                      case '\f':
                          outBuffer.append('\\');
                          outBuffer.append('f');
                          break;
                      case '=': // Fall through
                      case ':': // Fall through
                      case '#': // Fall through
                      case '!':
                          outBuffer.append('\\');
                          outBuffer.append(aChar);
                          break;
                      default:
                          if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) {
                              outBuffer.append('\\');
                              outBuffer.append('u');
                              outBuffer.append(toHex((aChar >> 12) & 0xF));
                              outBuffer.append(toHex((aChar >> 8) & 0xF));
                              outBuffer.append(toHex((aChar >> 4) & 0xF));
                              outBuffer.append(toHex(aChar & 0xF));
                          } else {
                              outBuffer.append(aChar);
                          }
                  }
              }
              return outBuffer.toString();
          }
  
          /**
           * Adaption of {@link java.util.Properties#store0(BufferedWriter,String,boolean)}.
           * The behavior differs as follows:<ul>
           * <li>Comments are not handled.</li>
           * <li>The time stamp comment is omited.</li>
           * <li>Messages are stored sorted alphabetically by key</li>
           * </ul>
           * @param bw writer to write to
           * @param escUnicode flag, indicating if unicode characters should be escaped
           * @throws IOException
           */
          @SuppressWarnings("javadoc")
          private void store0(BufferedWriter bw, boolean escUnicode) throws IOException {
  
              synchronized (this) {
                  List<Object> keys = new ArrayList<Object>(super.keySet());
                  Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
                  for (Object k : keys) {
                      String key = (String)k;
                      String val = (String)get(key);
                      key = saveConvert(key, true, escUnicode);
                      /* No need to escape embedded and trailing spaces for value, hence
                       * pass false to flag.
                       */
                      val = saveConvert(val, false, escUnicode);
                      bw.write(key + "=" + val);
                      bw.newLine();
                  }
              }
              bw.flush();
          }
      }
  
      /** The result of a key change. */
      enum KeyChangeResult {
          /** Key change was successful. */
          SUCCESS,
          /** Key change failed, because the new key already exists. */
          FAILED_DUPLICATED_KEY,
          /** Key change failed, because the key could not be changed for one or more languages. */
          FAILED_FOR_OTHER_LANGUAGE
      }
  
      /** The log object for this class. */
      private static final Log LOG = CmsLog.getLog(CmsMessageBundleEditorModel.class);
  
      /** The property for configuring the message bundle used for localizing the bundle descriptors entries. */
      public static final String PROPERTY_BUNDLE_DESCRIPTOR_LOCALIZATION = "bundle.descriptor.messages";
  
      /** CmsObject for read / write operations. */
      private CmsObject m_cms;
      /** The files currently edited. */
      private Map<Locale, LockedFile> m_lockedBundleFiles;
      /** The files of the bundle. */
      private Map<Locale, CmsResource> m_bundleFiles;
      /** The resource that was opened with the editor. */
      private CmsResource m_resource;
      /** The bundle descriptor resource. */
      private CmsResource m_desc;
      /** The bundle descriptor as unmarshalled XML Content. */
      private CmsXmlContent m_descContent;
      /** The xml bundle edited (or null, if a property bundle is edited). */
      private CmsXmlContent m_xmlBundle;
      /** The already loaded localizations. */
      private Map<Locale, SortedProperties> m_localizations;
      /** The bundle's base name. */
      private String m_basename;
      /** The site path to the folder where the edited resource is in. */
      private String m_sitepath;
      /** The currently edited locale. */
      private Locale m_locale;
      /** The type of the loaded bundle. */
      private CmsMessageBundleEditorTypes.BundleType m_bundleType;
  
      /** The complete key set as map from keys to the number of occurrences. */
      CmsMessageBundleEditorTypes.KeySet m_keyset;
  
      /** Containers holding the keys for each locale. */
      private IndexedContainer m_container;
      /** The available locales. */
      private Collection<Locale> m_locales;
      /** Map from edit mode to the editor state. */
      private Map<CmsMessageBundleEditorTypes.EditMode, EditorState> m_editorState;
      /** Flag, indicating if a master edit mode is available. */
      private boolean m_hasMasterMode;
      /** The current edit mode. */
      private CmsMessageBundleEditorTypes.EditMode m_editMode;
  
      /** Descriptor file, if edited besides a bundle. */
      private LockedFile m_descFile;
  
      /** The configured resource bundle used for the column headings of the bundle descriptor. */
      private String m_configuredBundle;
  
      /** Flag, indicating if the locale of the bundle that is edited has switched on opening. */
      private boolean m_switchedLocaleOnOpening;
  
      /** Flag, indicating if at least one default value is present in the current descriptor. */
      private boolean m_hasDefault;
  
      /** Flag, indicating if at least one description is present in the current descriptor. */
      private boolean m_hasDescription;
  
      /** Flag, indicating if the descriptor should be removed when editing is cancelled. */
      private boolean m_removeDescriptorOnCancel;
  
      /** Flag, indicating if something changed. */
      Set<Locale> m_changedTranslations;
  
      /** Flag, indicating if something changed. */
      boolean m_descriptorHasChanges;
  
      /** Flag, indicating if all localizations have already been loaded. */
      private boolean m_alreadyLoadedAllLocalizations;
  
      /**
       *
       * @param cms the {@link CmsObject} used for reading / writing.
       * @param resource the file that is opened for editing.
       * @throws CmsException thrown if reading some of the involved {@link CmsResource}s is not possible.
       * @throws IOException initialization of a property bundle fails
       */
      public CmsMessageBundleEditorModel(CmsObject cms, CmsResource resource)
      throws CmsException, IOException {
  
          if (cms == null) {
              throw new CmsException(Messages.get().container(Messages.ERR_LOADING_BUNDLE_CMS_OBJECT_NULL_0));
          }
  
          if (resource == null) {
              throw new CmsException(Messages.get().container(Messages.ERR_LOADING_BUNDLE_FILENAME_NULL_0));
          }
  
          m_cms = cms;
          m_resource = resource;
          m_editMode = CmsMessageBundleEditorTypes.EditMode.DEFAULT;
  
          m_bundleFiles = new HashMap<Locale, CmsResource>();
          m_lockedBundleFiles = new HashMap<Locale, LockedFile>();
          m_changedTranslations = new HashSet<Locale>();
          m_localizations = new HashMap<Locale, SortedProperties>();
          m_keyset = new CmsMessageBundleEditorTypes.KeySet();
  
          m_bundleType = initBundleType();
  
          m_locales = initLocales();
  
          //IMPORTANT: The order of the following method calls is important.
  
          if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.XML)) {
              initXmlBundle();
          }
  
          setResourceInformation();
  
          initDescriptor();
  
          if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.PROPERTY)) {
              initPropertyBundle();
          }
  
          initHasMasterMode();
  
          initEditorStates();
  
      }
  
      /**
       * Creates a descriptor for the currently edited message bundle.
       * @return <code>true</code> if the descriptor could be created, <code>false</code> otherwise.
       */
      public boolean addDescriptor() {
  
          saveLocalization();
          IndexedContainer oldContainer = m_container;
          try {
              createAndLockDescriptorFile();
              unmarshalDescriptor();
              updateBundleDescriptorContent();
              m_hasMasterMode = true;
              m_container = createContainer();
              m_editorState.put(EditMode.DEFAULT, getDefaultState());
              m_editorState.put(EditMode.MASTER, getMasterState());
          } catch (CmsException | IOException e) {
              LOG.error(e.getLocalizedMessage(), e);
              if (m_descContent != null) {
                  m_descContent = null;
              }
              if (m_descFile != null) {
                  m_descFile = null;
              }
              if (m_desc != null) {
                  try {
                      m_cms.deleteResource(m_desc, CmsResourceDeleteMode.valueOf(1));
                  } catch (CmsException ex) {
                      LOG.error(ex.getLocalizedMessage(), ex);
                  }
                  m_desc = null;
              }
              m_hasMasterMode = false;
              m_container = oldContainer;
              return false;
          }
          m_removeDescriptorOnCancel = true;
          return true;
      }
  
      /**
       * Returns a flag, indicating if keys can be added in the current edit mode.
       * @return a flag, indicating if keys can be added in the current edit mode.
       */
      public boolean canAddKeys() {
  
          return !hasDescriptor() || getEditMode().equals(EditMode.MASTER);
      }
  
      /**
       * When the descriptor was added while editing, but the change was not saved, it has to be removed
       * when the editor is closed.
       * @throws CmsException thrown when deleting the descriptor resource fails
       */
      public void deleteDescriptorIfNecessary() throws CmsException {
  
          if (m_removeDescriptorOnCancel && (m_desc != null)) {
              m_cms.deleteResource(m_desc, CmsResourceDeleteMode.valueOf(2));
          }
  
      }
  
      /** Returns the a set with all keys that are used at least in one translation.
       * @return the a set with all keys that are used at least in one translation.
       */
      public Set<Object> getAllUsedKeys() {
  
          return m_keyset.getKeySet();
      }
  
      /** Returns the type of the currently edited bundle.
       * @return the type of the currently edited bundle.
       */
      public BundleType getBundleType() {
  
          return m_bundleType;
      }
  
      /**
       * Returns the configured bundle, or the provided default bundle.
       * @param defaultMessages the default bundle
       * @param locale the preferred locale
       * @return the configured bundle or, if not found, the default bundle.
       */
      public ConfigurableMessages getConfigurableMessages(CmsMessages defaultMessages, Locale locale) {
  
          return new ConfigurableMessages(defaultMessages, locale, m_configuredBundle);
  
      }
  
      /**
       * Returns the container filled according to the current locale.
       * @return the container filled according to the current locale.
       * @throws IOException thrown if reading a bundle resource fails.
       * @throws CmsException thrown if reading a bundle resource fails.
       */
      public IndexedContainer getContainerForCurrentLocale() throws IOException, CmsException {
  
          if (null == m_container) {
              m_container = createContainer();
          }
          return m_container;
      }
  
      /**
       * Returns the context menu for the table item.
       * @param itemId the table item.
       * @return the context menu for the given item.
       */
      public CmsContextMenu getContextMenuForItem(Object itemId) {
  
          CmsContextMenu result = null;
          try {
              final Item item = m_container.getItem(itemId);
              Property<?> keyProp = item.getItemProperty(TableProperty.KEY);
              String key = (String)keyProp.getValue();
              if ((null != key) && !key.isEmpty()) {
                  loadAllRemainingLocalizations();
                  final Map<Locale, String> localesWithEntries = new HashMap<Locale, String>();
                  for (Locale l : m_localizations.keySet()) {
                      if (l != m_locale) {
                          String value = m_localizations.get(l).getProperty(key);
                          if ((null != value) && !value.isEmpty()) {
                              localesWithEntries.put(l, value);
                          }
                      }
                  }
                  if (!localesWithEntries.isEmpty()) {
                      result = new CmsContextMenu();
                      ContextMenuItem mainItem = result.addItem(
                          Messages.get().getBundle(UI.getCurrent().getLocale()).key(
                              Messages.GUI_BUNDLE_EDITOR_CONTEXT_COPY_LOCALE_0));
                      for (final Locale l : localesWithEntries.keySet()) {
  
                          ContextMenuItem menuItem = mainItem.addItem(l.getDisplayName(UI.getCurrent().getLocale()));
                          menuItem.addItemClickListener(new ContextMenuItemClickListener() {
  
                              public void contextMenuItemClicked(ContextMenuItemClickEvent event) {
  
                                  item.getItemProperty(TableProperty.TRANSLATION).setValue(localesWithEntries.get(l));
  
                              }
                          });
                      }
                  }
              }
          } catch (Exception e) {
              LOG.error(e.getLocalizedMessage(), e);
              //TODO: Improve
          }
          return result;
      }
  
      /**
       * Returns the editable columns for the current edit mode.
       * @return the editable columns for the current edit mode.
       */
      public List<TableProperty> getEditableColumns() {
  
          return m_editorState.get(m_editMode).getEditableColumns();
      }
  
      /**
       * Returns the editable columns for the provided edit mode.
       * @param mode the edit mode.
       * @return the editable columns for the provided edit mode.
       */
      public List<TableProperty> getEditableColumns(CmsMessageBundleEditorTypes.EditMode mode) {
  
          return m_editorState.get(mode).getEditableColumns();
      }
  
      /**
       * Returns the site path for the edited bundle file.
       *
       * @return the site path for the edited bundle file.
       */
      public String getEditedFilePath() {
  
          switch (getBundleType()) {
              case DESCRIPTOR:
                  return m_cms.getSitePath(m_desc);
              case PROPERTY:
                  return null != m_lockedBundleFiles.get(getLocale())
                  ? m_cms.getSitePath(m_lockedBundleFiles.get(getLocale()).getFile())
                  : m_cms.getSitePath(m_resource);
              case XML:
                  return m_cms.getSitePath(m_resource);
              default:
                  throw new IllegalArgumentException();
          }
      }
  
      /** Returns the current edit mode.
       * @return the current edit mode.
       */
      public CmsMessageBundleEditorTypes.EditMode getEditMode() {
  
          return m_editMode;
      }
  
      /**
       * Returns the currently edited locale.
       *
       * @return the currently edited locale.
       */
      public Locale getLocale() {
  
          return m_locale;
      }
  
      /**
       * Returns the locales available for the specific resource.
       *
       * @return the locales available for the specific resource.
       */
      public Collection<Locale> getLocales() {
  
          return m_locales;
      }
  
      /**
       * Returns a flag, indicating if the locale has been switched on opening.
       * @return a flag, indicating if the locale has been switched on opening.
       */
      public boolean getSwitchedLocaleOnOpening() {
  
          return m_switchedLocaleOnOpening;
      }
  
      /**
       * Handles the change of a value in the current translation.
       * @param propertyId the property id of the column where the value has changed.
       */
      public void handleChange(Object propertyId) {
  
          try {
              lockOnChange(propertyId);
          } catch (CmsException e) {
              LOG.debug(e.getLocalizedMessage(), e);
          }
          if (isDescriptorProperty(propertyId)) {
              m_descriptorHasChanges = true;
          }
          if (isBundleProperty(propertyId)) {
              m_changedTranslations.add(getLocale());
          }
  
      }
  
      /**
       * Handles a key change.
       *
       * @param event the key change event.
       * @param allLanguages <code>true</code> for changing the key for all languages, <code>false</code> if the key should be changed only for the current language.
       * @return result, indicating if the key change was successful.
       */
      public KeyChangeResult handleKeyChange(EntryChangeEvent event, boolean allLanguages) {
  
          if (m_keyset.getKeySet().contains(event.getNewValue())) {
              m_container.getItem(event.getItemId()).getItemProperty(TableProperty.KEY).setValue(event.getOldValue());
              return KeyChangeResult.FAILED_DUPLICATED_KEY;
          }
          if (allLanguages && !renameKeyForAllLanguages(event.getOldValue(), event.getNewValue())) {
              m_container.getItem(event.getItemId()).getItemProperty(TableProperty.KEY).setValue(event.getOldValue());
              return KeyChangeResult.FAILED_FOR_OTHER_LANGUAGE;
          }
          return KeyChangeResult.SUCCESS;
      }
  
      /**
       * Handles the deletion of a key.
       * @param key the deleted key.
       * @return <code>true</code> if the deletion was successful, <code>false</code> otherwise.
       */
      public boolean handleKeyDeletion(final String key) {
  
          if (m_keyset.getKeySet().contains(key)) {
              if (removeKeyForAllLanguages(key)) {
                  m_keyset.removeKey(key);
                  return true;
              } else {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Returns a flag, indicating if something was edited.
       * @return a flag, indicating if something was edited.
       */
      public boolean hasChanges() {
  
          return !m_changedTranslations.isEmpty() || m_descriptorHasChanges;
      }
  
      /**
       * Returns a flag, indicating if the descriptor specifies any default values.
       * @return flag, indicating if the descriptor specifies any default values.
       */
      public boolean hasDefaultValues() {
  
          return m_hasDefault;
      }
  
      /**
       * Returns a flag, indicating if the descriptor specifies any descriptions.
       * @return a flag, indicating if the descriptor specifies any descriptions.
       */
      public boolean hasDescriptionValues() {
  
          return m_hasDescription;
      }
  
      /** Returns a flag, indicating if a bundle descriptor is present.
       * @return flag, indicating if a bundle descriptor is present.
       */
      public boolean hasDescriptor() {
  
          return !m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR) && (m_descContent != null);
      }
  
      /**
       * Returns a flag, indicating if a master edit mode is available.
       * @return a flag, indicating if a master edit mode is available.
       */
      public boolean hasMasterMode() {
  
          return m_hasMasterMode;
      }
  
      /**
       * Returns a flag, indicating if the options column (with add and delete option for rows)
       * should be shown in the given edit mode.
       * @param mode the edit mode for which the column option is requested.
       * @return a flag, indicating if the options column (with add and delete option for rows)
       */
      public boolean isShowOptionsColumn(CmsMessageBundleEditorTypes.EditMode mode) {
  
          return m_editorState.get(mode).isShowOptions();
      }
  
      /**
       * Publish the bundle resources directly.
       */
      public void publish() {
  
          CmsDirectPublishDialogAction action = new CmsDirectPublishDialogAction();
          List<CmsResource> resources = getBundleResources();
          I_CmsDialogContext context = new A_CmsDialogContext("", ContextType.appToolbar, resources) {
  
              public void focus(CmsUUID structureId) {
  
                  //Nothing to do.
              }
  
              public List<CmsUUID> getAllStructureIdsInView() {
  
                  return null;
              }
  
              public void updateUserInfo() {
  
                  //Nothing to do.
              }
          };
          action.executeAction(context);
          updateLockInformation();
  
      }
  
      /**
       * Saves the messages for all languages that were opened in the editor.
       *
       * @throws CmsException thrown if saving fails.
       */
      public void save() throws CmsException {
  
          if (hasChanges()) {
              switch (m_bundleType) {
                  case PROPERTY:
                      saveLocalization();
                      saveToPropertyVfsBundle();
                      break;
  
                  case XML:
                      saveLocalization();
                      saveToXmlVfsBundle();
  
                      break;
  
                  case DESCRIPTOR:
                      break;
                  default:
                      throw new IllegalArgumentException();
              }
              if (null != m_descFile) {
                  saveToBundleDescriptor();
              }
  
              resetChanges();
          }
  
      }
  
      /**
       * Saves the loaded XML bundle as property bundle.
       * @throws UnsupportedEncodingException thrown if localizations from the XML bundle could not be loaded correctly.
       * @throws CmsException thrown if any of the interactions with the VFS fails.
       * @throws IOException thrown if localizations from the XML bundle could not be loaded correctly.
       */
      public void saveAsPropertyBundle() throws UnsupportedEncodingException, CmsException, IOException {
  
          switch (m_bundleType) {
              case XML:
                  saveLocalization();
                  loadAllRemainingLocalizations();
                  createPropertyVfsBundleFiles();
                  saveToPropertyVfsBundle();
                  m_bundleType = BundleType.PROPERTY;
                  removeXmlBundleFile();
  
                  break;
              default:
                  throw new IllegalArgumentException(
                      "The method should only be called when editing an XMLResourceBundle.");
          }
  
      }
  
      /**
       * Set the edit mode.
       * @param mode the edit mode to set.
       * @return flag, indicating if the mode could be changed.
       */
      public boolean setEditMode(CmsMessageBundleEditorTypes.EditMode mode) {
  
          try {
              if ((mode == CmsMessageBundleEditorTypes.EditMode.MASTER) && (null == m_descFile)) {
                  m_descFile = LockedFile.lockResource(m_cms, m_desc);
              }
              m_editMode = mode;
          } catch (CmsException e) {
              return false;
          }
          return true;
      }
  
      /**
       * Set the currently edited locale.
       *
       * @param locale the currently edited locale.
       * @return <code>true</code> if the locale could be set, <code>false</code> otherwise.
       */
      public boolean setLocale(Locale locale) {
  
          if (adjustExistingContainer(locale)) {
              m_locale = locale;
              return true;
          }
          return false;
      }
  
      /**
       * Unlock all files opened for writing.
       */
      public void unlock() {
  
          for (Locale l : m_lockedBundleFiles.keySet()) {
              LockedFile f = m_lockedBundleFiles.get(l);
              f.tryUnlock();
          }
          if (null != m_descFile) {
              m_descFile.tryUnlock();
          }
      }
  
      /**
       * Returns all resources that belong to the bundle
       * This includes the descriptor if one exists.
       *
       * @return List of the bundle resources, including the descriptor.
       */
      List<CmsResource> getBundleResources() {
  
          List<CmsResource> resources = new ArrayList<>(m_bundleFiles.values());
          if (m_desc != null) {
              resources.add(m_desc);
          }
          return resources;
  
      }
  
      /**
       * Adjusts the locale for an already existing container by first saving the translation for the current locale and the loading the values of the new locale.
       *
       * @param locale the locale for which the container should be adjusted.
       * @return <code>true</code> if the locale could be switched, <code>false</code> otherwise.
       */
      private boolean adjustExistingContainer(Locale locale) {
  
          saveLocalization();
          return replaceValues(locale);
  
      }
  
      /**
       * Creates a descriptor for the bundle in the same folder where the bundle files are located.
       * @throws CmsException thrown if creation fails.
       */
      private void createAndLockDescriptorFile() throws CmsException {
  
          String sitePath = m_sitepath + m_basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX;
          m_desc = m_cms.createResource(
              sitePath,
              OpenCms.getResourceManager().getResourceType(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR.toString()));
          m_descFile = LockedFile.lockResource(m_cms, m_desc);
          m_descFile.setCreated(true);
      }
  
      /**
       * Initializes the IndexedContainer shown in the table for the current locale.
       * Therefore, the involved {@link CmsResource}s will be read, if not already done.
       * @return the created container
       * @throws IOException thrown if reading of an involved file fails.
       * @throws CmsException thrown if reading of an involved file fails.
       */
      private IndexedContainer createContainer() throws IOException, CmsException {
  
          IndexedContainer container = null;
  
          if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR)) {
              container = createContainerForDescriptorEditing();
          } else {
              if (hasDescriptor()) {
                  container = createContainerForBundleWithDescriptor();
              } else {
                  container = createContainerForBundleWithoutDescriptor();
              }
          }
          container.setItemSorter(new DefaultItemSorter(CmsCaseInsensitiveStringComparator.getInstance()));
          return container;
      }
  
      /**
       * Creates the container for a bundle with descriptor.
       * @return the container for a bundle with descriptor.
       * @throws IOException thrown if reading the bundle fails.
       * @throws CmsException thrown if reading the bundle fails.
       */
      private IndexedContainer createContainerForBundleWithDescriptor() throws IOException, CmsException {
  
          IndexedContainer container = new IndexedContainer();
  
          // create properties
          container.addContainerProperty(TableProperty.KEY, String.class, "");
          container.addContainerProperty(TableProperty.DESCRIPTION, String.class, "");
          container.addContainerProperty(TableProperty.DEFAULT, String.class, "");
          container.addContainerProperty(TableProperty.TRANSLATION, String.class, "");
  
          // add entries
          SortedProperties localization = getLocalization(m_locale);
          CmsXmlContentValueSequence messages = m_descContent.getValueSequence(Descriptor.N_MESSAGE, Descriptor.LOCALE);
          String descValue;
          boolean hasDescription = false;
          String defaultValue;
          boolean hasDefault = false;
          for (int i = 0; i < messages.getElementCount(); i++) {
  
              String prefix = messages.getValue(i).getPath() + "/";
              Object itemId = container.addItem();
              Item item = container.getItem(itemId);
              String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
              item.getItemProperty(TableProperty.KEY).setValue(key);
              String translation = localization.getProperty(key);
              item.getItemProperty(TableProperty.TRANSLATION).setValue(null == translation ? "" : translation);
              descValue = m_descContent.getValue(prefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).getStringValue(
                  m_cms);
              item.getItemProperty(TableProperty.DESCRIPTION).setValue(descValue);
              hasDescription = hasDescription || !descValue.isEmpty();
              defaultValue = m_descContent.getValue(prefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).getStringValue(
                  m_cms);
              item.getItemProperty(TableProperty.DEFAULT).setValue(defaultValue);
              hasDefault = hasDefault || !defaultValue.isEmpty();
          }
  
          m_hasDefault = hasDefault;
          m_hasDescription = hasDescription;
          return container;
  
      }
  
      /**
       * Creates the container for a bundle without descriptor.
       * @return the container for a bundle without descriptor.
       * @throws IOException thrown if reading the bundle fails.
       * @throws CmsException thrown if reading the bundle fails.
       */
      private IndexedContainer createContainerForBundleWithoutDescriptor() throws IOException, CmsException {
  
          IndexedContainer container = new IndexedContainer();
  
          // create properties
          container.addContainerProperty(TableProperty.KEY, String.class, "");
          container.addContainerProperty(TableProperty.TRANSLATION, String.class, "");
  
          // add entries
          SortedProperties localization = getLocalization(m_locale);
          Set<Object> keySet = m_keyset.getKeySet();
          for (Object key : keySet) {
  
              Object itemId = container.addItem();
              Item item = container.getItem(itemId);
              item.getItemProperty(TableProperty.KEY).setValue(key);
              Object translation = localization.get(key);
              item.getItemProperty(TableProperty.TRANSLATION).setValue(null == translation ? "" : translation);
          }
  
          return container;
      }
  
      /**
       * Creates the container for a bundle descriptor.
       * @return the container for a bundle descriptor.
       */
      private IndexedContainer createContainerForDescriptorEditing() {
  
          IndexedContainer container = new IndexedContainer();
  
          // create properties
          container.addContainerProperty(TableProperty.KEY, String.class, "");
          container.addContainerProperty(TableProperty.DESCRIPTION, String.class, "");
          container.addContainerProperty(TableProperty.DEFAULT, String.class, "");
  
          // add entries
          CmsXmlContentValueSequence messages = m_descContent.getValueSequence(
              "/" + Descriptor.N_MESSAGE,
              Descriptor.LOCALE);
          for (int i = 0; i < messages.getElementCount(); i++) {
  
              String prefix = messages.getValue(i).getPath() + "/";
              Object itemId = container.addItem();
              Item item = container.getItem(itemId);
              String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
              item.getItemProperty(TableProperty.KEY).setValue(key);
              item.getItemProperty(TableProperty.DESCRIPTION).setValue(
                  m_descContent.getValue(prefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).getStringValue(m_cms));
              item.getItemProperty(TableProperty.DEFAULT).setValue(
                  m_descContent.getValue(prefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).getStringValue(m_cms));
          }
  
          return container;
  
      }
  
      /**
       * Creates all propertyvfsbundle files for the currently loaded translations.
       * The method is used to convert xmlvfsbundle files into propertyvfsbundle files.
       *
       * @throws CmsIllegalArgumentException thrown if resource creation fails.
       * @throws CmsLoaderException thrown if the propertyvfsbundle type can't be read from the resource manager.
       * @throws CmsException thrown if creation, type retrieval or locking fails.
       */
      private void createPropertyVfsBundleFiles() throws CmsIllegalArgumentException, CmsLoaderException, CmsException {
  
          String bundleFileBasePath = m_sitepath + m_basename + "_";
          for (Locale l : m_localizations.keySet()) {
              CmsResource res = m_cms.createResource(
                  bundleFileBasePath + l.toString(),
                  OpenCms.getResourceManager().getResourceType(
                      CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString()));
              m_bundleFiles.put(l, res);
              LockedFile file = LockedFile.lockResource(m_cms, res);
              file.setCreated(true);
              m_lockedBundleFiles.put(l, file);
              m_changedTranslations.add(l);
          }
  
      }
  
      /**
       * Creates the default editor state for editing a bundle with descriptor.
       * @return the default editor state for editing a bundle with descriptor.
       */
      private EditorState getDefaultState() {
  
          List<TableProperty> cols = new ArrayList<TableProperty>(1);
          cols.add(TableProperty.TRANSLATION);
  
          return new EditorState(cols, false);
      }
  
      /**
       * Returns a map with key property as key and item as value.<p>
       *
       * @return HashMap
       */
      private Map<String, Item> getKeyItemMap() {
  
          Map<String, Item> ret = new HashMap<String, Item>();
          for (Object itemId : m_container.getItemIds()) {
              ret.put(
                  m_container.getItem(itemId).getItemProperty(TableProperty.KEY).getValue().toString(),
                  m_container.getItem(itemId));
          }
          return ret;
  
      }
  
      /**
       * Reads the current properties for a language. If not already done, the properties are read from the respective file.
       * @param locale the locale for which the localization should be returned.
       * @return the properties.
       * @throws IOException thrown if reading the properties from a file fails.
       * @throws CmsException thrown if reading the properties from a file fails.
       */
      private SortedProperties getLocalization(Locale locale) throws IOException, CmsException {
  
          if (null == m_localizations.get(locale)) {
              switch (m_bundleType) {
                  case PROPERTY:
                      loadLocalizationFromPropertyBundle(locale);
                      break;
                  case XML:
                      loadLocalizationFromXmlBundle(locale);
                      break;
                  case DESCRIPTOR:
                      return null;
                  default:
                      break;
              }
          }
          return m_localizations.get(locale);
      }
  
      /**
       * Returns the master mode's editor state for editing a bundle with descriptor.
       * @return the master mode's editor state for editing a bundle with descriptor.
       */
      private EditorState getMasterState() {
  
          List<TableProperty> cols = new ArrayList<TableProperty>(4);
          cols.add(TableProperty.KEY);
          cols.add(TableProperty.DESCRIPTION);
          cols.add(TableProperty.DEFAULT);
          cols.add(TableProperty.TRANSLATION);
          return new EditorState(cols, true);
      }
  
      /**
       * Init the bundle type member variable.
       * @return the bundle type of the opened resource.
       */
      private CmsMessageBundleEditorTypes.BundleType initBundleType() {
  
          String resourceTypeName = OpenCms.getResourceManager().getResourceType(m_resource).getTypeName();
          return CmsMessageBundleEditorTypes.BundleType.toBundleType(resourceTypeName);
      }
  
      /**
       * Reads the bundle descriptor, sets m_desc and m_descContent.
       * @throws CmsXmlException thrown when unmarshalling fails.
       * @throws CmsException thrown when reading the resource fails or several bundle descriptors for the bundle exist.
       */
      private void initDescriptor() throws CmsXmlException, CmsException {
  
          if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR)) {
              m_desc = m_resource;
          } else {
              //First try to read from same folder like resource, if it fails use CmsMessageBundleEditorTypes.getDescriptor()
              try {
                  m_desc = m_cms.readResource(m_sitepath + m_basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX);
              } catch (CmsVfsResourceNotFoundException e) {
                  m_desc = CmsMessageBundleEditorTypes.getDescriptor(m_cms, m_basename);
              }
          }
          unmarshalDescriptor();
  
      }
  
      /**
       * Initializes the editor states for the different modes, depending on the type of the opened file.
       */
      private void initEditorStates() {
  
          m_editorState = new HashMap<CmsMessageBundleEditorTypes.EditMode, EditorState>();
          List<TableProperty> cols = null;
          switch (m_bundleType) {
              case PROPERTY:
              case XML:
                  if (hasDescriptor()) { // bundle descriptor is present, keys are not editable in default mode, maybe master mode is available
                      m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, getDefaultState());
                      if (hasMasterMode()) { // the bundle descriptor is editable
                          m_editorState.put(CmsMessageBundleEditorTypes.EditMode.MASTER, getMasterState());
                      }
                  } else { // no bundle descriptor given - implies no master mode
                      cols = new ArrayList<TableProperty>(1);
                      cols.add(TableProperty.KEY);
                      cols.add(TableProperty.TRANSLATION);
                      m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, new EditorState(cols, true));
                  }
                  break;
              case DESCRIPTOR:
                  cols = new ArrayList<TableProperty>(3);
                  cols.add(TableProperty.KEY);
                  cols.add(TableProperty.DESCRIPTION);
                  cols.add(TableProperty.DEFAULT);
                  m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, new EditorState(cols, true));
                  break;
              default:
                  throw new IllegalArgumentException();
          }
  
      }
  
      /**
       * Initializes the information on an available master mode.
       * @throws CmsException thrown if the write permission check on the bundle descriptor fails.
       */
      private void initHasMasterMode() throws CmsException {
  
          if (hasDescriptor()
              && m_cms.hasPermissions(m_desc, CmsPermissionSet.ACCESS_WRITE, false, CmsResourceFilter.ALL)) {
              m_hasMasterMode = true;
          } else {
              m_hasMasterMode = false;
          }
      }
  
      /**
       * Initialize the key set for an xml bundle.
       */
      private void initKeySetForXmlBundle() {
  
          // consider only available locales
          for (Locale l : m_locales) {
              if (m_xmlBundle.hasLocale(l)) {
                  Set<Object> keys = new HashSet<Object>();
                  for (I_CmsXmlContentValue msg : m_xmlBundle.getValueSequence("Message", l).getValues()) {
                      String msgpath = msg.getPath();
                      keys.add(m_xmlBundle.getStringValue(m_cms, msgpath + "/Key", l));
                  }
                  m_keyset.updateKeySet(null, keys);
              }
          }
  
      }
  
      /**
       * Initializes the locales that can be selected via the language switcher in the bundle editor.
       * @return the locales for which keys can be edited.
       */
      private Collection<Locale> initLocales() {
  
          Collection<Locale> locales = null;
          switch (m_bundleType) {
              case DESCRIPTOR:
                  locales = new ArrayList<Locale>(1);
                  locales.add(Descriptor.LOCALE);
                  break;
              case XML:
              case PROPERTY:
                  locales = OpenCms.getLocaleManager().getAvailableLocales(m_cms, m_resource);
                  break;
              default:
                  throw new IllegalArgumentException();
          }
          return locales;
  
      }
  
      /**
       * Initialization necessary for editing a property bundle.
       *
       * @throws CmsLoaderException thrown if loading a bundle file fails.
       * @throws CmsException thrown if loading a bundle file fails.
       * @throws IOException thrown if loading a bundle file fails.
       */
      private void initPropertyBundle() throws CmsLoaderException, CmsException, IOException {
  
          for (Locale l : m_locales) {
              String filePath = m_sitepath + m_basename + "_" + l.toString();
              CmsResource resource = null;
              if (m_cms.existsResource(
                  filePath,
                  CmsResourceFilter.requireType(
                      OpenCms.getResourceManager().getResourceType(BundleType.PROPERTY.toString())))) {
                  resource = m_cms.readResource(filePath);
                  SortedProperties props = new SortedProperties();
                  CmsFile file = m_cms.readFile(resource);
                  props.load(
                      new InputStreamReader(
                          new ByteArrayInputStream(file.getContents()),
                          CmsFileUtil.getEncoding(m_cms, file)));
                  m_keyset.updateKeySet(null, props.keySet());
                  m_bundleFiles.put(l, resource);
              }
          }
  
      }
  
      /**
       * Unmarshals the XML content and adds the file to the bundle files.
       * @throws CmsException thrown if reading the file or unmarshaling fails.
       */
      private void initXmlBundle() throws CmsException {
  
          CmsFile file = m_cms.readFile(m_resource);
          m_bundleFiles.put(null, m_resource);
          m_xmlBundle = CmsXmlContentFactory.unmarshal(m_cms, file);
          initKeySetForXmlBundle();
  
      }
  
      /**
       * Check if values in the column "property" are written to the bundle files.
       * @param property the property id of the table column.
       * @return a flag, indicating if values of the table column are stored to the bundle files.
       */
      private boolean isBundleProperty(Object property) {
  
          return (property.equals(TableProperty.KEY) || property.equals(TableProperty.TRANSLATION));
      }
  
      /**
       * Check if values in the column "property" are written to the bundle descriptor.
       * @param property the property id of the table column.
       * @return a flag, indicating if values of the table column are stored to the bundle descriptor.
       */
      private boolean isDescriptorProperty(Object property) {
  
          return (getBundleType().equals(BundleType.DESCRIPTOR)
              || (hasDescriptor()
                  && (property.equals(TableProperty.KEY)
                      || property.equals(TableProperty.DEFAULT)
                      || property.equals(TableProperty.DESCRIPTION))));
      }
  
      /**
       * Loads all localizations not already loaded.
       * @throws CmsException thrown if locking a file fails.
       * @throws UnsupportedEncodingException thrown if reading  a file fails.
       * @throws IOException thrown if reading  a file fails.
       */
      private void loadAllRemainingLocalizations() throws CmsException, UnsupportedEncodingException, IOException {
  
          if (!m_alreadyLoadedAllLocalizations) {
              // is only necessary for property bundles
              if (m_bundleType.equals(BundleType.PROPERTY)) {
                  for (Locale l : m_locales) {
                      if (null == m_localizations.get(l)) {
                          CmsResource resource = m_bundleFiles.get(l);
                          if (resource != null) {
                              CmsFile file = m_cms.readFile(resource);
                              m_bundleFiles.put(l, file);
                              SortedProperties props = new SortedProperties();
                              props.load(
                                  new InputStreamReader(
                                      new ByteArrayInputStream(file.getContents()),
                                      CmsFileUtil.getEncoding(m_cms, file)));
                              m_localizations.put(l, props);
                          }
                      }
                  }
              }
              if (m_bundleType.equals(BundleType.XML)) {
                  for (Locale l : m_locales) {
                      if (null == m_localizations.get(l)) {
                          loadLocalizationFromXmlBundle(l);
                      }
                  }
              }
              m_alreadyLoadedAllLocalizations = true;
          }
  
      }
  
      /**
       * Loads the propertyvfsbundle for the provided locale.
       * If the bundle file is not present, it will be created.
       * @param locale the locale for which the localization should be loaded
       *
       * @throws IOException thrown if loading fails.
       * @throws CmsException thrown if reading or creation fails.
       */
      private void loadLocalizationFromPropertyBundle(Locale locale) throws IOException, CmsException {
  
          // may throw exception again
          String sitePath = m_sitepath + m_basename + "_" + locale.toString();
          CmsResource resource = null;
          if (m_cms.existsResource(sitePath)) {
              resource = m_cms.readResource(sitePath);
              if (!OpenCms.getResourceManager().getResourceType(resource).getTypeName().equals(
                  CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString())) {
                  throw new CmsException(
                      new CmsMessageContainer(
                          Messages.get(),
                          Messages.ERR_RESOURCE_HAS_WRONG_TYPE_2,
                          locale.getDisplayName(),
                          resource.getRootPath()));
              }
          } else {
              resource = m_cms.createResource(
                  sitePath,
                  OpenCms.getResourceManager().getResourceType(
                      CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString()));
              LockedFile lf = LockedFile.lockResource(m_cms, resource);
              lf.setCreated(true);
              m_lockedBundleFiles.put(locale, lf);
          }
          m_bundleFiles.put(locale, resource);
          SortedProperties props = new SortedProperties();
          props.load(
              new InputStreamReader(
                  new ByteArrayInputStream(m_cms.readFile(resource).getContents()),
                  CmsFileUtil.getEncoding(m_cms, resource)));
          m_localizations.put(locale, props);
  
      }
  
      /**
       * Loads the localization for the current locale from a bundle of type xmlvfsbundle.
       * It assumes, the content has already been unmarshalled before.
       * @param locale the locale for which the localization should be loaded
       */
      private void loadLocalizationFromXmlBundle(Locale locale) {
  
          CmsXmlContentValueSequence messages = m_xmlBundle.getValueSequence("Message", locale);
          SortedProperties props = new SortedProperties();
          if (null != messages) {
              for (I_CmsXmlContentValue msg : messages.getValues()) {
                  String msgpath = msg.getPath();
                  props.put(
                      m_xmlBundle.getStringValue(m_cms, msgpath + "/Key", locale),
                      m_xmlBundle.getStringValue(m_cms, msgpath + "/Value", locale));
              }
          }
          m_localizations.put(locale, props);
      }
  
      /**
       * Locks all files of the currently edited bundle (that contain the provided key if it is not null).
       * @param key the key that must be contained in the localization to lock. If null, the files for all localizations are locked.
       * @throws CmsException thrown if locking fails.
       */
      private void lockAllLocalizations(String key) throws CmsException {
  
          for (Locale l : m_bundleFiles.keySet()) {
              if ((null == l) || m_localizations.get(l).containsKey(key)) {
                  lockLocalization(l);
              }
          }
      }
  
      /**
       * Locks the bundle descriptor.
       * @throws CmsException thrown if locking fails.
       */
      private void lockDescriptor() throws CmsException {
  
          if ((null == m_descFile) && (null != m_desc)) {
              m_descFile = LockedFile.lockResource(m_cms, m_desc);
          }
      }
  
      /**
       * Locks the bundle file that contains the translation for the provided locale.
       * @param l the locale for which the bundle file should be locked.
       * @throws CmsException thrown if locking fails.
       */
      private void lockLocalization(Locale l) throws CmsException {
  
          if (null == m_lockedBundleFiles.get(l)) {
              LockedFile lf = LockedFile.lockResource(m_cms, m_bundleFiles.get(l));
              m_lockedBundleFiles.put(l, lf);
          }
  
      }
  
      /**
       * Lock a file lazily, if a value that should be written to the file has changed.
       * @param propertyId the table column in which the value has changed (e.g., KEY, TRANSLATION, ...)
       * @throws CmsException thrown if locking fails.
       */
      private void lockOnChange(Object propertyId) throws CmsException {
  
          if (isDescriptorProperty(propertyId)) {
              lockDescriptor();
          } else {
              Locale l = m_bundleType.equals(BundleType.PROPERTY) ? m_locale : null;
              lockLocalization(l);
          }
  
      }
  
      /**
       * Remove a key for all language versions. If a descriptor is present, the key is only removed in the descriptor.
       *
       * @param key the key to remove.
       * @return <code>true</code> if removing was successful, <code>false</code> otherwise.
       */
      private boolean removeKeyForAllLanguages(String key) {
  
          try {
              if (hasDescriptor()) {
                  lockDescriptor();
              }
              loadAllRemainingLocalizations();
              lockAllLocalizations(key);
          } catch (CmsException | IOException e) {
              LOG.warn("Not able lock all localications for bundle.", e);
              return false;
          }
          if (!hasDescriptor()) {
  
              for (Entry<Locale, SortedProperties> entry : m_localizations.entrySet()) {
                  SortedProperties localization = entry.getValue();
                  if (localization.containsKey(key)) {
                      localization.remove(key);
                      m_changedTranslations.add(entry.getKey());
                  }
              }
          }
          return true;
      }
  
      /**
       * Deletes the VFS XML bundle file.
       * @throws CmsException thrown if the delete operation fails.
       */
      private void removeXmlBundleFile() throws CmsException {
  
          lockLocalization(null);
          m_cms.deleteResource(m_resource, CmsResource.DELETE_PRESERVE_SIBLINGS);
          m_resource = null;
  
      }
  
      /**
       * Rename a key for all languages.
       * @param oldKey the key to rename
       * @param newKey the new key name
       * @return <code>true</code> if renaming was successful, <code>false</code> otherwise.
       */
      private boolean renameKeyForAllLanguages(String oldKey, String newKey) {
  
          try {
              loadAllRemainingLocalizations();
              lockAllLocalizations(oldKey);
              if (hasDescriptor()) {
                  lockDescriptor();
              }
          } catch (CmsException | IOException e) {
              LOG.error(e.getLocalizedMessage(), e);
              return false;
          }
          for (Entry<Locale, SortedProperties> entry : m_localizations.entrySet()) {
              SortedProperties localization = entry.getValue();
              if (localization.containsKey(oldKey)) {
                  String value = localization.getProperty(oldKey);
                  localization.remove(oldKey);
                  localization.put(newKey, value);
                  m_changedTranslations.add(entry.getKey());
              }
          }
          if (hasDescriptor()) {
              CmsXmlContentValueSequence messages = m_descContent.getValueSequence(
                  Descriptor.N_MESSAGE,
                  Descriptor.LOCALE);
              for (int i = 0; i < messages.getElementCount(); i++) {
  
                  String prefix = messages.getValue(i).getPath() + "/";
                  String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
                  if (key == oldKey) {
                      m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).setStringValue(m_cms, newKey);
                      break;
                  }
              }
              m_descriptorHasChanges = true;
          }
          m_keyset.renameKey(oldKey, newKey);
          return true;
      }
  
      /**
       * Replaces the translations in an existing container with the translations for the provided locale.
       * @param locale the locale for which translations should be loaded.
       * @return <code>true</code> if replacing succeeded, <code>false</code> otherwise.
       */
      private boolean replaceValues(Locale locale) {
  
          try {
              SortedProperties localization = getLocalization(locale);
              if (hasDescriptor()) {
                  for (Object itemId : m_container.getItemIds()) {
                      Item item = m_container.getItem(itemId);
                      String key = item.getItemProperty(TableProperty.KEY).getValue().toString();
                      Object value = localization.get(key);
                      item.getItemProperty(TableProperty.TRANSLATION).setValue(null == value ? "" : value);
                  }
              } else {
                  m_container.removeAllItems();
                  Set<Object> keyset = m_keyset.getKeySet();
                  for (Object key : keyset) {
                      Object itemId = m_container.addItem();
                      Item item = m_container.getItem(itemId);
                      item.getItemProperty(TableProperty.KEY).setValue(key);
                      Object value = localization.get(key);
                      item.getItemProperty(TableProperty.TRANSLATION).setValue(null == value ? "" : value);
                  }
                  if (m_container.getItemIds().isEmpty()) {
                      m_container.addItem();
                  }
              }
              return true;
          } catch (IOException | CmsException e) {
              // The problem should typically be a problem with locking or reading the file containing the translation.
              // This should be reported in the editor, if false is returned here.
              return false;
          }
      }
  
      /**
       * Resets all information on changes. I.e., the editor assumes no changes are present anymore.
       * Call this method after a successful save action.
       */
      private void resetChanges() {
  
          m_changedTranslations.clear();
          m_descriptorHasChanges = false;
  
      }
  
      /**
       * Saves the current translations from the container to the respective localization.
       */
      private void saveLocalization() {
  
          SortedProperties localization = new SortedProperties();
          for (Object itemId : m_container.getItemIds()) {
              Item item = m_container.getItem(itemId);
              String key = item.getItemProperty(TableProperty.KEY).getValue().toString();
              String value = item.getItemProperty(TableProperty.TRANSLATION).getValue().toString();
              if (!(key.isEmpty() || value.isEmpty())) {
                  localization.put(key, value);
              }
          }
          m_keyset.updateKeySet(m_localizations.get(m_locale).keySet(), localization.keySet());
          m_localizations.put(m_locale, localization);
  
      }
  
      /**
       * Save the values to the bundle descriptor.
       * @throws CmsException thrown if saving fails.
       */
      private void saveToBundleDescriptor() throws CmsException {
  
          if (null != m_descFile) {
              m_removeDescriptorOnCancel = false;
              updateBundleDescriptorContent();
              m_descFile.getFile().setContents(m_descContent.marshal());
              m_cms.writeFile(m_descFile.getFile());
          }
      }
  
      /**
       * Saves messages to a propertyvfsbundle file.
       *
       * @throws CmsException thrown if writing to the file fails.
       */
      private void saveToPropertyVfsBundle() throws CmsException {
  
          for (Locale l : m_changedTranslations) {
              SortedProperties props = m_localizations.get(l);
              LockedFile f = m_lockedBundleFiles.get(l);
              if ((null != props) && (null != f)) {
                  try {
                      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                      Writer writer = new OutputStreamWriter(outputStream, f.getEncoding());
                      props.store(writer, null);
                      byte[] contentBytes = outputStream.toByteArray();
                      CmsFile file = f.getFile();
                      file.setContents(contentBytes);
                      String contentEncodingProperty = m_cms.readPropertyObject(
                          file,
                          CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
                          false).getValue();
                      if ((null == contentEncodingProperty) || !contentEncodingProperty.equals(f.getEncoding())) {
                          m_cms.writePropertyObject(
                              m_cms.getSitePath(file),
                              new CmsProperty(
                                  CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
                                  f.getEncoding(),
                                  f.getEncoding()));
                      }
                      m_cms.writeFile(file);
                  } catch (IOException e) {
                      LOG.error(
                          Messages.get().getBundle().key(
                              Messages.ERR_READING_FILE_UNSUPPORTED_ENCODING_2,
                              f.getFile().getRootPath(),
                              f.getEncoding()),
                          e);
  
                  }
              }
          }
      }
  
      /**
       * Saves messages to a xmlvfsbundle file.
       *
       * @throws CmsException thrown if writing to the file fails.
       */
      private void saveToXmlVfsBundle() throws CmsException {
  
          if (m_lockedBundleFiles.get(null) != null) { // If the file was not locked, no changes were made, i.e., storing is not necessary.
              for (Locale l : m_locales) {
                  SortedProperties props = m_localizations.get(l);
                  if (null != props) {
                      if (m_xmlBundle.hasLocale(l)) {
                          m_xmlBundle.removeLocale(l);
                      }
                      m_xmlBundle.addLocale(m_cms, l);
                      int i = 0;
                      List<Object> keys = new ArrayList<Object>(props.keySet());
                      Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
                      for (Object key : keys) {
                          if ((null != key) && !key.toString().isEmpty()) {
                              String value = props.getProperty(key.toString());
                              if (!value.isEmpty()) {
                                  m_xmlBundle.addValue(m_cms, "Message", l, i);
                                  i++;
                                  m_xmlBundle.getValue("Message[" + i + "]/Key", l).setStringValue(m_cms, key.toString());
                                  m_xmlBundle.getValue("Message[" + i + "]/Value", l).setStringValue(m_cms, value);
                              }
                          }
                      }
                  }
                  CmsFile bundleFile = m_lockedBundleFiles.get(null).getFile();
                  bundleFile.setContents(m_xmlBundle.marshal());
                  m_cms.writeFile(bundleFile);
              }
          }
      }
  
      /** Extract site path, base name and locale from the resource opened with the editor. */
      private void setResourceInformation() {
  
          String sitePath = m_cms.getSitePath(m_resource);
          int pathEnd = sitePath.lastIndexOf('/') + 1;
          String baseName = sitePath.substring(pathEnd);
          m_sitepath = sitePath.substring(0, pathEnd);
          switch (CmsMessageBundleEditorTypes.BundleType.toBundleType(
              OpenCms.getResourceManager().getResourceType(m_resource).getTypeName())) {
              case PROPERTY:
                  String localeSuffix = CmsStringUtil.getLocaleSuffixForName(baseName);
                  if ((null != localeSuffix) && !localeSuffix.isEmpty()) {
                      baseName = baseName.substring(
                          0,
                          baseName.lastIndexOf(localeSuffix) - (1 /* cut off trailing underscore, too*/));
                      m_locale = CmsLocaleManager.getLocale(localeSuffix);
                  }
                  if ((null == m_locale) || !m_locales.contains(m_locale)) {
                      m_switchedLocaleOnOpening = true;
                      m_locale = m_locales.iterator().next();
                  }
                  break;
              case XML:
                  m_locale = OpenCms.getLocaleManager().getBestAvailableLocaleForXmlContent(
                      m_cms,
                      m_resource,
                      m_xmlBundle);
                  break;
              case DESCRIPTOR:
                  m_basename = baseName.substring(
                      0,
                      baseName.length() - CmsMessageBundleEditorTypes.Descriptor.POSTFIX.length());
                  m_locale = new Locale("en");
                  break;
              default:
                  throw new IllegalArgumentException(
                      Messages.get().container(
                          Messages.ERR_UNSUPPORTED_BUNDLE_TYPE_1,
                          CmsMessageBundleEditorTypes.BundleType.toBundleType(
                              OpenCms.getResourceManager().getResourceType(m_resource).getTypeName())).toString());
          }
          m_basename = baseName;
  
      }
  
      /**
       * Unmarshals the descriptor content.
       *
       * @throws CmsXmlException thrown if the XML structure of the descriptor is wrong.
       * @throws CmsException thrown if reading the descriptor file fails.
       */
      private void unmarshalDescriptor() throws CmsXmlException, CmsException {
  
          if (null != m_desc) {
  
              // unmarshal descriptor
              m_descContent = CmsXmlContentFactory.unmarshal(m_cms, m_cms.readFile(m_desc));
  
              // configure messages if wanted
              CmsProperty bundleProp = m_cms.readPropertyObject(m_desc, PROPERTY_BUNDLE_DESCRIPTOR_LOCALIZATION, true);
              if (!(bundleProp.isNullProperty() || bundleProp.getValue().trim().isEmpty())) {
                  m_configuredBundle = bundleProp.getValue();
              }
          }
  
      }
  
      /**
      * Update the descriptor content with values from the editor.
      * @throws CmsXmlException thrown if update fails due to a wrong XML structure (should never happen)
      */
      private void updateBundleDescriptorContent() throws CmsXmlException {
  
          if (m_descContent.hasLocale(Descriptor.LOCALE)) {
              m_descContent.removeLocale(Descriptor.LOCALE);
          }
          m_descContent.addLocale(m_cms, Descriptor.LOCALE);
  
          int i = 0;
          Property<Object> descProp;
          String desc;
          Property<Object> defaultValueProp;
          String defaultValue;
          Map<String, Item> keyItemMap = getKeyItemMap();
          List<String> keys = new ArrayList<String>(keyItemMap.keySet());
          Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
          for (Object key : keys) {
              if ((null != key) && !key.toString().isEmpty()) {
  
                  m_descContent.addValue(m_cms, Descriptor.N_MESSAGE, Descriptor.LOCALE, i);
                  i++;
                  String messagePrefix = Descriptor.N_MESSAGE + "[" + i + "]/";
  
                  m_descContent.getValue(messagePrefix + Descriptor.N_KEY, Descriptor.LOCALE).setStringValue(
                      m_cms,
                      (String)key);
                  descProp = keyItemMap.get(key).getItemProperty(TableProperty.DESCRIPTION);
                  if ((null != descProp) && (null != descProp.getValue())) {
                      desc = descProp.getValue().toString();
                      m_descContent.getValue(messagePrefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).setStringValue(
                          m_cms,
                          desc);
                  }
  
                  defaultValueProp = keyItemMap.get(key).getItemProperty(TableProperty.DEFAULT);
                  if ((null != defaultValueProp) && (null != defaultValueProp.getValue())) {
                      defaultValue = defaultValueProp.getValue().toString();
                      m_descContent.getValue(messagePrefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).setStringValue(
                          m_cms,
                          defaultValue);
                  }
  
              }
          }
  
      }
  
      /**
       * Clears all internal lock info.
       */
      private void updateLockInformation() {
  
          m_lockedBundleFiles.clear();
          m_descFile = null;
  
      }
  }