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.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.i18n.CmsMessages;
033import org.opencms.main.CmsLog;
034import org.opencms.main.OpenCms;
035import org.opencms.search.CmsSearchException;
036import org.opencms.search.solr.CmsSolrIndex;
037import org.opencms.search.solr.CmsSolrQuery;
038import org.opencms.search.solr.CmsSolrResultList;
039import org.opencms.ui.FontOpenCms;
040import org.opencms.ui.components.extensions.CmsAutoGrowingTextArea;
041
042import java.io.Serializable;
043import java.util.ArrayList;
044import java.util.Collection;
045import java.util.HashMap;
046import java.util.HashSet;
047import java.util.List;
048import java.util.Locale;
049import java.util.Map;
050import java.util.Set;
051
052import org.apache.commons.logging.Log;
053
054import org.tepi.filtertable.FilterTable;
055
056import com.vaadin.event.Action;
057import com.vaadin.event.Action.Handler;
058import com.vaadin.event.FieldEvents.BlurEvent;
059import com.vaadin.event.FieldEvents.BlurListener;
060import com.vaadin.event.FieldEvents.FocusEvent;
061import com.vaadin.event.FieldEvents.FocusListener;
062import com.vaadin.event.ShortcutAction;
063import com.vaadin.ui.Button;
064import com.vaadin.ui.Button.ClickEvent;
065import com.vaadin.ui.Button.ClickListener;
066import com.vaadin.ui.Component;
067import com.vaadin.ui.Notification;
068import com.vaadin.ui.Notification.Type;
069import com.vaadin.ui.UI;
070import com.vaadin.v7.data.Container;
071import com.vaadin.v7.data.Property;
072import com.vaadin.v7.data.Property.ValueChangeEvent;
073import com.vaadin.v7.data.validator.AbstractStringValidator;
074import com.vaadin.v7.ui.AbstractTextField;
075import com.vaadin.v7.ui.DefaultFieldFactory;
076import com.vaadin.v7.ui.Field;
077import com.vaadin.v7.ui.HorizontalLayout;
078import com.vaadin.v7.ui.Table;
079import com.vaadin.v7.ui.Table.CellStyleGenerator;
080import com.vaadin.v7.ui.TextArea;
081import com.vaadin.v7.ui.TextField;
082
083/** Types and helper classes used by the message bundle editor. */
084public final class CmsMessageBundleEditorTypes {
085
086    /** Types of bundles editable by the Editor. */
087    public enum BundleType {
088        /** A bundle of type propertyvfsbundle. */
089        PROPERTY,
090        /** A bundle of type xmlvfsbundle. */
091        XML,
092        /** A bundle descriptor. */
093        DESCRIPTOR;
094
095        /**
096         * An adjusted version of what is typically Enum.valueOf().
097         * @param value the resource type name that should be transformed into BundleType
098         * @return The bundle type for the resource type name, or null, if the resource has no bundle type.
099         */
100        public static BundleType toBundleType(String value) {
101
102            if (null == value) {
103                return null;
104            }
105            if (value.equals(PROPERTY.toString())) {
106                return PROPERTY;
107            }
108            if (value.equals(XML.toString())) {
109                return XML;
110            }
111            if (value.equals(DESCRIPTOR.toString())) {
112                return DESCRIPTOR;
113            }
114
115            return null;
116        }
117
118        /**
119         * @see java.lang.Enum#toString()
120         */
121        @Override
122        public String toString() {
123
124            switch (this) {
125                case PROPERTY:
126                    return "propertyvfsbundle";
127                case XML:
128                    return "xmlvfsbundle";
129                case DESCRIPTOR:
130                    return "bundledescriptor";
131                default:
132                    throw new IllegalArgumentException();
133            }
134        }
135    }
136
137    /** Helper for accessing Bundle descriptor XML contents. */
138    public static final class Descriptor {
139
140        /** Message node. */
141        public static final String N_MESSAGE = "Message";
142        /** Key node. */
143        public static final String N_KEY = "Key";
144        /** Description node. */
145        public static final String N_DESCRIPTION = "Description";
146        /** Default node. */
147        public static final String N_DEFAULT = "Default";
148        /** Locale in which the content is available. */
149        public static final Locale LOCALE = new Locale("en");
150        /** The mandatory postfix of a bundle descriptor. */
151        public static final String POSTFIX = "_desc";
152
153    }
154
155    /** The propertyIds of the table columns. */
156    public enum TableProperty {
157        /** Table column with the message key. */
158        KEY,
159        /** Table column with the message description. */
160        DESCRIPTION,
161        /** Table column with the message's default value. */
162        DEFAULT,
163        /** Table column with the current (language specific) translation of the message. */
164        TRANSLATION,
165        /** Table column with the options (add, delete). */
166        OPTIONS
167    }
168
169    /**
170     * Data stored for each editable field in the message table.
171     */
172    static class ComponentData implements Serializable {
173
174        /** Serialization id. */
175        private static final long serialVersionUID = 1L;
176
177        /** Id of the editable column. */
178        private int m_editableId;
179        /** Id of the table row. */
180        private Object m_itemId;
181        /** The value in the field when it gets the focus, i.e., before a current edit operation. */
182        private String m_lastValue;
183
184        /**
185         * Default constructor.
186         *
187         * @param editableId id of the editable column.
188         * @param itemId id of the table row.
189         * @param lastValue the value in the field when it gets the focus, i.e., before a current edit operation.
190         */
191        public ComponentData(int editableId, Object itemId, String lastValue) {
192
193            m_editableId = editableId;
194            m_itemId = itemId;
195            m_lastValue = lastValue;
196        }
197
198        /**
199         * Returns the editable column id.
200         * @return the editable column id.
201         */
202        public int getEditableColumnId() {
203
204            return m_editableId;
205        }
206
207        /**
208         * Returns the id of the table row.
209         * @return the id of the table row.
210         */
211        public Object getItemId() {
212
213            return m_itemId;
214        }
215
216        /**
217         * Returns the last value in the field (before the current edit operation).
218         * @return the last value in the field (before the current edit operation).
219         */
220        public String getLastValue() {
221
222            return m_lastValue;
223        }
224
225        /**
226         * Set the last value in the field. Do this when the field is focused.
227         * @param lastValue the last value in the field.
228         */
229        public void setLastValue(String lastValue) {
230
231            m_lastValue = lastValue;
232        }
233    }
234
235    /** The different edit modes. */
236    enum EditMode {
237        /** Editing the messages and the descriptor. */
238        MASTER,
239        /** Only editing messages. */
240        DEFAULT
241    }
242
243    /**
244     * The editor state holds the information on what columns of the editors table
245     * should be editable and if the options column should be shown.
246     * The state depends on the loaded bundle and the edit mode.
247     */
248    static class EditorState {
249
250        /** The editable columns (from left to right).*/
251        private List<TableProperty> m_editableColumns;
252        /** Flag, indicating if the options column should be shown. */
253        private boolean m_showOptions;
254
255        /** Constructor, setting all the state information directly.
256         * @param editableColumns the property ids of the editable columns (from left to right)
257         * @param showOptions flag, indicating if the options column should be shown.
258         */
259        public EditorState(List<TableProperty> editableColumns, boolean showOptions) {
260
261            m_editableColumns = editableColumns;
262            m_showOptions = showOptions;
263        }
264
265        /** Returns the editable columns from left to right (as there property ids).
266         * @return the editable columns from left to right (as there property ids).
267         */
268        public List<TableProperty> getEditableColumns() {
269
270            return m_editableColumns;
271        }
272
273        /** Returns a flag, indicating if the options column should be shown.
274         * @return a flag, indicating if the options column should be shown.
275         */
276        public boolean isShowOptions() {
277
278            return m_showOptions;
279        }
280    }
281
282    /** Key change event. */
283    static class EntryChangeEvent {
284
285        /** The field via which the key was edited. */
286        private AbstractTextField m_source;
287        /** The item id of the table row in which the key was edited. */
288        private Object m_itemId;
289        /** The property id the table column in which the value was edited. */
290        private Object m_propertyId;
291        /** The value before it was edited. */
292        private String m_oldValue;
293        /** The value after it was edited. */
294        private String m_newValue;
295
296        /** Default constructor.
297         * @param source the field via which the entry was edited.
298         * @param itemId the item id of the table row in which the entry was edited.
299         * @param propertyId the property id of the table column in which the entry was edited
300         * @param oldKey the key before it was edited.
301         * @param newKey the key after it was edited.
302         */
303        public EntryChangeEvent(
304            AbstractTextField source,
305            Object itemId,
306            Object propertyId,
307            String oldKey,
308            String newKey) {
309
310            m_source = source;
311            m_itemId = itemId;
312            m_propertyId = propertyId;
313            m_oldValue = oldKey;
314            m_newValue = newKey;
315        }
316
317        /**
318         * Returns the item id of the table row in which the entry was edited.
319         * @return the item id of the table row in which the entry was edited.
320         */
321        public Object getItemId() {
322
323            return m_itemId;
324        }
325
326        /**
327         * Returns the value after it was edited.
328         * @return the value after it was edited.
329         */
330        public String getNewValue() {
331
332            return m_newValue;
333        }
334
335        /**
336         * Returns the value before it was edited.
337         * @return the value before it was edited.
338         */
339        public String getOldValue() {
340
341            return m_oldValue;
342        }
343
344        /**
345         * Returns the property id of the table column in which the entry was edited.
346         * @return the property id of the table column in which the entry was edited.
347         */
348        public Object getPropertyId() {
349
350            return m_propertyId;
351        }
352
353        /**
354         * Returns the field via which the entry was edited.
355         * @return the field via which the entry was edited.
356         */
357        public AbstractTextField getSource() {
358
359            return m_source;
360        }
361    }
362
363    /** Interface for a entry change handler. */
364    static interface I_EntryChangeListener {
365
366        /**
367         * Called when a entry change event is fired.
368         * @param event the entry change event.
369         */
370        void handleEntryChange(EntryChangeEvent event);
371    }
372
373    /** Interface for a item deletion listener. */
374    static interface I_ItemDeletionListener {
375
376        /**
377         * Called when an item deletion event is fired.
378         * @param e the event
379         * @return <code>true</code> if deletion handling was successful, <code>false</code> otherwise.
380         */
381        boolean handleItemDeletion(ItemDeletionEvent e);
382    }
383
384    /** Interface for an Listener for changes in the options. */
385    static interface I_OptionListener {
386
387        /**
388         * Handles adding a key.
389         * @param key the key to add.
390         * @return flag, indicating if the key was added. If not, it was already present.
391         */
392        boolean handleAddKey(String key);
393
394        /**
395         * Handles a language change.
396         * @param language the newly selected language.
397         */
398        void handleLanguageChange(Locale language);
399
400        /**
401         * Handles the change of the edit mode.
402         * @param mode the newly selected edit mode.
403         */
404        void handleModeChange(EditMode mode);
405
406    }
407
408    /** Item deletion event. */
409    static class ItemDeletionEvent {
410
411        /** The id of the deleted item. */
412        private Object m_itemId;
413
414        /** Default constructor.
415         * @param itemId the id of the deleted item.
416         */
417        public ItemDeletionEvent(Object itemId) {
418
419            m_itemId = itemId;
420        }
421
422        /**
423         * Returns the id of the deleted item.
424         * @return the id of the deleted item.
425         */
426        public Object getItemId() {
427
428            return m_itemId;
429        }
430
431    }
432
433    /** Manages the keys used in at least one locale. */
434    static final class KeySet {
435
436        /** Map from keys to the number of locales they are present. */
437        Map<Object, Integer> m_keyset;
438
439        /** Default constructor. */
440        public KeySet() {
441
442            m_keyset = new HashMap<Object, Integer>();
443        }
444
445        /**
446         * Returns the current key set.
447         * @return the current key set.
448         */
449        public Set<Object> getKeySet() {
450
451            return new HashSet<Object>(m_keyset.keySet());
452        }
453
454        /**
455         * Removes the given key.
456         * @param key the key to be removed.
457         */
458        public void removeKey(final String key) {
459
460            m_keyset.remove(key);
461        }
462
463        /**
464         * Rename a key.
465         * @param oldKey the current key name.
466         * @param newKey the substitution for the key name.
467         */
468        public void renameKey(String oldKey, String newKey) {
469
470            if (m_keyset.containsKey(oldKey) && !m_keyset.containsKey(newKey)) {
471                Integer count = m_keyset.get(oldKey);
472                m_keyset.remove(oldKey);
473                m_keyset.put(newKey, count);
474            } else {
475                //TODO: should never be the case, but handle it anyway?
476            }
477
478        }
479
480        /**
481         * Updates the set with all keys that are used in at least one language.
482         * @param oldKeys keys of a locale as registered before
483         * @param newKeys keys of the locale now
484         */
485        public void updateKeySet(Set<Object> oldKeys, Set<Object> newKeys) {
486
487            // Remove keys that are not present anymore
488            if (null != oldKeys) {
489                Set<Object> removedKeys = new HashSet<Object>(oldKeys);
490                if (null != newKeys) {
491                    removedKeys.removeAll(newKeys);
492                }
493                for (Object key : removedKeys) {
494                    Integer i = m_keyset.get(key);
495                    int uses = null != i ? i.intValue() : 0;
496                    if (uses > 1) {
497                        m_keyset.put(key, Integer.valueOf(uses - 1));
498                    } else if (uses == 1) {
499                        m_keyset.remove(key);
500                    }
501                }
502            }
503
504            // Add keys that are new
505            if (null != newKeys) {
506                Set<Object> addedKeys = new HashSet<Object>(newKeys);
507                if (null != oldKeys) {
508                    addedKeys.removeAll(oldKeys);
509                }
510                for (Object key : addedKeys) {
511                    if (m_keyset.containsKey(key)) {
512                        m_keyset.put(key, Integer.valueOf(m_keyset.get(key).intValue() + 1));
513                    } else {
514                        m_keyset.put(key, Integer.valueOf(1));
515                    }
516                }
517            }
518
519        }
520
521    }
522
523    /** Validates keys. */
524    @SuppressWarnings("serial")
525    static class KeyValidator extends AbstractStringValidator {
526
527        /**
528         * Default constructor.
529         */
530        public KeyValidator() {
531
532            super(Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_INVALID_KEY_0));
533
534        }
535
536        /**
537         * @see com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object)
538         */
539        @Override
540        protected boolean isValidValue(String value) {
541
542            if (null == value) {
543                return true;
544            }
545            return !value.matches(".*\\p{IsWhite_Space}.*");
546        }
547
548    }
549
550    /** A column generator that additionally adjusts the appearance of the options buttons to selection changes on the table. */
551    @SuppressWarnings("serial")
552    static class OptionColumnGenerator implements com.vaadin.v7.ui.Table.ColumnGenerator {
553
554        /** Map from itemId (row) -> option buttons in the row. */
555        Map<Object, Collection<Component>> m_buttons;
556        /** The id of the currently selected item (row). */
557        Object m_selectedItem;
558        /** The table, the column is generated for. */
559        FilterTable m_table;
560        /** The key deletion listeners. */
561        I_ItemDeletionListener m_listener;
562
563        /**
564         * Default constructor.
565         *
566         * @param table the table, for which the column is generated for.
567         */
568        public OptionColumnGenerator(final FilterTable table) {
569
570            m_buttons = new HashMap<Object, Collection<Component>>();
571            m_table = table;
572            m_table.addValueChangeListener(new Property.ValueChangeListener() {
573
574                public void valueChange(ValueChangeEvent event) {
575
576                    selectItem(m_table.getValue());
577                }
578            });
579
580        }
581
582        public Object generateCell(final Table source, final Object itemId, final Object columnId) {
583
584            CmsMessages messages = Messages.get().getBundle(UI.getCurrent().getLocale());
585            HorizontalLayout options = new HorizontalLayout();
586            Button delete = new Button();
587            delete.addStyleName("icon-only");
588            delete.addStyleName("borderless-colored");
589            delete.setDescription(messages.key(Messages.GUI_REMOVE_ROW_0));
590            delete.setIcon(FontOpenCms.CIRCLE_MINUS, messages.key(Messages.GUI_REMOVE_ROW_0));
591            delete.addClickListener(new ClickListener() {
592
593                public void buttonClick(ClickEvent event) {
594
595                    ItemDeletionEvent e = new ItemDeletionEvent(itemId);
596                    if ((null == m_listener) || m_listener.handleItemDeletion(e)) {
597                        m_table.removeItem(itemId);
598                    }
599                }
600            });
601
602            options.addComponent(delete);
603
604            Collection<Component> buttons = new ArrayList<Component>(1);
605            buttons.add(delete);
606            m_buttons.put(itemId, buttons);
607
608            if (source.isSelected(itemId)) {
609                selectItem(itemId);
610            }
611
612            return options;
613        }
614
615        /**
616         * Registers an item deletion listener. Only one listener can be registered.
617         * Registering a new listener will automatically unregister the previous one.
618         *
619         * @param listener the listener to register.
620         */
621        void registerItemDeletionListener(final I_ItemDeletionListener listener) {
622
623            m_listener = listener;
624        }
625
626        /**
627         * Call this method, when a new item is selected. It will adjust the style of the option buttons, thus that they stay visible.
628         *
629         * @param itemId the id of the newly selected item (row).
630         */
631        void selectItem(final Object itemId) {
632
633            if ((null != m_selectedItem) && (null != m_buttons.get(m_selectedItem))) {
634                for (Component button : m_buttons.get(m_selectedItem)) {
635                    button.removeStyleName("borderless");
636                    button.addStyleName("borderless-colored");
637
638                }
639            }
640            m_selectedItem = itemId;
641            if ((null != m_selectedItem) && (null != m_buttons.get(m_selectedItem))) {
642                for (Component button : m_buttons.get(m_selectedItem)) {
643                    button.removeStyleName("borderless-colored");
644                    button.addStyleName("borderless");
645                }
646            }
647        }
648
649    }
650
651    /** Handler to improve the keyboard navigation in the table. */
652    @SuppressWarnings("serial")
653    static class TableKeyboardHandler implements Handler {
654
655        /** The field factory keeps track of the editable rows and the row/col positions of the TextFields. */
656        private FilterTable m_table;
657
658        /** Tab was pressed. */
659        private Action m_tabNext = new ShortcutAction("Tab", ShortcutAction.KeyCode.TAB, null);
660        /** Tab+Shift was pressed. */
661        private Action m_tabPrev = new ShortcutAction(
662            "Shift+Tab",
663            ShortcutAction.KeyCode.TAB,
664            new int[] {ShortcutAction.ModifierKey.SHIFT});
665        /** Down was pressed. */
666        private Action m_curDown = new ShortcutAction("Down", ShortcutAction.KeyCode.ARROW_DOWN, null);
667        /** Up was pressed. */
668        private Action m_curUp = new ShortcutAction("Up", ShortcutAction.KeyCode.ARROW_UP, null);
669        /** Enter was pressed. */
670        private Action m_enter = new ShortcutAction("Enter", ShortcutAction.KeyCode.ENTER, null);
671
672        /**
673         * Shortcut-Handler to improve the navigation in the table component.
674         *
675         * @param table the table, the handler is attached to.
676         */
677        public TableKeyboardHandler(final FilterTable table) {
678
679            m_table = table;
680        }
681
682        /**
683         * @see com.vaadin.event.Action.Handler#getActions(java.lang.Object, java.lang.Object)
684         */
685        public Action[] getActions(Object target, Object sender) {
686
687            return new Action[] {m_tabNext, m_tabPrev, m_curDown, m_curUp, m_enter};
688        }
689
690        /**
691         * @see com.vaadin.event.Action.Handler#handleAction(com.vaadin.event.Action, java.lang.Object, java.lang.Object)
692         */
693        public void handleAction(Action action, Object sender, Object target) {
694
695            TranslateTableFieldFactory fieldFactory = (TranslateTableFieldFactory)m_table.getTableFieldFactory();
696            List<TableProperty> editableColums = fieldFactory.getEditableColumns();
697
698            if (target instanceof AbstractTextField) {
699                // Move according to keypress
700                ComponentData data = (ComponentData)(((AbstractTextField)target).getData());
701                // Abort if no data attribute found
702                if (null == data) {
703                    return;
704                }
705                int colId = data.getEditableColumnId();
706                Integer rowIdInteger = (Integer)data.getItemId();
707                @SuppressWarnings("boxing") // rowIdInteger should never be null
708                int rowId = Integer.valueOf(rowIdInteger);
709
710                // TODO: Find a better solution?
711                // NOTE: A collection is returned, but actually it's a linked list.
712                // It's a hack, but actually I don't know how to do better here.
713                @SuppressWarnings("unchecked")
714                List<Integer> visibleItemIds = (List<Integer>)m_table.getVisibleItemIds();
715
716                if ((action == m_curDown) || (action == m_enter)) {
717                    int currentRow = visibleItemIds.indexOf(Integer.valueOf(rowId));
718                    if (currentRow < (visibleItemIds.size() - 1)) {
719                        rowId = visibleItemIds.get(currentRow + 1).intValue();
720                    }
721                } else if (action == m_curUp) {
722                    int currentRow = visibleItemIds.indexOf(Integer.valueOf(rowId));
723                    if (currentRow > 0) {
724                        rowId = visibleItemIds.get(currentRow - 1).intValue();
725                    }
726                } else if (action == m_tabNext) {
727                    int nextColId = getNextColId(editableColums, colId);
728                    if (colId >= nextColId) {
729                        int currentRow = visibleItemIds.indexOf(Integer.valueOf(rowId));
730                        rowId = visibleItemIds.get((currentRow + 1) % visibleItemIds.size()).intValue();
731                    }
732                    colId = nextColId;
733                } else if (action == m_tabPrev) {
734                    int previousColId = getPreviousColId(editableColums, colId);
735                    if (colId <= previousColId) {
736                        int currentRow = visibleItemIds.indexOf(Integer.valueOf(rowId));
737                        rowId = visibleItemIds.get(
738                            ((currentRow + visibleItemIds.size()) - 1) % visibleItemIds.size()).intValue();
739                    }
740                    colId = previousColId;
741                }
742
743                AbstractTextField newTF = fieldFactory.getValueFields().get(Integer.valueOf(colId)).get(
744                    Integer.valueOf(rowId));
745                if (newTF != null) {
746                    newTF.focus();
747                }
748            }
749        }
750
751        /**
752         * Calculates the id of the next editable column.
753         * @param editableColumns all editable columns
754         * @param colId id (index in <code>editableColumns</code> plus 1) of the current column.
755         * @return id of the next editable column.
756         */
757        private int getNextColId(List<TableProperty> editableColumns, int colId) {
758
759            for (int i = colId % editableColumns.size(); i != (colId - 1); i = (i + 1) % editableColumns.size()) {
760                if (!m_table.isColumnCollapsed(editableColumns.get(i))) {
761                    return i + 1;
762                }
763            }
764            return colId;
765        }
766
767        /**
768         * Calculates the id of the previous editable column.
769         * @param editableColumns all editable columns
770         * @param colId id (index in <code>editableColumns</code> plus 1) of the current column.
771         * @return id of the previous editable column.
772         */
773        private int getPreviousColId(List<TableProperty> editableColumns, int colId) {
774
775            // use +4 instead of -1 to prevent negativ numbers
776            for (int i = ((colId + editableColumns.size()) - 2) % editableColumns.size(); i != (colId
777                - 1); i = ((i + editableColumns.size()) - 1) % editableColumns.size()) {
778                if (!m_table.isColumnCollapsed(editableColumns.get(i))) {
779                    return i + 1;
780                }
781            }
782            return colId;
783        }
784    }
785
786    /** Custom cell style generator to allow different style for editable columns. */
787    @SuppressWarnings("serial")
788    static class TranslateTableCellStyleGenerator implements CellStyleGenerator {
789
790        /** The editable columns. */
791        private List<TableProperty> m_editableColums;
792
793        /**
794         * Default constructor, taking the list of editable columns.
795         *
796         * @param editableColumns the list of editable columns.
797         */
798        public TranslateTableCellStyleGenerator(List<TableProperty> editableColumns) {
799
800            m_editableColums = editableColumns;
801            if (null == m_editableColums) {
802                m_editableColums = new ArrayList<TableProperty>();
803            }
804        }
805
806        /**
807         * @see com.vaadin.ui.CustomTable.CellStyleGenerator#getStyle(com.vaadin.ui.CustomTable, java.lang.Object, java.lang.Object)
808         */
809        public String getStyle(Table source, Object itemId, Object propertyId) {
810
811            String result = TableProperty.KEY.equals(propertyId) ? "key-" : "";
812            result += m_editableColums.contains(propertyId) ? "editable" : "fix";
813            return result;
814        }
815
816    }
817
818    /** TableFieldFactory for making only some columns editable and to support enhanced navigation. */
819    @SuppressWarnings("serial")
820    static class TranslateTableFieldFactory extends DefaultFieldFactory {
821
822        /** Mapping from column -> row -> AbstractTextField. */
823        private final Map<Integer, Map<Integer, AbstractTextField>> m_valueFields;
824        /** The editable columns. */
825        private final List<TableProperty> m_editableColumns;
826        /** Reference to the table, the factory is used for. */
827        final FilterTable m_table;
828        /** Registered key change listeners. */
829        private final Set<I_EntryChangeListener> m_keyChangeListeners = new HashSet<I_EntryChangeListener>();
830
831        /**
832         * Default constructor.
833         * @param table The table, the factory is used for.
834         * @param editableColumns the property names of the editable columns of the table.
835         */
836        public TranslateTableFieldFactory(FilterTable table, List<TableProperty> editableColumns) {
837
838            m_table = table;
839            m_valueFields = new HashMap<Integer, Map<Integer, AbstractTextField>>();
840            m_editableColumns = editableColumns;
841        }
842
843        /**
844         * @see com.vaadin.ui.TableFieldFactory#createField(com.vaadin.data.Container, java.lang.Object, java.lang.Object, com.vaadin.ui.Component)
845         */
846        @Override
847        public Field<?> createField(
848            final Container container,
849            final Object itemId,
850            final Object propertyId,
851            Component uiContext) {
852
853            final TableProperty pid = (TableProperty)propertyId;
854
855            for (int i = 1; i <= m_editableColumns.size(); i++) {
856                if (pid.equals(m_editableColumns.get(i - 1))) {
857
858                    AbstractTextField tf;
859                    if (pid.equals(TableProperty.KEY)) {
860                        tf = new TextField();
861                        tf.addValidator(new KeyValidator());
862                    } else {
863                        TextArea atf = new TextArea();
864                        atf.setRows(1);
865                        CmsAutoGrowingTextArea.addTo(atf, 20);
866                        tf = atf;
867                    }
868                    tf.setWidth("100%");
869                    tf.setResponsive(true);
870
871                    tf.setInputPrompt(
872                        Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_PLEASE_ADD_VALUE_0));
873                    tf.setData(new ComponentData(i, itemId, ""));
874                    if (!m_valueFields.containsKey(Integer.valueOf(i))) {
875                        m_valueFields.put(Integer.valueOf(i), new HashMap<Integer, AbstractTextField>());
876                    }
877                    m_valueFields.get(Integer.valueOf(i)).put((Integer)itemId, tf);
878                    tf.addFocusListener(new FocusListener() {
879
880                        public void focus(FocusEvent event) {
881
882                            if (!m_table.isSelected(itemId)) {
883                                m_table.select(itemId);
884                            }
885                            AbstractTextField field = (AbstractTextField)event.getComponent();
886                            // Update last value
887                            ComponentData data = (ComponentData)field.getData();
888                            data.setLastValue(field.getValue());
889                            field.setData(data);
890                        }
891
892                    });
893                    tf.addBlurListener(new BlurListener() {
894
895                        public void blur(BlurEvent event) {
896
897                            AbstractTextField field = (AbstractTextField)event.getComponent();
898                            ComponentData data = (ComponentData)field.getData();
899                            if (!data.getLastValue().equals(field.getValue())) {
900                                EntryChangeEvent ev = new EntryChangeEvent(
901                                    field,
902                                    data.getItemId(),
903                                    pid,
904                                    data.getLastValue(),
905                                    field.getValue());
906                                fireKeyChangeEvent(ev);
907                            }
908                        }
909                    });
910                    return tf;
911                }
912            }
913            return null;
914
915        }
916
917        /**
918         * Returns the editable columns.
919         * @return the editable columns.
920         */
921        public List<TableProperty> getEditableColumns() {
922
923            return m_editableColumns;
924        }
925
926        /**
927         * Returns the mapping from the position in the table to the TextField.
928         * @return the mapping from the position in the table to the TextField.
929         */
930        public Map<Integer, Map<Integer, AbstractTextField>> getValueFields() {
931
932            return m_valueFields;
933        }
934
935        /**
936         * Register a key change listener.
937         * @param listener the listener to register.
938         */
939        public void registerKeyChangeListener(final I_EntryChangeListener listener) {
940
941            m_keyChangeListeners.add(listener);
942        }
943
944        /**
945         * Called to fire a key change event.
946         * @param ev the event to fire.
947         */
948        void fireKeyChangeEvent(final EntryChangeEvent ev) {
949
950            for (I_EntryChangeListener listener : m_keyChangeListeners) {
951                listener.handleEntryChange(ev);
952            }
953        }
954    }
955
956    /** The log object for this class. */
957    static final Log LOG = CmsLog.getLog(CmsMessageBundleEditorTypes.class);
958
959    /** The width of the options column in pixel. */
960    public static final int OPTION_COLUMN_WIDTH = 42;
961
962    /** The width of the options column in pixel. */
963    public static final String OPTION_COLUMN_WIDTH_PX = OPTION_COLUMN_WIDTH + "px";
964
965    /** Hide default constructor. */
966    private CmsMessageBundleEditorTypes() {
967        //noop
968    }
969
970    /**
971     * Returns the bundle descriptor for the bundle with the provided base name.
972     * @param cms {@link CmsObject} used for searching.
973     * @param basename the bundle base name, for which the descriptor is searched.
974     * @return the bundle descriptor, or <code>null</code> if it does not exist or searching fails.
975     */
976    public static CmsResource getDescriptor(CmsObject cms, String basename) {
977
978        CmsSolrQuery query = new CmsSolrQuery();
979        query.setResourceTypes(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR.toString());
980        query.setFilterQueries("filename:\"" + basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX + "\"");
981        query.add("fl", "path");
982        CmsSolrResultList results;
983        try {
984            boolean isOnlineProject = cms.getRequestContext().getCurrentProject().isOnlineProject();
985            String indexName = isOnlineProject
986            ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE
987            : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE;
988            results = OpenCms.getSearchManager().getIndexSolr(indexName).search(cms, query, true, null, true, null);
989        } catch (CmsSearchException e) {
990            LOG.error(Messages.get().getBundle().key(Messages.ERR_BUNDLE_DESCRIPTOR_SEARCH_ERROR_0), e);
991            return null;
992        }
993
994        switch (results.size()) {
995            case 0:
996                return null;
997            case 1:
998                return results.get(0);
999            default:
1000                String files = "";
1001                for (CmsResource res : results) {
1002                    files += " " + res.getRootPath();
1003                }
1004                LOG.warn(Messages.get().getBundle().key(Messages.ERR_BUNDLE_DESCRIPTOR_NOT_UNIQUE_1, files));
1005                return results.get(0);
1006        }
1007    }
1008
1009    /**
1010     * Displays a localized warning.
1011     * @param caption the caption of the warning.
1012     * @param description the description of the warning.
1013     */
1014    static void showWarning(final String caption, final String description) {
1015
1016        Notification warning = new Notification(caption, description, Type.WARNING_MESSAGE, true);
1017        warning.setDelayMsec(-1);
1018        warning.show(UI.getCurrent().getPage());
1019
1020    }
1021}