001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.editors.messagebundle;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResource.CmsResourceDeleteMode;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.CmsVfsResourceNotFoundException;
038import org.opencms.i18n.CmsLocaleManager;
039import org.opencms.i18n.CmsMessageContainer;
040import org.opencms.i18n.CmsMessageException;
041import org.opencms.i18n.CmsMessages;
042import org.opencms.loader.CmsLoaderException;
043import org.opencms.lock.CmsLockUtil.LockedFile;
044import org.opencms.main.CmsException;
045import org.opencms.main.CmsIllegalArgumentException;
046import org.opencms.main.CmsLog;
047import org.opencms.main.OpenCms;
048import org.opencms.security.CmsPermissionSet;
049import org.opencms.ui.A_CmsDialogContext;
050import org.opencms.ui.I_CmsDialogContext;
051import org.opencms.ui.I_CmsDialogContext.ContextType;
052import org.opencms.ui.actions.CmsDirectPublishDialogAction;
053import org.opencms.ui.contextmenu.CmsContextMenu;
054import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItem;
055import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickEvent;
056import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickListener;
057import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.BundleType;
058import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.Descriptor;
059import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EditMode;
060import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EditorState;
061import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.EntryChangeEvent;
062import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.TableProperty;
063import org.opencms.util.CmsFileUtil;
064import org.opencms.util.CmsStringUtil;
065import org.opencms.util.CmsUUID;
066import org.opencms.xml.CmsXmlException;
067import org.opencms.xml.content.CmsXmlContent;
068import org.opencms.xml.content.CmsXmlContentFactory;
069import org.opencms.xml.content.CmsXmlContentValueSequence;
070import org.opencms.xml.types.I_CmsXmlContentValue;
071
072import java.io.BufferedWriter;
073import java.io.ByteArrayInputStream;
074import java.io.ByteArrayOutputStream;
075import java.io.IOException;
076import java.io.InputStreamReader;
077import java.io.OutputStream;
078import java.io.OutputStreamWriter;
079import java.io.UnsupportedEncodingException;
080import java.io.Writer;
081import java.util.ArrayList;
082import java.util.Collection;
083import java.util.Collections;
084import java.util.Comparator;
085import java.util.HashMap;
086import java.util.HashSet;
087import java.util.List;
088import java.util.Locale;
089import java.util.Map;
090import java.util.Map.Entry;
091import java.util.Properties;
092import java.util.Set;
093
094import org.apache.commons.logging.Log;
095
096import com.vaadin.ui.UI;
097import com.vaadin.v7.data.Item;
098import com.vaadin.v7.data.Property;
099import com.vaadin.v7.data.util.DefaultItemSorter;
100import com.vaadin.v7.data.util.IndexedContainer;
101
102/**
103 * The class contains the logic behind the message translation editor.
104 * In particular it reads / writes the involved files and provides the contents as {@link IndexedContainer}.
105 */
106public class CmsMessageBundleEditorModel {
107
108    /** Comparator that compares strings case insensitive. */
109    public static final class CmsCaseInsensitiveStringComparator implements Comparator<Object> {
110
111        /** Single instance of the comparator. */
112        private static CmsCaseInsensitiveStringComparator m_instance = new CmsCaseInsensitiveStringComparator();
113
114        /**
115         * Hide the default constructor.
116         */
117        private CmsCaseInsensitiveStringComparator() {
118
119            // Hide constructor
120        }
121
122        /**
123         * Returns the comparator instance.
124         * @return the comparator
125         */
126        public static CmsCaseInsensitiveStringComparator getInstance() {
127
128            // is never null, because it's directly initialized on class load.
129            return m_instance;
130        }
131
132        /**
133         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
134         */
135        @SuppressWarnings("unchecked")
136        public int compare(Object o1, Object o2) {
137
138            int r = 0;
139            // Normal non-null comparison
140            if ((o1 != null) && (o2 != null)) {
141                if ((o1 instanceof String) && (o2 instanceof String)) {
142                    String string1 = (String)o1;
143                    String string2 = (String)o2;
144                    r = String.CASE_INSENSITIVE_ORDER.compare(string1, string2);
145                    if (r == 0) {
146                        r = string1.compareTo(string2);
147                    }
148                } else {
149                    // Assume the objects can be cast to Comparable, throw
150                    // ClassCastException otherwise.
151                    r = ((Comparable<Object>)o1).compareTo(o2);
152                }
153            } else if (o1 == o2) {
154                // Objects are equal if both are null
155                r = 0;
156            } else {
157                if (o1 == null) {
158                    r = -1; // null is less than non-null
159                } else {
160                    r = 1; // non-null is greater than null
161                }
162            }
163
164            return r;
165        }
166    }
167
168    /** Wrapper for the configurable messages for the column headers of the message bundle editor. */
169    public static final class ConfigurableMessages {
170
171        /** The messages from the default message bundle. */
172        CmsMessages m_defaultMessages;
173        /** The messages from a configured message bundle, overwriting the ones from the default bundle. */
174        CmsMessages m_configuredMessages;
175
176        /**
177         * Default constructor.
178         * @param defaultMessages the default messages.
179         * @param locale the locale in which the messages are requested.
180         * @param configuredBundle the base name of the configured message bundle (can be <code>null</code>).
181         */
182        public ConfigurableMessages(CmsMessages defaultMessages, Locale locale, String configuredBundle) {
183
184            m_defaultMessages = defaultMessages;
185            if (null != configuredBundle) {
186                CmsMessages bundle = new CmsMessages(configuredBundle, locale);
187                if (null != bundle.getResourceBundle()) {
188                    m_configuredMessages = bundle;
189                }
190            }
191        }
192
193        /**
194         * Returns the localized column header.
195         * @param column the column's property (name).
196         * @return the localized columen header.
197         */
198        public String getColumnHeader(TableProperty column) {
199
200            switch (column) {
201                case DEFAULT:
202                    return getMessage(Messages.GUI_COLUMN_HEADER_DEFAULT_0);
203                case DESCRIPTION:
204                    return getMessage(Messages.GUI_COLUMN_HEADER_DESCRIPTION_0);
205                case KEY:
206                    return getMessage(Messages.GUI_COLUMN_HEADER_KEY_0);
207                case OPTIONS:
208                    return "";
209                case TRANSLATION:
210                    return getMessage(Messages.GUI_COLUMN_HEADER_TRANSLATION_0);
211                default:
212                    throw new IllegalArgumentException();
213            }
214        }
215
216        /**
217         * Returns the message for the key, either from the configured bundle, or - if not found - from the default bundle.
218         *
219         * @param key message key.
220         * @return the message for the key.
221         */
222        private String getMessage(String key) {
223
224            if (null != m_configuredMessages) {
225                try {
226                    return m_configuredMessages.getString(key);
227                } catch (CmsMessageException e) {
228                    // do nothing - use default messages
229                }
230            }
231            try {
232                return m_defaultMessages.getString(key);
233            } catch (CmsMessageException e) {
234                return "???" + key + "???";
235            }
236        }
237
238    }
239
240    /** Extension of {@link Properties} to allow saving with keys alphabetically ordered and without time stamp as first comment.
241     *
242     * NOTE: Can't handle comments. They are just discarded.
243     * 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.
244     * NOTE: The solution was taken to guarantee correct escaping when storing properties.
245     *
246     */
247    public static final class SortedProperties extends Properties {
248
249        /** Serialization id to implement Serializable. */
250        private static final long serialVersionUID = 8814525892788043348L;
251
252        /**
253         * NOTE: This is a one-to-one copy from {@link java.util.Properties} with HEX_DIGIT renamed to HEX_DIGIT.
254         * A table of hex digits.
255         */
256        private static final char[] HEX_DIGIT = {
257            '0',
258            '1',
259            '2',
260            '3',
261            '4',
262            '5',
263            '6',
264            '7',
265            '8',
266            '9',
267            'A',
268            'B',
269            'C',
270            'D',
271            'E',
272            'F'};
273
274        /**
275         * Default constructor.
276         */
277        public SortedProperties() {
278
279            super();
280        }
281
282        /**
283         * NOTE: This is a one-to-one copy from {@link java.util.Properties}
284         * Convert a nibble to a hex character.
285         * @param   nibble  the nibble to convert.
286         * @return the character as Hex
287         */
288        private static char toHex(int nibble) {
289
290            return HEX_DIGIT[(nibble & 0xF)];
291        }
292
293        /**
294         * Override to omit the date comment.
295         * @see java.util.Properties#store(java.io.OutputStream, java.lang.String)
296         */
297        @Override
298        public void store(OutputStream out, String comments) throws IOException {
299
300            store0(new BufferedWriter(new OutputStreamWriter(out, "8859_1")), true);
301        }
302
303        /**
304         * Override to omit the date comment.
305         * @see java.util.Properties#store(java.io.Writer, java.lang.String)
306         */
307        @Override
308        public void store(Writer writer, String comments) throws IOException {
309
310            store0((writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer), false);
311        }
312
313        /**
314         * NOTE: This is a one-to-one copy of the private method from {@link java.util.Properties}
315         * Converts unicodes to encoded &#92;uxxxx and escapes
316         * special characters with a preceding slash.
317         *
318         * @param theString string to convert
319         * @param escapeSpace flag, indicating if spaces should be escaped
320         * @param escapeUnicode flag, indicating if unicode signs should be escaped
321         * @return the converted string
322         */
323        private String saveConvert(String theString, boolean escapeSpace, boolean escapeUnicode) {
324
325            int len = theString.length();
326            int bufLen = len * 2;
327            if (bufLen < 0) {
328                bufLen = Integer.MAX_VALUE;
329            }
330            StringBuffer outBuffer = new StringBuffer(bufLen);
331
332            for (int x = 0; x < len; x++) {
333                char aChar = theString.charAt(x);
334                // Handle common case first, selecting largest block that
335                // avoids the specials below
336                if ((aChar > 61) && (aChar < 127)) {
337                    if (aChar == '\\') {
338                        outBuffer.append('\\');
339                        outBuffer.append('\\');
340                        continue;
341                    }
342                    outBuffer.append(aChar);
343                    continue;
344                }
345                switch (aChar) {
346                    case ' ':
347                        if ((x == 0) || escapeSpace) {
348                            outBuffer.append('\\');
349                        }
350                        outBuffer.append(' ');
351                        break;
352                    case '\t':
353                        outBuffer.append('\\');
354                        outBuffer.append('t');
355                        break;
356                    case '\n':
357                        outBuffer.append('\\');
358                        outBuffer.append('n');
359                        break;
360                    case '\r':
361                        outBuffer.append('\\');
362                        outBuffer.append('r');
363                        break;
364                    case '\f':
365                        outBuffer.append('\\');
366                        outBuffer.append('f');
367                        break;
368                    case '=': // Fall through
369                    case ':': // Fall through
370                    case '#': // Fall through
371                    case '!':
372                        outBuffer.append('\\');
373                        outBuffer.append(aChar);
374                        break;
375                    default:
376                        if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) {
377                            outBuffer.append('\\');
378                            outBuffer.append('u');
379                            outBuffer.append(toHex((aChar >> 12) & 0xF));
380                            outBuffer.append(toHex((aChar >> 8) & 0xF));
381                            outBuffer.append(toHex((aChar >> 4) & 0xF));
382                            outBuffer.append(toHex(aChar & 0xF));
383                        } else {
384                            outBuffer.append(aChar);
385                        }
386                }
387            }
388            return outBuffer.toString();
389        }
390
391        /**
392         * Adaption of {@link java.util.Properties#store0(BufferedWriter,String,boolean)}.
393         * The behavior differs as follows:<ul>
394         * <li>Comments are not handled.</li>
395         * <li>The time stamp comment is omited.</li>
396         * <li>Messages are stored sorted alphabetically by key</li>
397         * </ul>
398         * @param bw writer to write to
399         * @param escUnicode flag, indicating if unicode characters should be escaped
400         * @throws IOException
401         */
402        @SuppressWarnings("javadoc")
403        private void store0(BufferedWriter bw, boolean escUnicode) throws IOException {
404
405            synchronized (this) {
406                List<Object> keys = new ArrayList<Object>(super.keySet());
407                Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
408                for (Object k : keys) {
409                    String key = (String)k;
410                    String val = (String)get(key);
411                    key = saveConvert(key, true, escUnicode);
412                    /* No need to escape embedded and trailing spaces for value, hence
413                     * pass false to flag.
414                     */
415                    val = saveConvert(val, false, escUnicode);
416                    bw.write(key + "=" + val);
417                    bw.newLine();
418                }
419            }
420            bw.flush();
421        }
422    }
423
424    /** The result of a key change. */
425    enum KeyChangeResult {
426        /** Key change was successful. */
427        SUCCESS,
428        /** Key change failed, because the new key already exists. */
429        FAILED_DUPLICATED_KEY,
430        /** Key change failed, because the key could not be changed for one or more languages. */
431        FAILED_FOR_OTHER_LANGUAGE
432    }
433
434    /** The log object for this class. */
435    private static final Log LOG = CmsLog.getLog(CmsMessageBundleEditorModel.class);
436
437    /** The property for configuring the message bundle used for localizing the bundle descriptors entries. */
438    public static final String PROPERTY_BUNDLE_DESCRIPTOR_LOCALIZATION = "bundle.descriptor.messages";
439
440    /** CmsObject for read / write operations. */
441    private CmsObject m_cms;
442    /** The files currently edited. */
443    private Map<Locale, LockedFile> m_lockedBundleFiles;
444    /** The files of the bundle. */
445    private Map<Locale, CmsResource> m_bundleFiles;
446    /** The resource that was opened with the editor. */
447    private CmsResource m_resource;
448    /** The bundle descriptor resource. */
449    private CmsResource m_desc;
450    /** The bundle descriptor as unmarshalled XML Content. */
451    private CmsXmlContent m_descContent;
452    /** The xml bundle edited (or null, if a property bundle is edited). */
453    private CmsXmlContent m_xmlBundle;
454    /** The already loaded localizations. */
455    private Map<Locale, SortedProperties> m_localizations;
456    /** The bundle's base name. */
457    private String m_basename;
458    /** The site path to the folder where the edited resource is in. */
459    private String m_sitepath;
460    /** The currently edited locale. */
461    private Locale m_locale;
462    /** The type of the loaded bundle. */
463    private CmsMessageBundleEditorTypes.BundleType m_bundleType;
464
465    /** The complete key set as map from keys to the number of occurrences. */
466    CmsMessageBundleEditorTypes.KeySet m_keyset;
467
468    /** Containers holding the keys for each locale. */
469    private IndexedContainer m_container;
470    /** The available locales. */
471    private Collection<Locale> m_locales;
472    /** Map from edit mode to the editor state. */
473    private Map<CmsMessageBundleEditorTypes.EditMode, EditorState> m_editorState;
474    /** Flag, indicating if a master edit mode is available. */
475    private boolean m_hasMasterMode;
476    /** The current edit mode. */
477    private CmsMessageBundleEditorTypes.EditMode m_editMode;
478
479    /** Descriptor file, if edited besides a bundle. */
480    private LockedFile m_descFile;
481
482    /** The configured resource bundle used for the column headings of the bundle descriptor. */
483    private String m_configuredBundle;
484
485    /** Flag, indicating if the locale of the bundle that is edited has switched on opening. */
486    private boolean m_switchedLocaleOnOpening;
487
488    /** Flag, indicating if at least one default value is present in the current descriptor. */
489    private boolean m_hasDefault;
490
491    /** Flag, indicating if at least one description is present in the current descriptor. */
492    private boolean m_hasDescription;
493
494    /** Flag, indicating if the descriptor should be removed when editing is cancelled. */
495    private boolean m_removeDescriptorOnCancel;
496
497    /** Flag, indicating if something changed. */
498    Set<Locale> m_changedTranslations;
499
500    /** Flag, indicating if something changed. */
501    boolean m_descriptorHasChanges;
502
503    /** Flag, indicating if all localizations have already been loaded. */
504    private boolean m_alreadyLoadedAllLocalizations;
505
506    /**
507     *
508     * @param cms the {@link CmsObject} used for reading / writing.
509     * @param resource the file that is opened for editing.
510     * @throws CmsException thrown if reading some of the involved {@link CmsResource}s is not possible.
511     * @throws IOException initialization of a property bundle fails
512     */
513    public CmsMessageBundleEditorModel(CmsObject cms, CmsResource resource)
514    throws CmsException, IOException {
515
516        if (cms == null) {
517            throw new CmsException(Messages.get().container(Messages.ERR_LOADING_BUNDLE_CMS_OBJECT_NULL_0));
518        }
519
520        if (resource == null) {
521            throw new CmsException(Messages.get().container(Messages.ERR_LOADING_BUNDLE_FILENAME_NULL_0));
522        }
523
524        m_cms = cms;
525        m_resource = resource;
526        m_editMode = CmsMessageBundleEditorTypes.EditMode.DEFAULT;
527
528        m_bundleFiles = new HashMap<Locale, CmsResource>();
529        m_lockedBundleFiles = new HashMap<Locale, LockedFile>();
530        m_changedTranslations = new HashSet<Locale>();
531        m_localizations = new HashMap<Locale, SortedProperties>();
532        m_keyset = new CmsMessageBundleEditorTypes.KeySet();
533
534        m_bundleType = initBundleType();
535
536        m_locales = initLocales();
537
538        //IMPORTANT: The order of the following method calls is important.
539
540        if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.XML)) {
541            initXmlBundle();
542        }
543
544        setResourceInformation();
545
546        initDescriptor();
547
548        if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.PROPERTY)) {
549            initPropertyBundle();
550        }
551
552        initHasMasterMode();
553
554        initEditorStates();
555
556    }
557
558    /**
559     * Creates a descriptor for the currently edited message bundle.
560     * @return <code>true</code> if the descriptor could be created, <code>false</code> otherwise.
561     */
562    public boolean addDescriptor() {
563
564        saveLocalization();
565        IndexedContainer oldContainer = m_container;
566        try {
567            createAndLockDescriptorFile();
568            unmarshalDescriptor();
569            updateBundleDescriptorContent();
570            m_hasMasterMode = true;
571            m_container = createContainer();
572            m_editorState.put(EditMode.DEFAULT, getDefaultState());
573            m_editorState.put(EditMode.MASTER, getMasterState());
574        } catch (CmsException | IOException e) {
575            LOG.error(e.getLocalizedMessage(), e);
576            if (m_descContent != null) {
577                m_descContent = null;
578            }
579            if (m_descFile != null) {
580                m_descFile = null;
581            }
582            if (m_desc != null) {
583                try {
584                    m_cms.deleteResource(m_desc, CmsResourceDeleteMode.valueOf(1));
585                } catch (CmsException ex) {
586                    LOG.error(ex.getLocalizedMessage(), ex);
587                }
588                m_desc = null;
589            }
590            m_hasMasterMode = false;
591            m_container = oldContainer;
592            return false;
593        }
594        m_removeDescriptorOnCancel = true;
595        return true;
596    }
597
598    /**
599     * Returns a flag, indicating if keys can be added in the current edit mode.
600     * @return a flag, indicating if keys can be added in the current edit mode.
601     */
602    public boolean canAddKeys() {
603
604        return !hasDescriptor() || getEditMode().equals(EditMode.MASTER);
605    }
606
607    /**
608     * When the descriptor was added while editing, but the change was not saved, it has to be removed
609     * when the editor is closed.
610     * @throws CmsException thrown when deleting the descriptor resource fails
611     */
612    public void deleteDescriptorIfNecessary() throws CmsException {
613
614        if (m_removeDescriptorOnCancel && (m_desc != null)) {
615            m_cms.deleteResource(m_desc, CmsResourceDeleteMode.valueOf(2));
616        }
617
618    }
619
620    /** Returns the a set with all keys that are used at least in one translation.
621     * @return the a set with all keys that are used at least in one translation.
622     */
623    public Set<Object> getAllUsedKeys() {
624
625        return m_keyset.getKeySet();
626    }
627
628    /** Returns the type of the currently edited bundle.
629     * @return the type of the currently edited bundle.
630     */
631    public BundleType getBundleType() {
632
633        return m_bundleType;
634    }
635
636    /**
637     * Returns the configured bundle, or the provided default bundle.
638     * @param defaultMessages the default bundle
639     * @param locale the preferred locale
640     * @return the configured bundle or, if not found, the default bundle.
641     */
642    public ConfigurableMessages getConfigurableMessages(CmsMessages defaultMessages, Locale locale) {
643
644        return new ConfigurableMessages(defaultMessages, locale, m_configuredBundle);
645
646    }
647
648    /**
649     * Returns the container filled according to the current locale.
650     * @return the container filled according to the current locale.
651     * @throws IOException thrown if reading a bundle resource fails.
652     * @throws CmsException thrown if reading a bundle resource fails.
653     */
654    public IndexedContainer getContainerForCurrentLocale() throws IOException, CmsException {
655
656        if (null == m_container) {
657            m_container = createContainer();
658        }
659        return m_container;
660    }
661
662    /**
663     * Returns the context menu for the table item.
664     * @param itemId the table item.
665     * @return the context menu for the given item.
666     */
667    public CmsContextMenu getContextMenuForItem(Object itemId) {
668
669        CmsContextMenu result = null;
670        try {
671            final Item item = m_container.getItem(itemId);
672            Property<?> keyProp = item.getItemProperty(TableProperty.KEY);
673            String key = (String)keyProp.getValue();
674            if ((null != key) && !key.isEmpty()) {
675                loadAllRemainingLocalizations();
676                final Map<Locale, String> localesWithEntries = new HashMap<Locale, String>();
677                for (Locale l : m_localizations.keySet()) {
678                    if (l != m_locale) {
679                        String value = m_localizations.get(l).getProperty(key);
680                        if ((null != value) && !value.isEmpty()) {
681                            localesWithEntries.put(l, value);
682                        }
683                    }
684                }
685                if (!localesWithEntries.isEmpty()) {
686                    result = new CmsContextMenu();
687                    ContextMenuItem mainItem = result.addItem(
688                        Messages.get().getBundle(UI.getCurrent().getLocale()).key(
689                            Messages.GUI_BUNDLE_EDITOR_CONTEXT_COPY_LOCALE_0));
690                    for (final Locale l : localesWithEntries.keySet()) {
691
692                        ContextMenuItem menuItem = mainItem.addItem(l.getDisplayName(UI.getCurrent().getLocale()));
693                        menuItem.addItemClickListener(new ContextMenuItemClickListener() {
694
695                            public void contextMenuItemClicked(ContextMenuItemClickEvent event) {
696
697                                item.getItemProperty(TableProperty.TRANSLATION).setValue(localesWithEntries.get(l));
698
699                            }
700                        });
701                    }
702                }
703            }
704        } catch (Exception e) {
705            LOG.error(e.getLocalizedMessage(), e);
706            //TODO: Improve
707        }
708        return result;
709    }
710
711    /**
712     * Returns the editable columns for the current edit mode.
713     * @return the editable columns for the current edit mode.
714     */
715    public List<TableProperty> getEditableColumns() {
716
717        return m_editorState.get(m_editMode).getEditableColumns();
718    }
719
720    /**
721     * Returns the editable columns for the provided edit mode.
722     * @param mode the edit mode.
723     * @return the editable columns for the provided edit mode.
724     */
725    public List<TableProperty> getEditableColumns(CmsMessageBundleEditorTypes.EditMode mode) {
726
727        return m_editorState.get(mode).getEditableColumns();
728    }
729
730    /**
731     * Returns the site path for the edited bundle file.
732     *
733     * @return the site path for the edited bundle file.
734     */
735    public String getEditedFilePath() {
736
737        switch (getBundleType()) {
738            case DESCRIPTOR:
739                return m_cms.getSitePath(m_desc);
740            case PROPERTY:
741                return null != m_lockedBundleFiles.get(getLocale())
742                ? m_cms.getSitePath(m_lockedBundleFiles.get(getLocale()).getFile())
743                : m_cms.getSitePath(m_resource);
744            case XML:
745                return m_cms.getSitePath(m_resource);
746            default:
747                throw new IllegalArgumentException();
748        }
749    }
750
751    /** Returns the current edit mode.
752     * @return the current edit mode.
753     */
754    public CmsMessageBundleEditorTypes.EditMode getEditMode() {
755
756        return m_editMode;
757    }
758
759    /**
760     * Returns the currently edited locale.
761     *
762     * @return the currently edited locale.
763     */
764    public Locale getLocale() {
765
766        return m_locale;
767    }
768
769    /**
770     * Returns the locales available for the specific resource.
771     *
772     * @return the locales available for the specific resource.
773     */
774    public Collection<Locale> getLocales() {
775
776        return m_locales;
777    }
778
779    /**
780     * Returns a flag, indicating if the locale has been switched on opening.
781     * @return a flag, indicating if the locale has been switched on opening.
782     */
783    public boolean getSwitchedLocaleOnOpening() {
784
785        return m_switchedLocaleOnOpening;
786    }
787
788    /**
789     * Handles the change of a value in the current translation.
790     * @param propertyId the property id of the column where the value has changed.
791     */
792    public void handleChange(Object propertyId) {
793
794        try {
795            lockOnChange(propertyId);
796        } catch (CmsException e) {
797            LOG.debug(e.getLocalizedMessage(), e);
798        }
799        if (isDescriptorProperty(propertyId)) {
800            m_descriptorHasChanges = true;
801        }
802        if (isBundleProperty(propertyId)) {
803            m_changedTranslations.add(getLocale());
804        }
805
806    }
807
808    /**
809     * Handles a key change.
810     *
811     * @param event the key change event.
812     * @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.
813     * @return result, indicating if the key change was successful.
814     */
815    public KeyChangeResult handleKeyChange(EntryChangeEvent event, boolean allLanguages) {
816
817        if (m_keyset.getKeySet().contains(event.getNewValue())) {
818            m_container.getItem(event.getItemId()).getItemProperty(TableProperty.KEY).setValue(event.getOldValue());
819            return KeyChangeResult.FAILED_DUPLICATED_KEY;
820        }
821        if (allLanguages && !renameKeyForAllLanguages(event.getOldValue(), event.getNewValue())) {
822            m_container.getItem(event.getItemId()).getItemProperty(TableProperty.KEY).setValue(event.getOldValue());
823            return KeyChangeResult.FAILED_FOR_OTHER_LANGUAGE;
824        }
825        return KeyChangeResult.SUCCESS;
826    }
827
828    /**
829     * Handles the deletion of a key.
830     * @param key the deleted key.
831     * @return <code>true</code> if the deletion was successful, <code>false</code> otherwise.
832     */
833    public boolean handleKeyDeletion(final String key) {
834
835        if (m_keyset.getKeySet().contains(key)) {
836            if (removeKeyForAllLanguages(key)) {
837                m_keyset.removeKey(key);
838                return true;
839            } else {
840                return false;
841            }
842        }
843        return true;
844    }
845
846    /**
847     * Returns a flag, indicating if something was edited.
848     * @return a flag, indicating if something was edited.
849     */
850    public boolean hasChanges() {
851
852        return !m_changedTranslations.isEmpty() || m_descriptorHasChanges;
853    }
854
855    /**
856     * Returns a flag, indicating if the descriptor specifies any default values.
857     * @return flag, indicating if the descriptor specifies any default values.
858     */
859    public boolean hasDefaultValues() {
860
861        return m_hasDefault;
862    }
863
864    /**
865     * Returns a flag, indicating if the descriptor specifies any descriptions.
866     * @return a flag, indicating if the descriptor specifies any descriptions.
867     */
868    public boolean hasDescriptionValues() {
869
870        return m_hasDescription;
871    }
872
873    /** Returns a flag, indicating if a bundle descriptor is present.
874     * @return flag, indicating if a bundle descriptor is present.
875     */
876    public boolean hasDescriptor() {
877
878        return !m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR) && (m_descContent != null);
879    }
880
881    /**
882     * Returns a flag, indicating if a master edit mode is available.
883     * @return a flag, indicating if a master edit mode is available.
884     */
885    public boolean hasMasterMode() {
886
887        return m_hasMasterMode;
888    }
889
890    /**
891     * Returns a flag, indicating if the options column (with add and delete option for rows)
892     * should be shown in the given edit mode.
893     * @param mode the edit mode for which the column option is requested.
894     * @return a flag, indicating if the options column (with add and delete option for rows)
895     */
896    public boolean isShowOptionsColumn(CmsMessageBundleEditorTypes.EditMode mode) {
897
898        return m_editorState.get(mode).isShowOptions();
899    }
900
901    /**
902     * Publish the bundle resources directly.
903     */
904    public void publish() {
905
906        CmsDirectPublishDialogAction action = new CmsDirectPublishDialogAction();
907        List<CmsResource> resources = getBundleResources();
908        I_CmsDialogContext context = new A_CmsDialogContext("", ContextType.appToolbar, resources) {
909
910            public void focus(CmsUUID structureId) {
911
912                //Nothing to do.
913            }
914
915            public List<CmsUUID> getAllStructureIdsInView() {
916
917                return null;
918            }
919
920            public void updateUserInfo() {
921
922                //Nothing to do.
923            }
924        };
925        action.executeAction(context);
926        updateLockInformation();
927
928    }
929
930    /**
931     * Saves the messages for all languages that were opened in the editor.
932     *
933     * @throws CmsException thrown if saving fails.
934     */
935    public void save() throws CmsException {
936
937        if (hasChanges()) {
938            switch (m_bundleType) {
939                case PROPERTY:
940                    saveLocalization();
941                    saveToPropertyVfsBundle();
942                    break;
943
944                case XML:
945                    saveLocalization();
946                    saveToXmlVfsBundle();
947
948                    break;
949
950                case DESCRIPTOR:
951                    break;
952                default:
953                    throw new IllegalArgumentException();
954            }
955            if (null != m_descFile) {
956                saveToBundleDescriptor();
957            }
958
959            resetChanges();
960        }
961
962    }
963
964    /**
965     * Saves the loaded XML bundle as property bundle.
966     * @throws UnsupportedEncodingException thrown if localizations from the XML bundle could not be loaded correctly.
967     * @throws CmsException thrown if any of the interactions with the VFS fails.
968     * @throws IOException thrown if localizations from the XML bundle could not be loaded correctly.
969     */
970    public void saveAsPropertyBundle() throws UnsupportedEncodingException, CmsException, IOException {
971
972        switch (m_bundleType) {
973            case XML:
974                saveLocalization();
975                loadAllRemainingLocalizations();
976                createPropertyVfsBundleFiles();
977                saveToPropertyVfsBundle();
978                m_bundleType = BundleType.PROPERTY;
979                removeXmlBundleFile();
980
981                break;
982            default:
983                throw new IllegalArgumentException(
984                    "The method should only be called when editing an XMLResourceBundle.");
985        }
986
987    }
988
989    /**
990     * Set the edit mode.
991     * @param mode the edit mode to set.
992     * @return flag, indicating if the mode could be changed.
993     */
994    public boolean setEditMode(CmsMessageBundleEditorTypes.EditMode mode) {
995
996        try {
997            if ((mode == CmsMessageBundleEditorTypes.EditMode.MASTER) && (null == m_descFile)) {
998                m_descFile = LockedFile.lockResource(m_cms, m_desc);
999            }
1000            m_editMode = mode;
1001        } catch (CmsException e) {
1002            return false;
1003        }
1004        return true;
1005    }
1006
1007    /**
1008     * Set the currently edited locale.
1009     *
1010     * @param locale the currently edited locale.
1011     * @return <code>true</code> if the locale could be set, <code>false</code> otherwise.
1012     */
1013    public boolean setLocale(Locale locale) {
1014
1015        if (adjustExistingContainer(locale)) {
1016            m_locale = locale;
1017            return true;
1018        }
1019        return false;
1020    }
1021
1022    /**
1023     * Unlock all files opened for writing.
1024     */
1025    public void unlock() {
1026
1027        for (Locale l : m_lockedBundleFiles.keySet()) {
1028            LockedFile f = m_lockedBundleFiles.get(l);
1029            f.tryUnlock();
1030        }
1031        if (null != m_descFile) {
1032            m_descFile.tryUnlock();
1033        }
1034    }
1035
1036    /**
1037     * Returns all resources that belong to the bundle
1038     * This includes the descriptor if one exists.
1039     *
1040     * @return List of the bundle resources, including the descriptor.
1041     */
1042    List<CmsResource> getBundleResources() {
1043
1044        List<CmsResource> resources = new ArrayList<>(m_bundleFiles.values());
1045        if (m_desc != null) {
1046            resources.add(m_desc);
1047        }
1048        return resources;
1049
1050    }
1051
1052    /**
1053     * 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.
1054     *
1055     * @param locale the locale for which the container should be adjusted.
1056     * @return <code>true</code> if the locale could be switched, <code>false</code> otherwise.
1057     */
1058    private boolean adjustExistingContainer(Locale locale) {
1059
1060        saveLocalization();
1061        return replaceValues(locale);
1062
1063    }
1064
1065    /**
1066     * Creates a descriptor for the bundle in the same folder where the bundle files are located.
1067     * @throws CmsException thrown if creation fails.
1068     */
1069    private void createAndLockDescriptorFile() throws CmsException {
1070
1071        String sitePath = m_sitepath + m_basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX;
1072        m_desc = m_cms.createResource(
1073            sitePath,
1074            OpenCms.getResourceManager().getResourceType(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR.toString()));
1075        m_descFile = LockedFile.lockResource(m_cms, m_desc);
1076        m_descFile.setCreated(true);
1077    }
1078
1079    /**
1080     * Initializes the IndexedContainer shown in the table for the current locale.
1081     * Therefore, the involved {@link CmsResource}s will be read, if not already done.
1082     * @return the created container
1083     * @throws IOException thrown if reading of an involved file fails.
1084     * @throws CmsException thrown if reading of an involved file fails.
1085     */
1086    private IndexedContainer createContainer() throws IOException, CmsException {
1087
1088        IndexedContainer container = null;
1089
1090        if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR)) {
1091            container = createContainerForDescriptorEditing();
1092        } else {
1093            if (hasDescriptor()) {
1094                container = createContainerForBundleWithDescriptor();
1095            } else {
1096                container = createContainerForBundleWithoutDescriptor();
1097            }
1098        }
1099        container.setItemSorter(new DefaultItemSorter(CmsCaseInsensitiveStringComparator.getInstance()));
1100        return container;
1101    }
1102
1103    /**
1104     * Creates the container for a bundle with descriptor.
1105     * @return the container for a bundle with descriptor.
1106     * @throws IOException thrown if reading the bundle fails.
1107     * @throws CmsException thrown if reading the bundle fails.
1108     */
1109    private IndexedContainer createContainerForBundleWithDescriptor() throws IOException, CmsException {
1110
1111        IndexedContainer container = new IndexedContainer();
1112
1113        // create properties
1114        container.addContainerProperty(TableProperty.KEY, String.class, "");
1115        container.addContainerProperty(TableProperty.DESCRIPTION, String.class, "");
1116        container.addContainerProperty(TableProperty.DEFAULT, String.class, "");
1117        container.addContainerProperty(TableProperty.TRANSLATION, String.class, "");
1118
1119        // add entries
1120        SortedProperties localization = getLocalization(m_locale);
1121        CmsXmlContentValueSequence messages = m_descContent.getValueSequence(Descriptor.N_MESSAGE, Descriptor.LOCALE);
1122        String descValue;
1123        boolean hasDescription = false;
1124        String defaultValue;
1125        boolean hasDefault = false;
1126        for (int i = 0; i < messages.getElementCount(); i++) {
1127
1128            String prefix = messages.getValue(i).getPath() + "/";
1129            Object itemId = container.addItem();
1130            Item item = container.getItem(itemId);
1131            String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
1132            item.getItemProperty(TableProperty.KEY).setValue(key);
1133            String translation = localization.getProperty(key);
1134            item.getItemProperty(TableProperty.TRANSLATION).setValue(null == translation ? "" : translation);
1135            descValue = m_descContent.getValue(prefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).getStringValue(
1136                m_cms);
1137            item.getItemProperty(TableProperty.DESCRIPTION).setValue(descValue);
1138            hasDescription = hasDescription || !descValue.isEmpty();
1139            defaultValue = m_descContent.getValue(prefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).getStringValue(
1140                m_cms);
1141            item.getItemProperty(TableProperty.DEFAULT).setValue(defaultValue);
1142            hasDefault = hasDefault || !defaultValue.isEmpty();
1143        }
1144
1145        m_hasDefault = hasDefault;
1146        m_hasDescription = hasDescription;
1147        return container;
1148
1149    }
1150
1151    /**
1152     * Creates the container for a bundle without descriptor.
1153     * @return the container for a bundle without descriptor.
1154     * @throws IOException thrown if reading the bundle fails.
1155     * @throws CmsException thrown if reading the bundle fails.
1156     */
1157    private IndexedContainer createContainerForBundleWithoutDescriptor() throws IOException, CmsException {
1158
1159        IndexedContainer container = new IndexedContainer();
1160
1161        // create properties
1162        container.addContainerProperty(TableProperty.KEY, String.class, "");
1163        container.addContainerProperty(TableProperty.TRANSLATION, String.class, "");
1164
1165        // add entries
1166        SortedProperties localization = getLocalization(m_locale);
1167        Set<Object> keySet = m_keyset.getKeySet();
1168        for (Object key : keySet) {
1169
1170            Object itemId = container.addItem();
1171            Item item = container.getItem(itemId);
1172            item.getItemProperty(TableProperty.KEY).setValue(key);
1173            Object translation = localization.get(key);
1174            item.getItemProperty(TableProperty.TRANSLATION).setValue(null == translation ? "" : translation);
1175        }
1176
1177        return container;
1178    }
1179
1180    /**
1181     * Creates the container for a bundle descriptor.
1182     * @return the container for a bundle descriptor.
1183     */
1184    private IndexedContainer createContainerForDescriptorEditing() {
1185
1186        IndexedContainer container = new IndexedContainer();
1187
1188        // create properties
1189        container.addContainerProperty(TableProperty.KEY, String.class, "");
1190        container.addContainerProperty(TableProperty.DESCRIPTION, String.class, "");
1191        container.addContainerProperty(TableProperty.DEFAULT, String.class, "");
1192
1193        // add entries
1194        CmsXmlContentValueSequence messages = m_descContent.getValueSequence(
1195            "/" + Descriptor.N_MESSAGE,
1196            Descriptor.LOCALE);
1197        for (int i = 0; i < messages.getElementCount(); i++) {
1198
1199            String prefix = messages.getValue(i).getPath() + "/";
1200            Object itemId = container.addItem();
1201            Item item = container.getItem(itemId);
1202            String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
1203            item.getItemProperty(TableProperty.KEY).setValue(key);
1204            item.getItemProperty(TableProperty.DESCRIPTION).setValue(
1205                m_descContent.getValue(prefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).getStringValue(m_cms));
1206            item.getItemProperty(TableProperty.DEFAULT).setValue(
1207                m_descContent.getValue(prefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).getStringValue(m_cms));
1208        }
1209
1210        return container;
1211
1212    }
1213
1214    /**
1215     * Creates all propertyvfsbundle files for the currently loaded translations.
1216     * The method is used to convert xmlvfsbundle files into propertyvfsbundle files.
1217     *
1218     * @throws CmsIllegalArgumentException thrown if resource creation fails.
1219     * @throws CmsLoaderException thrown if the propertyvfsbundle type can't be read from the resource manager.
1220     * @throws CmsException thrown if creation, type retrieval or locking fails.
1221     */
1222    private void createPropertyVfsBundleFiles() throws CmsIllegalArgumentException, CmsLoaderException, CmsException {
1223
1224        String bundleFileBasePath = m_sitepath + m_basename + "_";
1225        for (Locale l : m_localizations.keySet()) {
1226            CmsResource res = m_cms.createResource(
1227                bundleFileBasePath + l.toString(),
1228                OpenCms.getResourceManager().getResourceType(
1229                    CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString()));
1230            m_bundleFiles.put(l, res);
1231            LockedFile file = LockedFile.lockResource(m_cms, res);
1232            file.setCreated(true);
1233            m_lockedBundleFiles.put(l, file);
1234            m_changedTranslations.add(l);
1235        }
1236
1237    }
1238
1239    /**
1240     * Creates the default editor state for editing a bundle with descriptor.
1241     * @return the default editor state for editing a bundle with descriptor.
1242     */
1243    private EditorState getDefaultState() {
1244
1245        List<TableProperty> cols = new ArrayList<TableProperty>(1);
1246        cols.add(TableProperty.TRANSLATION);
1247
1248        return new EditorState(cols, false);
1249    }
1250
1251    /**
1252     * Returns a map with key property as key and item as value.<p>
1253     *
1254     * @return HashMap
1255     */
1256    private Map<String, Item> getKeyItemMap() {
1257
1258        Map<String, Item> ret = new HashMap<String, Item>();
1259        for (Object itemId : m_container.getItemIds()) {
1260            ret.put(
1261                m_container.getItem(itemId).getItemProperty(TableProperty.KEY).getValue().toString(),
1262                m_container.getItem(itemId));
1263        }
1264        return ret;
1265
1266    }
1267
1268    /**
1269     * Reads the current properties for a language. If not already done, the properties are read from the respective file.
1270     * @param locale the locale for which the localization should be returned.
1271     * @return the properties.
1272     * @throws IOException thrown if reading the properties from a file fails.
1273     * @throws CmsException thrown if reading the properties from a file fails.
1274     */
1275    private SortedProperties getLocalization(Locale locale) throws IOException, CmsException {
1276
1277        if (null == m_localizations.get(locale)) {
1278            switch (m_bundleType) {
1279                case PROPERTY:
1280                    loadLocalizationFromPropertyBundle(locale);
1281                    break;
1282                case XML:
1283                    loadLocalizationFromXmlBundle(locale);
1284                    break;
1285                case DESCRIPTOR:
1286                    return null;
1287                default:
1288                    break;
1289            }
1290        }
1291        return m_localizations.get(locale);
1292    }
1293
1294    /**
1295     * Returns the master mode's editor state for editing a bundle with descriptor.
1296     * @return the master mode's editor state for editing a bundle with descriptor.
1297     */
1298    private EditorState getMasterState() {
1299
1300        List<TableProperty> cols = new ArrayList<TableProperty>(4);
1301        cols.add(TableProperty.KEY);
1302        cols.add(TableProperty.DESCRIPTION);
1303        cols.add(TableProperty.DEFAULT);
1304        cols.add(TableProperty.TRANSLATION);
1305        return new EditorState(cols, true);
1306    }
1307
1308    /**
1309     * Init the bundle type member variable.
1310     * @return the bundle type of the opened resource.
1311     */
1312    private CmsMessageBundleEditorTypes.BundleType initBundleType() {
1313
1314        String resourceTypeName = OpenCms.getResourceManager().getResourceType(m_resource).getTypeName();
1315        return CmsMessageBundleEditorTypes.BundleType.toBundleType(resourceTypeName);
1316    }
1317
1318    /**
1319     * Reads the bundle descriptor, sets m_desc and m_descContent.
1320     * @throws CmsXmlException thrown when unmarshalling fails.
1321     * @throws CmsException thrown when reading the resource fails or several bundle descriptors for the bundle exist.
1322     */
1323    private void initDescriptor() throws CmsXmlException, CmsException {
1324
1325        if (m_bundleType.equals(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR)) {
1326            m_desc = m_resource;
1327        } else {
1328            //First try to read from same folder like resource, if it fails use CmsMessageBundleEditorTypes.getDescriptor()
1329            try {
1330                m_desc = m_cms.readResource(m_sitepath + m_basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX);
1331            } catch (CmsVfsResourceNotFoundException e) {
1332                m_desc = CmsMessageBundleEditorTypes.getDescriptor(m_cms, m_basename);
1333            }
1334        }
1335        unmarshalDescriptor();
1336
1337    }
1338
1339    /**
1340     * Initializes the editor states for the different modes, depending on the type of the opened file.
1341     */
1342    private void initEditorStates() {
1343
1344        m_editorState = new HashMap<CmsMessageBundleEditorTypes.EditMode, EditorState>();
1345        List<TableProperty> cols = null;
1346        switch (m_bundleType) {
1347            case PROPERTY:
1348            case XML:
1349                if (hasDescriptor()) { // bundle descriptor is present, keys are not editable in default mode, maybe master mode is available
1350                    m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, getDefaultState());
1351                    if (hasMasterMode()) { // the bundle descriptor is editable
1352                        m_editorState.put(CmsMessageBundleEditorTypes.EditMode.MASTER, getMasterState());
1353                    }
1354                } else { // no bundle descriptor given - implies no master mode
1355                    cols = new ArrayList<TableProperty>(1);
1356                    cols.add(TableProperty.KEY);
1357                    cols.add(TableProperty.TRANSLATION);
1358                    m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, new EditorState(cols, true));
1359                }
1360                break;
1361            case DESCRIPTOR:
1362                cols = new ArrayList<TableProperty>(3);
1363                cols.add(TableProperty.KEY);
1364                cols.add(TableProperty.DESCRIPTION);
1365                cols.add(TableProperty.DEFAULT);
1366                m_editorState.put(CmsMessageBundleEditorTypes.EditMode.DEFAULT, new EditorState(cols, true));
1367                break;
1368            default:
1369                throw new IllegalArgumentException();
1370        }
1371
1372    }
1373
1374    /**
1375     * Initializes the information on an available master mode.
1376     * @throws CmsException thrown if the write permission check on the bundle descriptor fails.
1377     */
1378    private void initHasMasterMode() throws CmsException {
1379
1380        if (hasDescriptor()
1381            && m_cms.hasPermissions(m_desc, CmsPermissionSet.ACCESS_WRITE, false, CmsResourceFilter.ALL)) {
1382            m_hasMasterMode = true;
1383        } else {
1384            m_hasMasterMode = false;
1385        }
1386    }
1387
1388    /**
1389     * Initialize the key set for an xml bundle.
1390     */
1391    private void initKeySetForXmlBundle() {
1392
1393        // consider only available locales
1394        for (Locale l : m_locales) {
1395            if (m_xmlBundle.hasLocale(l)) {
1396                Set<Object> keys = new HashSet<Object>();
1397                for (I_CmsXmlContentValue msg : m_xmlBundle.getValueSequence("Message", l).getValues()) {
1398                    String msgpath = msg.getPath();
1399                    keys.add(m_xmlBundle.getStringValue(m_cms, msgpath + "/Key", l));
1400                }
1401                m_keyset.updateKeySet(null, keys);
1402            }
1403        }
1404
1405    }
1406
1407    /**
1408     * Initializes the locales that can be selected via the language switcher in the bundle editor.
1409     * @return the locales for which keys can be edited.
1410     */
1411    private Collection<Locale> initLocales() {
1412
1413        Collection<Locale> locales = null;
1414        switch (m_bundleType) {
1415            case DESCRIPTOR:
1416                locales = new ArrayList<Locale>(1);
1417                locales.add(Descriptor.LOCALE);
1418                break;
1419            case XML:
1420            case PROPERTY:
1421                locales = OpenCms.getLocaleManager().getAvailableLocales(m_cms, m_resource);
1422                break;
1423            default:
1424                throw new IllegalArgumentException();
1425        }
1426        return locales;
1427
1428    }
1429
1430    /**
1431     * Initialization necessary for editing a property bundle.
1432     *
1433     * @throws CmsLoaderException thrown if loading a bundle file fails.
1434     * @throws CmsException thrown if loading a bundle file fails.
1435     * @throws IOException thrown if loading a bundle file fails.
1436     */
1437    private void initPropertyBundle() throws CmsLoaderException, CmsException, IOException {
1438
1439        for (Locale l : m_locales) {
1440            String filePath = m_sitepath + m_basename + "_" + l.toString();
1441            CmsResource resource = null;
1442            if (m_cms.existsResource(
1443                filePath,
1444                CmsResourceFilter.requireType(
1445                    OpenCms.getResourceManager().getResourceType(BundleType.PROPERTY.toString())))) {
1446                resource = m_cms.readResource(filePath);
1447                SortedProperties props = new SortedProperties();
1448                CmsFile file = m_cms.readFile(resource);
1449                props.load(
1450                    new InputStreamReader(
1451                        new ByteArrayInputStream(file.getContents()),
1452                        CmsFileUtil.getEncoding(m_cms, file)));
1453                m_keyset.updateKeySet(null, props.keySet());
1454                m_bundleFiles.put(l, resource);
1455            }
1456        }
1457
1458    }
1459
1460    /**
1461     * Unmarshals the XML content and adds the file to the bundle files.
1462     * @throws CmsException thrown if reading the file or unmarshaling fails.
1463     */
1464    private void initXmlBundle() throws CmsException {
1465
1466        CmsFile file = m_cms.readFile(m_resource);
1467        m_bundleFiles.put(null, m_resource);
1468        m_xmlBundle = CmsXmlContentFactory.unmarshal(m_cms, file);
1469        initKeySetForXmlBundle();
1470
1471    }
1472
1473    /**
1474     * Check if values in the column "property" are written to the bundle files.
1475     * @param property the property id of the table column.
1476     * @return a flag, indicating if values of the table column are stored to the bundle files.
1477     */
1478    private boolean isBundleProperty(Object property) {
1479
1480        return (property.equals(TableProperty.KEY) || property.equals(TableProperty.TRANSLATION));
1481    }
1482
1483    /**
1484     * Check if values in the column "property" are written to the bundle descriptor.
1485     * @param property the property id of the table column.
1486     * @return a flag, indicating if values of the table column are stored to the bundle descriptor.
1487     */
1488    private boolean isDescriptorProperty(Object property) {
1489
1490        return (getBundleType().equals(BundleType.DESCRIPTOR)
1491            || (hasDescriptor()
1492                && (property.equals(TableProperty.KEY)
1493                    || property.equals(TableProperty.DEFAULT)
1494                    || property.equals(TableProperty.DESCRIPTION))));
1495    }
1496
1497    /**
1498     * Loads all localizations not already loaded.
1499     * @throws CmsException thrown if locking a file fails.
1500     * @throws UnsupportedEncodingException thrown if reading  a file fails.
1501     * @throws IOException thrown if reading  a file fails.
1502     */
1503    private void loadAllRemainingLocalizations() throws CmsException, UnsupportedEncodingException, IOException {
1504
1505        if (!m_alreadyLoadedAllLocalizations) {
1506            // is only necessary for property bundles
1507            if (m_bundleType.equals(BundleType.PROPERTY)) {
1508                for (Locale l : m_locales) {
1509                    if (null == m_localizations.get(l)) {
1510                        CmsResource resource = m_bundleFiles.get(l);
1511                        if (resource != null) {
1512                            CmsFile file = m_cms.readFile(resource);
1513                            m_bundleFiles.put(l, file);
1514                            SortedProperties props = new SortedProperties();
1515                            props.load(
1516                                new InputStreamReader(
1517                                    new ByteArrayInputStream(file.getContents()),
1518                                    CmsFileUtil.getEncoding(m_cms, file)));
1519                            m_localizations.put(l, props);
1520                        }
1521                    }
1522                }
1523            }
1524            if (m_bundleType.equals(BundleType.XML)) {
1525                for (Locale l : m_locales) {
1526                    if (null == m_localizations.get(l)) {
1527                        loadLocalizationFromXmlBundle(l);
1528                    }
1529                }
1530            }
1531            m_alreadyLoadedAllLocalizations = true;
1532        }
1533
1534    }
1535
1536    /**
1537     * Loads the propertyvfsbundle for the provided locale.
1538     * If the bundle file is not present, it will be created.
1539     * @param locale the locale for which the localization should be loaded
1540     *
1541     * @throws IOException thrown if loading fails.
1542     * @throws CmsException thrown if reading or creation fails.
1543     */
1544    private void loadLocalizationFromPropertyBundle(Locale locale) throws IOException, CmsException {
1545
1546        // may throw exception again
1547        String sitePath = m_sitepath + m_basename + "_" + locale.toString();
1548        CmsResource resource = null;
1549        if (m_cms.existsResource(sitePath)) {
1550            resource = m_cms.readResource(sitePath);
1551            if (!OpenCms.getResourceManager().getResourceType(resource).getTypeName().equals(
1552                CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString())) {
1553                throw new CmsException(
1554                    new CmsMessageContainer(
1555                        Messages.get(),
1556                        Messages.ERR_RESOURCE_HAS_WRONG_TYPE_2,
1557                        locale.getDisplayName(),
1558                        resource.getRootPath()));
1559            }
1560        } else {
1561            resource = m_cms.createResource(
1562                sitePath,
1563                OpenCms.getResourceManager().getResourceType(
1564                    CmsMessageBundleEditorTypes.BundleType.PROPERTY.toString()));
1565            LockedFile lf = LockedFile.lockResource(m_cms, resource);
1566            lf.setCreated(true);
1567            m_lockedBundleFiles.put(locale, lf);
1568        }
1569        m_bundleFiles.put(locale, resource);
1570        SortedProperties props = new SortedProperties();
1571        props.load(
1572            new InputStreamReader(
1573                new ByteArrayInputStream(m_cms.readFile(resource).getContents()),
1574                CmsFileUtil.getEncoding(m_cms, resource)));
1575        m_localizations.put(locale, props);
1576
1577    }
1578
1579    /**
1580     * Loads the localization for the current locale from a bundle of type xmlvfsbundle.
1581     * It assumes, the content has already been unmarshalled before.
1582     * @param locale the locale for which the localization should be loaded
1583     */
1584    private void loadLocalizationFromXmlBundle(Locale locale) {
1585
1586        CmsXmlContentValueSequence messages = m_xmlBundle.getValueSequence("Message", locale);
1587        SortedProperties props = new SortedProperties();
1588        if (null != messages) {
1589            for (I_CmsXmlContentValue msg : messages.getValues()) {
1590                String msgpath = msg.getPath();
1591                props.put(
1592                    m_xmlBundle.getStringValue(m_cms, msgpath + "/Key", locale),
1593                    m_xmlBundle.getStringValue(m_cms, msgpath + "/Value", locale));
1594            }
1595        }
1596        m_localizations.put(locale, props);
1597    }
1598
1599    /**
1600     * Locks all files of the currently edited bundle (that contain the provided key if it is not null).
1601     * @param key the key that must be contained in the localization to lock. If null, the files for all localizations are locked.
1602     * @throws CmsException thrown if locking fails.
1603     */
1604    private void lockAllLocalizations(String key) throws CmsException {
1605
1606        for (Locale l : m_bundleFiles.keySet()) {
1607            if ((null == l) || m_localizations.get(l).containsKey(key)) {
1608                lockLocalization(l);
1609            }
1610        }
1611    }
1612
1613    /**
1614     * Locks the bundle descriptor.
1615     * @throws CmsException thrown if locking fails.
1616     */
1617    private void lockDescriptor() throws CmsException {
1618
1619        if ((null == m_descFile) && (null != m_desc)) {
1620            m_descFile = LockedFile.lockResource(m_cms, m_desc);
1621        }
1622    }
1623
1624    /**
1625     * Locks the bundle file that contains the translation for the provided locale.
1626     * @param l the locale for which the bundle file should be locked.
1627     * @throws CmsException thrown if locking fails.
1628     */
1629    private void lockLocalization(Locale l) throws CmsException {
1630
1631        if (null == m_lockedBundleFiles.get(l)) {
1632            LockedFile lf = LockedFile.lockResource(m_cms, m_bundleFiles.get(l));
1633            m_lockedBundleFiles.put(l, lf);
1634        }
1635
1636    }
1637
1638    /**
1639     * Lock a file lazily, if a value that should be written to the file has changed.
1640     * @param propertyId the table column in which the value has changed (e.g., KEY, TRANSLATION, ...)
1641     * @throws CmsException thrown if locking fails.
1642     */
1643    private void lockOnChange(Object propertyId) throws CmsException {
1644
1645        if (isDescriptorProperty(propertyId)) {
1646            lockDescriptor();
1647        } else {
1648            Locale l = m_bundleType.equals(BundleType.PROPERTY) ? m_locale : null;
1649            lockLocalization(l);
1650        }
1651
1652    }
1653
1654    /**
1655     * Remove a key for all language versions. If a descriptor is present, the key is only removed in the descriptor.
1656     *
1657     * @param key the key to remove.
1658     * @return <code>true</code> if removing was successful, <code>false</code> otherwise.
1659     */
1660    private boolean removeKeyForAllLanguages(String key) {
1661
1662        try {
1663            if (hasDescriptor()) {
1664                lockDescriptor();
1665            }
1666            loadAllRemainingLocalizations();
1667            lockAllLocalizations(key);
1668        } catch (CmsException | IOException e) {
1669            LOG.warn("Not able lock all localications for bundle.", e);
1670            return false;
1671        }
1672        if (!hasDescriptor()) {
1673
1674            for (Entry<Locale, SortedProperties> entry : m_localizations.entrySet()) {
1675                SortedProperties localization = entry.getValue();
1676                if (localization.containsKey(key)) {
1677                    localization.remove(key);
1678                    m_changedTranslations.add(entry.getKey());
1679                }
1680            }
1681        }
1682        return true;
1683    }
1684
1685    /**
1686     * Deletes the VFS XML bundle file.
1687     * @throws CmsException thrown if the delete operation fails.
1688     */
1689    private void removeXmlBundleFile() throws CmsException {
1690
1691        lockLocalization(null);
1692        m_cms.deleteResource(m_resource, CmsResource.DELETE_PRESERVE_SIBLINGS);
1693        m_resource = null;
1694
1695    }
1696
1697    /**
1698     * Rename a key for all languages.
1699     * @param oldKey the key to rename
1700     * @param newKey the new key name
1701     * @return <code>true</code> if renaming was successful, <code>false</code> otherwise.
1702     */
1703    private boolean renameKeyForAllLanguages(String oldKey, String newKey) {
1704
1705        try {
1706            loadAllRemainingLocalizations();
1707            lockAllLocalizations(oldKey);
1708            if (hasDescriptor()) {
1709                lockDescriptor();
1710            }
1711        } catch (CmsException | IOException e) {
1712            LOG.error(e.getLocalizedMessage(), e);
1713            return false;
1714        }
1715        for (Entry<Locale, SortedProperties> entry : m_localizations.entrySet()) {
1716            SortedProperties localization = entry.getValue();
1717            if (localization.containsKey(oldKey)) {
1718                String value = localization.getProperty(oldKey);
1719                localization.remove(oldKey);
1720                localization.put(newKey, value);
1721                m_changedTranslations.add(entry.getKey());
1722            }
1723        }
1724        if (hasDescriptor()) {
1725            CmsXmlContentValueSequence messages = m_descContent.getValueSequence(
1726                Descriptor.N_MESSAGE,
1727                Descriptor.LOCALE);
1728            for (int i = 0; i < messages.getElementCount(); i++) {
1729
1730                String prefix = messages.getValue(i).getPath() + "/";
1731                String key = m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(m_cms);
1732                if (key == oldKey) {
1733                    m_descContent.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).setStringValue(m_cms, newKey);
1734                    break;
1735                }
1736            }
1737            m_descriptorHasChanges = true;
1738        }
1739        m_keyset.renameKey(oldKey, newKey);
1740        return true;
1741    }
1742
1743    /**
1744     * Replaces the translations in an existing container with the translations for the provided locale.
1745     * @param locale the locale for which translations should be loaded.
1746     * @return <code>true</code> if replacing succeeded, <code>false</code> otherwise.
1747     */
1748    private boolean replaceValues(Locale locale) {
1749
1750        try {
1751            SortedProperties localization = getLocalization(locale);
1752            if (hasDescriptor()) {
1753                for (Object itemId : m_container.getItemIds()) {
1754                    Item item = m_container.getItem(itemId);
1755                    String key = item.getItemProperty(TableProperty.KEY).getValue().toString();
1756                    Object value = localization.get(key);
1757                    item.getItemProperty(TableProperty.TRANSLATION).setValue(null == value ? "" : value);
1758                }
1759            } else {
1760                m_container.removeAllItems();
1761                Set<Object> keyset = m_keyset.getKeySet();
1762                for (Object key : keyset) {
1763                    Object itemId = m_container.addItem();
1764                    Item item = m_container.getItem(itemId);
1765                    item.getItemProperty(TableProperty.KEY).setValue(key);
1766                    Object value = localization.get(key);
1767                    item.getItemProperty(TableProperty.TRANSLATION).setValue(null == value ? "" : value);
1768                }
1769                if (m_container.getItemIds().isEmpty()) {
1770                    m_container.addItem();
1771                }
1772            }
1773            return true;
1774        } catch (IOException | CmsException e) {
1775            // The problem should typically be a problem with locking or reading the file containing the translation.
1776            // This should be reported in the editor, if false is returned here.
1777            return false;
1778        }
1779    }
1780
1781    /**
1782     * Resets all information on changes. I.e., the editor assumes no changes are present anymore.
1783     * Call this method after a successful save action.
1784     */
1785    private void resetChanges() {
1786
1787        m_changedTranslations.clear();
1788        m_descriptorHasChanges = false;
1789
1790    }
1791
1792    /**
1793     * Saves the current translations from the container to the respective localization.
1794     */
1795    private void saveLocalization() {
1796
1797        SortedProperties localization = new SortedProperties();
1798        for (Object itemId : m_container.getItemIds()) {
1799            Item item = m_container.getItem(itemId);
1800            String key = item.getItemProperty(TableProperty.KEY).getValue().toString();
1801            String value = item.getItemProperty(TableProperty.TRANSLATION).getValue().toString();
1802            if (!(key.isEmpty() || value.isEmpty())) {
1803                localization.put(key, value);
1804            }
1805        }
1806        m_keyset.updateKeySet(m_localizations.get(m_locale).keySet(), localization.keySet());
1807        m_localizations.put(m_locale, localization);
1808
1809    }
1810
1811    /**
1812     * Save the values to the bundle descriptor.
1813     * @throws CmsException thrown if saving fails.
1814     */
1815    private void saveToBundleDescriptor() throws CmsException {
1816
1817        if (null != m_descFile) {
1818            m_removeDescriptorOnCancel = false;
1819            updateBundleDescriptorContent();
1820            m_descFile.getFile().setContents(m_descContent.marshal());
1821            m_cms.writeFile(m_descFile.getFile());
1822        }
1823    }
1824
1825    /**
1826     * Saves messages to a propertyvfsbundle file.
1827     *
1828     * @throws CmsException thrown if writing to the file fails.
1829     */
1830    private void saveToPropertyVfsBundle() throws CmsException {
1831
1832        for (Locale l : m_changedTranslations) {
1833            SortedProperties props = m_localizations.get(l);
1834            LockedFile f = m_lockedBundleFiles.get(l);
1835            if ((null != props) && (null != f)) {
1836                try {
1837                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1838                    Writer writer = new OutputStreamWriter(outputStream, f.getEncoding());
1839                    props.store(writer, null);
1840                    byte[] contentBytes = outputStream.toByteArray();
1841                    CmsFile file = f.getFile();
1842                    file.setContents(contentBytes);
1843                    String contentEncodingProperty = m_cms.readPropertyObject(
1844                        file,
1845                        CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
1846                        false).getValue();
1847                    if ((null == contentEncodingProperty) || !contentEncodingProperty.equals(f.getEncoding())) {
1848                        m_cms.writePropertyObject(
1849                            m_cms.getSitePath(file),
1850                            new CmsProperty(
1851                                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
1852                                f.getEncoding(),
1853                                f.getEncoding()));
1854                    }
1855                    m_cms.writeFile(file);
1856                } catch (IOException e) {
1857                    LOG.error(
1858                        Messages.get().getBundle().key(
1859                            Messages.ERR_READING_FILE_UNSUPPORTED_ENCODING_2,
1860                            f.getFile().getRootPath(),
1861                            f.getEncoding()),
1862                        e);
1863
1864                }
1865            }
1866        }
1867    }
1868
1869    /**
1870     * Saves messages to a xmlvfsbundle file.
1871     *
1872     * @throws CmsException thrown if writing to the file fails.
1873     */
1874    private void saveToXmlVfsBundle() throws CmsException {
1875
1876        if (m_lockedBundleFiles.get(null) != null) { // If the file was not locked, no changes were made, i.e., storing is not necessary.
1877            for (Locale l : m_locales) {
1878                SortedProperties props = m_localizations.get(l);
1879                if (null != props) {
1880                    if (m_xmlBundle.hasLocale(l)) {
1881                        m_xmlBundle.removeLocale(l);
1882                    }
1883                    m_xmlBundle.addLocale(m_cms, l);
1884                    int i = 0;
1885                    List<Object> keys = new ArrayList<Object>(props.keySet());
1886                    Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
1887                    for (Object key : keys) {
1888                        if ((null != key) && !key.toString().isEmpty()) {
1889                            String value = props.getProperty(key.toString());
1890                            if (!value.isEmpty()) {
1891                                m_xmlBundle.addValue(m_cms, "Message", l, i);
1892                                i++;
1893                                m_xmlBundle.getValue("Message[" + i + "]/Key", l).setStringValue(m_cms, key.toString());
1894                                m_xmlBundle.getValue("Message[" + i + "]/Value", l).setStringValue(m_cms, value);
1895                            }
1896                        }
1897                    }
1898                }
1899                CmsFile bundleFile = m_lockedBundleFiles.get(null).getFile();
1900                bundleFile.setContents(m_xmlBundle.marshal());
1901                m_cms.writeFile(bundleFile);
1902            }
1903        }
1904    }
1905
1906    /** Extract site path, base name and locale from the resource opened with the editor. */
1907    private void setResourceInformation() {
1908
1909        String sitePath = m_cms.getSitePath(m_resource);
1910        int pathEnd = sitePath.lastIndexOf('/') + 1;
1911        String baseName = sitePath.substring(pathEnd);
1912        m_sitepath = sitePath.substring(0, pathEnd);
1913        switch (CmsMessageBundleEditorTypes.BundleType.toBundleType(
1914            OpenCms.getResourceManager().getResourceType(m_resource).getTypeName())) {
1915            case PROPERTY:
1916                String localeSuffix = CmsStringUtil.getLocaleSuffixForName(baseName);
1917                if ((null != localeSuffix) && !localeSuffix.isEmpty()) {
1918                    baseName = baseName.substring(
1919                        0,
1920                        baseName.lastIndexOf(localeSuffix) - (1 /* cut off trailing underscore, too*/));
1921                    m_locale = CmsLocaleManager.getLocale(localeSuffix);
1922                }
1923                if ((null == m_locale) || !m_locales.contains(m_locale)) {
1924                    m_switchedLocaleOnOpening = true;
1925                    m_locale = m_locales.iterator().next();
1926                }
1927                break;
1928            case XML:
1929                m_locale = OpenCms.getLocaleManager().getBestAvailableLocaleForXmlContent(
1930                    m_cms,
1931                    m_resource,
1932                    m_xmlBundle);
1933                break;
1934            case DESCRIPTOR:
1935                m_basename = baseName.substring(
1936                    0,
1937                    baseName.length() - CmsMessageBundleEditorTypes.Descriptor.POSTFIX.length());
1938                m_locale = new Locale("en");
1939                break;
1940            default:
1941                throw new IllegalArgumentException(
1942                    Messages.get().container(
1943                        Messages.ERR_UNSUPPORTED_BUNDLE_TYPE_1,
1944                        CmsMessageBundleEditorTypes.BundleType.toBundleType(
1945                            OpenCms.getResourceManager().getResourceType(m_resource).getTypeName())).toString());
1946        }
1947        m_basename = baseName;
1948
1949    }
1950
1951    /**
1952     * Unmarshals the descriptor content.
1953     *
1954     * @throws CmsXmlException thrown if the XML structure of the descriptor is wrong.
1955     * @throws CmsException thrown if reading the descriptor file fails.
1956     */
1957    private void unmarshalDescriptor() throws CmsXmlException, CmsException {
1958
1959        if (null != m_desc) {
1960
1961            // unmarshal descriptor
1962            m_descContent = CmsXmlContentFactory.unmarshal(m_cms, m_cms.readFile(m_desc));
1963
1964            // configure messages if wanted
1965            CmsProperty bundleProp = m_cms.readPropertyObject(m_desc, PROPERTY_BUNDLE_DESCRIPTOR_LOCALIZATION, true);
1966            if (!(bundleProp.isNullProperty() || bundleProp.getValue().trim().isEmpty())) {
1967                m_configuredBundle = bundleProp.getValue();
1968            }
1969        }
1970
1971    }
1972
1973    /**
1974    * Update the descriptor content with values from the editor.
1975    * @throws CmsXmlException thrown if update fails due to a wrong XML structure (should never happen)
1976    */
1977    private void updateBundleDescriptorContent() throws CmsXmlException {
1978
1979        if (m_descContent.hasLocale(Descriptor.LOCALE)) {
1980            m_descContent.removeLocale(Descriptor.LOCALE);
1981        }
1982        m_descContent.addLocale(m_cms, Descriptor.LOCALE);
1983
1984        int i = 0;
1985        Property<Object> descProp;
1986        String desc;
1987        Property<Object> defaultValueProp;
1988        String defaultValue;
1989        Map<String, Item> keyItemMap = getKeyItemMap();
1990        List<String> keys = new ArrayList<String>(keyItemMap.keySet());
1991        Collections.sort(keys, CmsCaseInsensitiveStringComparator.getInstance());
1992        for (Object key : keys) {
1993            if ((null != key) && !key.toString().isEmpty()) {
1994
1995                m_descContent.addValue(m_cms, Descriptor.N_MESSAGE, Descriptor.LOCALE, i);
1996                i++;
1997                String messagePrefix = Descriptor.N_MESSAGE + "[" + i + "]/";
1998
1999                m_descContent.getValue(messagePrefix + Descriptor.N_KEY, Descriptor.LOCALE).setStringValue(
2000                    m_cms,
2001                    (String)key);
2002                descProp = keyItemMap.get(key).getItemProperty(TableProperty.DESCRIPTION);
2003                if ((null != descProp) && (null != descProp.getValue())) {
2004                    desc = descProp.getValue().toString();
2005                    m_descContent.getValue(messagePrefix + Descriptor.N_DESCRIPTION, Descriptor.LOCALE).setStringValue(
2006                        m_cms,
2007                        desc);
2008                }
2009
2010                defaultValueProp = keyItemMap.get(key).getItemProperty(TableProperty.DEFAULT);
2011                if ((null != defaultValueProp) && (null != defaultValueProp.getValue())) {
2012                    defaultValue = defaultValueProp.getValue().toString();
2013                    m_descContent.getValue(messagePrefix + Descriptor.N_DEFAULT, Descriptor.LOCALE).setStringValue(
2014                        m_cms,
2015                        defaultValue);
2016                }
2017
2018            }
2019        }
2020
2021    }
2022
2023    /**
2024     * Clears all internal lock info.
2025     */
2026    private void updateLockInformation() {
2027
2028        m_lockedBundleFiles.clear();
2029        m_descFile = null;
2030
2031    }
2032}