001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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.FocusEvent; 059import com.vaadin.event.FieldEvents.FocusListener; 060import com.vaadin.event.ShortcutAction; 061import com.vaadin.ui.Button; 062import com.vaadin.ui.Button.ClickEvent; 063import com.vaadin.ui.Button.ClickListener; 064import com.vaadin.ui.Component; 065import com.vaadin.ui.Notification; 066import com.vaadin.ui.Notification.Type; 067import com.vaadin.ui.UI; 068import com.vaadin.v7.data.Container; 069import com.vaadin.v7.data.Property; 070import com.vaadin.v7.data.Property.ValueChangeEvent; 071import com.vaadin.v7.data.Property.ValueChangeListener; 072import com.vaadin.v7.data.validator.AbstractStringValidator; 073import com.vaadin.v7.ui.AbstractTextField; 074import com.vaadin.v7.ui.DefaultFieldFactory; 075import com.vaadin.v7.ui.Field; 076import com.vaadin.v7.ui.HorizontalLayout; 077import com.vaadin.v7.ui.Table; 078import com.vaadin.v7.ui.Table.CellStyleGenerator; 079import com.vaadin.v7.ui.TextArea; 080import com.vaadin.v7.ui.TextField; 081 082/** Types and helper classes used by the message bundle editor. */ 083public final class CmsMessageBundleEditorTypes { 084 085 /** Types of bundles editable by the Editor. */ 086 public enum BundleType { 087 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 /** Flag, indicating if change events should be handled. */ 823 private boolean m_shouldHandleChanges = true; 824 /** Mapping from column -> row -> AbstractTextField. */ 825 private final Map<Integer, Map<Integer, AbstractTextField>> m_valueFields; 826 /** The editable columns. */ 827 private final List<TableProperty> m_editableColumns; 828 /** Reference to the table, the factory is used for. */ 829 final FilterTable m_table; 830 /** Registered key change listeners. */ 831 private final Set<I_EntryChangeListener> m_keyChangeListeners = new HashSet<I_EntryChangeListener>(); 832 833 /** 834 * Default constructor. 835 * @param table The table, the factory is used for. 836 * @param editableColumns the property names of the editable columns of the table. 837 */ 838 public TranslateTableFieldFactory(FilterTable table, List<TableProperty> editableColumns) { 839 840 m_table = table; 841 m_valueFields = new HashMap<Integer, Map<Integer, AbstractTextField>>(); 842 m_editableColumns = editableColumns; 843 } 844 845 /** 846 * @see com.vaadin.ui.TableFieldFactory#createField(com.vaadin.data.Container, java.lang.Object, java.lang.Object, com.vaadin.ui.Component) 847 */ 848 @Override 849 public Field<?> createField( 850 final Container container, 851 final Object itemId, 852 final Object propertyId, 853 Component uiContext) { 854 855 final TableProperty pid = (TableProperty)propertyId; 856 857 for (int i = 1; i <= m_editableColumns.size(); i++) { 858 if (pid.equals(m_editableColumns.get(i - 1))) { 859 860 final AbstractTextField tf; 861 if (pid.equals(TableProperty.KEY)) { 862 tf = new TextField(); 863 tf.addValidator(new KeyValidator()); 864 } else { 865 TextArea atf = new TextArea(); 866 atf.setRows(1); 867 CmsAutoGrowingTextArea.addTo(atf, 20); 868 tf = atf; 869 } 870 tf.setWidth("100%"); 871 tf.setResponsive(true); 872 873 tf.setInputPrompt( 874 Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_PLEASE_ADD_VALUE_0)); 875 tf.setData( 876 new ComponentData( 877 i, 878 itemId, 879 (String)container.getItem(itemId).getItemProperty(propertyId).getValue())); 880 if (!m_valueFields.containsKey(Integer.valueOf(i))) { 881 m_valueFields.put(Integer.valueOf(i), new HashMap<Integer, AbstractTextField>()); 882 } 883 m_valueFields.get(Integer.valueOf(i)).put((Integer)itemId, tf); 884 tf.addFocusListener(new FocusListener() { 885 886 public void focus(FocusEvent event) { 887 888 if (!m_table.isSelected(itemId)) { 889 m_table.select(itemId); 890 } 891 AbstractTextField field = (AbstractTextField)event.getComponent(); 892 // Update last value 893 ComponentData data = (ComponentData)field.getData(); 894 data.setLastValue(field.getValue()); 895 field.setData(data); 896 } 897 898 }); 899 tf.addValueChangeListener(new ValueChangeListener() { 900 901 @Override 902 public void valueChange(ValueChangeEvent event) { 903 904 if (((TranslateTableFieldFactory)m_table.getTableFieldFactory()).shouldHandleChanges()) { 905 ComponentData data = (ComponentData)tf.getData(); 906 String newValue = (String)event.getProperty().getValue(); 907 if (!data.getLastValue().equals(newValue)) { 908 EntryChangeEvent ev = new EntryChangeEvent( 909 tf, 910 data.getItemId(), 911 pid, 912 data.getLastValue(), 913 newValue); 914 fireKeyChangeEvent(ev); 915 } 916 } 917 } 918 919 }); 920 921 return tf; 922 } 923 } 924 return null; 925 926 } 927 928 /** 929 * Returns the editable columns. 930 * @return the editable columns. 931 */ 932 public List<TableProperty> getEditableColumns() { 933 934 return m_editableColumns; 935 } 936 937 /** 938 * Returns the mapping from the position in the table to the TextField. 939 * @return the mapping from the position in the table to the TextField. 940 */ 941 public Map<Integer, Map<Integer, AbstractTextField>> getValueFields() { 942 943 return m_valueFields; 944 } 945 946 /** 947 * Register a key change listener. 948 * @param listener the listener to register. 949 */ 950 public void registerKeyChangeListener(final I_EntryChangeListener listener) { 951 952 m_keyChangeListeners.add(listener); 953 } 954 955 /** 956 * Set the should handle changes flag. 957 * 958 * When doing programmatic changes that are not triggered by the 959 * editing person, handling changes should be disabled. 960 * 961 * @param shouldHandle set to true, iff changes should be handled. 962 */ 963 public void setShouldHandleChanges(boolean shouldHandle) { 964 965 m_shouldHandleChanges = shouldHandle; 966 } 967 968 /** 969 * @return a flag, indicating if changes should be handled. 970 */ 971 public boolean shouldHandleChanges() { 972 973 return m_shouldHandleChanges; 974 } 975 976 /** 977 * Called to fire a key change event. 978 * @param ev the event to fire. 979 */ 980 void fireKeyChangeEvent(final EntryChangeEvent ev) { 981 982 for (I_EntryChangeListener listener : m_keyChangeListeners) { 983 listener.handleEntryChange(ev); 984 } 985 } 986 } 987 988 /** The log object for this class. */ 989 static final Log LOG = CmsLog.getLog(CmsMessageBundleEditorTypes.class); 990 991 /** The width of the options column in pixel. */ 992 public static final int OPTION_COLUMN_WIDTH = 42; 993 994 /** The width of the options column in pixel. */ 995 public static final String OPTION_COLUMN_WIDTH_PX = OPTION_COLUMN_WIDTH + "px"; 996 997 /** Hide default constructor. */ 998 private CmsMessageBundleEditorTypes() { 999 1000 //noop 1001 } 1002 1003 /** 1004 * Returns the bundle descriptor for the bundle with the provided base name. 1005 * @param cms {@link CmsObject} used for searching. 1006 * @param basename the bundle base name, for which the descriptor is searched. 1007 * @return the bundle descriptor, or <code>null</code> if it does not exist or searching fails. 1008 */ 1009 public static CmsResource getDescriptor(CmsObject cms, String basename) { 1010 1011 CmsSolrQuery query = new CmsSolrQuery(); 1012 query.setResourceTypes(CmsMessageBundleEditorTypes.BundleType.DESCRIPTOR.toString()); 1013 query.setFilterQueries("filename:\"" + basename + CmsMessageBundleEditorTypes.Descriptor.POSTFIX + "\""); 1014 query.add("fl", "path"); 1015 CmsSolrResultList results; 1016 try { 1017 boolean isOnlineProject = cms.getRequestContext().getCurrentProject().isOnlineProject(); 1018 String indexName = isOnlineProject 1019 ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE 1020 : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE; 1021 results = OpenCms.getSearchManager().getIndexSolr(indexName).search(cms, query, true, null, true, null); 1022 } catch (CmsSearchException e) { 1023 LOG.error(Messages.get().getBundle().key(Messages.ERR_BUNDLE_DESCRIPTOR_SEARCH_ERROR_0), e); 1024 return null; 1025 } 1026 1027 switch (results.size()) { 1028 case 0: 1029 return null; 1030 case 1: 1031 return results.get(0); 1032 default: 1033 String files = ""; 1034 for (CmsResource res : results) { 1035 files += " " + res.getRootPath(); 1036 } 1037 LOG.warn(Messages.get().getBundle().key(Messages.ERR_BUNDLE_DESCRIPTOR_NOT_UNIQUE_1, files)); 1038 return results.get(0); 1039 } 1040 } 1041 1042 /** 1043 * Displays a localized warning. 1044 * @param caption the caption of the warning. 1045 * @param description the description of the warning. 1046 */ 1047 static void showWarning(final String caption, final String description) { 1048 1049 Notification warning = new Notification(caption, description, Type.WARNING_MESSAGE, true); 1050 warning.setDelayMsec(-1); 1051 warning.show(UI.getCurrent().getPage()); 1052 1053 } 1054}