All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.properties.BundleStructure Maven / Gradle / Ivy

There is a newer version: RELEASE230
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


package org.netbeans.modules.properties;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.openide.filesystems.FileObject;
import org.openide.loaders.MultiDataObject.Entry;
import org.openide.util.WeakListeners;


/**
 * Structure of a bundle of .properties files.
 * Provides structure of entries (one entry per one .properties file)
 * for one PropertiesDataObject.
 * 

* This structure provides support for sorting entries and fast mapping * of integers to entries. *

* The sorting support in this class is a design flaw - * consider it deprecated. * * @author Petr Jiricka */ public class BundleStructure { /** * PropertiesDataObject whose structure is described * by this object */ PropertiesDataObject obj; /** * file entries of the PropertiesDataObject. * The first entry always represents the primary file. * The other entries represent secondary files and are sorted * by the corresponding files' names. * * @see #updateEntries */ private PropertiesFileEntry[] entries; /** * sorted list of non-escaped keys from all entries * * @see #buildKeySet */ private List keyList; /** * Compartor which sorts keylist. * Default set is sort according keys in file order. */ private KeyComparator comparator = new KeyComparator(); /** * registry of PropertyBundleListeners and support * for firing PropertyBundleEvents. * Methods for registering and notification of listeners delegate to it. */ private PropertyBundleSupport propBundleSupport = new PropertyBundleSupport(this); /** listens to changes on the underlying PropertyDataObject */ private PropertyChangeListener propListener; protected BundleStructure() { obj = null; } /** * Creates a new instance describing a given * PropertiesDataObject. * * @param obj PropertiesDataObject to be desribed */ public BundleStructure(PropertiesDataObject obj) { this.obj = obj; updateEntries(); // Listen on the PropertiesDataObject. propListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals( PropertiesDataObject.PROP_FILES)) { updateEntries(); propBundleSupport.fireBundleStructureChanged(); } } }; obj.addPropertyChangeListener( WeakListeners.propertyChange(propListener, obj)); } /** * Retrieves n-th entry from the list, indexed from 0. * The first entry is always the primary entry. * * @param index index of entry to be retrieved, starting at 0 * @return entry at the specified index; * or null if the index is out of bounds */ public PropertiesFileEntry getNthEntry(int index) { if (entries == null) { notifyEntriesNotInitialized(); } if (index >= 0 && index < entries.length) { return entries[index]; } else { return null; } } /** * Retrieves an index of a file entry representing the given file. * * @param fileName simple name (without path and extension) of the * primary or secondary file * @return index of the entry representing a file with the given filename; * or -1 if no such entry is found * @exception java.lang.IllegalStateException * if the list of entries has not been initialized yet * @see #getEntryByFileName */ public int getEntryIndexByFileName(String fileName) { if (entries == null) { notifyEntriesNotInitialized(); } for (int i = 0; i < getEntryCount(); i++) { if (entries[i].getFile().getName().equals(fileName)) { return i; } } return -1; } /** * Retrieves a file entry representing the given file * * @param fileName simple name (excl. path, incl. extension) of the * primary or secondary file * @return entry representing the given file; * or null if not such entry is found * @exception java.lang.IllegalStateException * if the list of entries has not been initialized yet * @see #getEntryIndexByFileName */ public PropertiesFileEntry getEntryByFileName(String fileName) { int index = getEntryIndexByFileName(fileName); return ((index == -1) ? null : entries[index]); } /** * Retrieves number of file entries. * * @return number of file entries * @exception java.lang.IllegalStateException * if the list of entries has not been initialized yet */ public int getEntryCount() { if (entries == null) { notifyEntriesNotInitialized(); } return entries.length; } // Sorted keys management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Retrieves all un-escaped keys in bundle. * * @return sorted array of non-escaped keys * @exception java.lang.IllegalStateException * if the list of keys has not been initialized yet * @see #sort */ public String[] getKeys() { if (keyList == null) { notifyKeyListNotInitialized(); } return keyList.toArray(new String[0]); } /** * Retrieves the n-th bundle key from the list, indexed from 0. * * @param keyIndex index according to the current order of keys * @return non-escaped key at the given position; * or null if the given index is out of range * @exception java.lang.IllegalStateException * if the list of keys has not been initialized yet */ public String keyAt(int keyIndex) { if (keyList == null) { notifyKeyListNotInitialized(); } if (keyIndex < 0 || keyIndex >= keyList.size()) { return null; } else { return keyList.get(keyIndex); } } /** * Returns the index of the given key within the sorted list of keys * * @param key non-escaped key * @return position of the given key in the bundle; * or -1 if the key was not found * @exception java.lang.IllegalStateException * if the list of keys has not been initialized yet */ public int getKeyIndexByName(String key) { if (keyList == null) { notifyKeyListNotInitialized(); } return keyList.indexOf(key); } /** * Finds a free key in the budnle. If the suggested key is not free, * a number is appended to it. */ public String findFreeKey(String keySpec) { if (keyList == null) { notifyKeyListNotInitialized(); } int n = 1; String key = keySpec; while (keyList.contains(key)) { key = keySpec + "_" + n++; } return key; } /** * Retrieves keyIndex-th key in the entryIndex-th entry from the list, * indexed from 0. * * @return item for keyIndex-th key in the entryIndex-th entry; * or null if the entry does not contain * the key or entry doesn't exist */ public Element.ItemElem getItem(int entryIndex, int keyIndex) { String key = keyAt(keyIndex); return getItem(entryIndex, key); } /** * Returns a property item having a given key, from a given file entry. * * @param entryIndex index of the file entry to get the item from * @param key key of the property to receive * @return item from the given file entry, having the given key; * or null if one of the following is true: *

    *
  • entry index is out of bounds
  • *
  • null was passed as a key
  • *
  • the given key was not found in the given entry
  • *
  • structure of the given file entry is not available * because of an error while reading the entry * or because parsing of the file entry was stopped * for some reason
  • *
* @see org.netbeans.modules.properties.Element.ItemElem */ public Element.ItemElem getItem(int entryIndex, String key) { if (key == null) { return null; } PropertiesFileEntry pfe = getNthEntry(entryIndex); if (pfe == null) { return null; } PropertiesStructure ps = pfe.getHandler().getStructure(); if (ps != null) { return ps.getItem(key); } else { return null; } } /** * Returns property item of given key from localization corresponding to * given file name. If not found in given file directly then "parent" files * are scanned - the same way as ResourceBundle would work when asked for * locale specific key. * * @param localizationFile name of file entry without extension * corresponding to the desired specific localization * @param key the key of the item in the model. See clarifications * {@link PropertiesStructure#getItem(java.lang.String) here}. * @return a property item if is it possible, otherwise {@code null}. */ public Element.ItemElem getItem(String localizationFile, String key) { int score = 0; // number of same characters in the file name Element.ItemElem item = null; for (int i=0; i < getEntryCount(); i++) { PropertiesFileEntry pfe = getNthEntry(i); if (pfe != null) { String fName = pfe.getFile().getName(); if (localizationFile.startsWith(fName) && (item == null || fName.length() > score)) { // try to find the item in the entry with longest file name // matching (most specific localization) PropertiesStructure ps = pfe.getHandler().getStructure(); if (ps != null) { Element.ItemElem it = ps.getItem(key); if (it != null) { item = it; score = fName.length(); } } } } } return item; } /** * Gets all data for given key from all locales. * @return String[] array of strings - repeating: locale, value, comments */ public String[] getAllData(String key) { List list = null; for (int i=0; i < getEntryCount(); i++) { PropertiesFileEntry pfe = getNthEntry(i); if (pfe != null) { PropertiesStructure ps = pfe.getHandler().getStructure(); if (ps != null) { Element.ItemElem item = ps.getItem(key); if (item != null) { String locale = Util.getLocaleSuffix(pfe); if (list == null) { list = new ArrayList(); } list.add(locale); list.add(item.getValue()); list.add(item.getComment()); } } } } return list != null ? list.toArray(new String[list.size()]) : null; } public void setAllData(String key, String[] data) { // create missing file entries boolean entryCreated = false; for (int i=0; i < data.length; i+=3) { String locale = data[i]; PropertiesFileEntry localeFile = null; for (int j=0; j < getEntryCount(); j++) { PropertiesFileEntry pfe = getNthEntry(j); if (pfe != null && Util.getLocaleSuffix(pfe).equals(locale)) { localeFile = pfe; break; } } if (localeFile == null) { Util.createLocaleFile(obj, locale.substring(1), false); entryCreated = true; } } if (entryCreated) updateEntries(); // add all provided data for (int i=0; i < data.length; i+=3) { String locale = data[i]; for (int j=0; j < getEntryCount(); j++) { PropertiesFileEntry pfe = getNthEntry(j); if (pfe != null && Util.getLocaleSuffix(pfe).equals(locale)) { PropertiesStructure ps = pfe.getHandler().getStructure(); if (ps != null) { Element.ItemElem item = ps.getItem(key); if (item != null) { item.setValue(data[i+1]); item.setComment(data[i+2]); } else { ps.addItem(key, data[i+1], data[i+2]); } } break; } } } // remove superfluous data if (getEntryCount() > data.length/3) { for (int j=0; j < getEntryCount(); j++) { PropertiesFileEntry pfe = getNthEntry(j); PropertiesStructure ps = pfe.getHandler().getStructure(); if (pfe == null || ps == null) continue; boolean found = false; for (int i=0; i < data.length; i+=3) { String locale = data[i]; if (Util.getLocaleSuffix(pfe).equals(locale)) { found = true; break; } } if (!found) { ps.deleteItem(key); } } } } /** * Returns count of all unique keys found in all file entries. * * @return size of a union of keys from all entries * @exception java.lang.IllegalStateException * if the list of keys has not been initialized yet */ public int getKeyCount() { if (keyList != null) { return keyList.size(); } else { notifyKeyListNotInitialized(); return 0; //will not happen } } /** * Adds to or changes an item in specified localization file and its parents. */ public void addItem(String localizationFile, String key, String value, String comment, boolean changeIfExists) { PropertiesStructure[] ps = getRelatedStructures(localizationFile); boolean changed = false; for (int i=0; i < ps.length; i++) { Element.ItemElem item = ps[i].getItem(key); if (item != null) { if (changeIfExists && !changed) { item.setValue(value); item.setComment(comment); changed = true; // change only once - in the most specific set } } else { ps[i].addItem(key, value, comment); changed = true; // change only once - in the most specific set } } } /** * Deletes item with given key from all files of this bundle. */ public void removeItem(String key) { for (int i=0; i < getEntryCount(); i++) { PropertiesFileEntry pfe = getNthEntry(i); if (pfe != null) { PropertiesStructure ps = pfe.getHandler().getStructure(); if (ps != null) { ps.deleteItem(key); } } } } /** * Sorts the keylist according the values of entry which index is given * to this method. * * @param index sorts accordinng nth-1 entry values, 0 means * sort by keys, if less than 0 it re-compares * keylist with the same un-changed comparator. */ public void sort(int index) { if (index >= 0) { comparator.setIndex(index); } synchronized (this) { Collections.sort(keyList, comparator); } propBundleSupport.fireBundleDataChanged(); } /** * Gets index accoring which is bundle key list sorted. * * @return index, 0 means according keys, * -1 means sorting as in default * properties file */ public int getSortIndex() { return comparator.getIndex(); } /** * Gets current order of sort. * * @return true if ascending, alse descending order * (until sort index is -1, then unsorted) */ public boolean getSortOrder() { return comparator.isAscending(); } PropertiesOpen getOpenSupport() { throw new UnsupportedOperationException("Not yet implemented"); } /** * Builds (or rebuilds) a sorted list of entries of the underlying * PropertiesDataObject and a sorted list of keys gathered * from all the entries. * * @see #entries * @see #keyList */ void updateEntries() { Map tm = new TreeMap( PropertiesDataObject.getSecondaryFilesComparator()); for (Entry entry : obj.secondaryEntries()) { tm.put(entry.getFile().getName(), (PropertiesFileEntry) entry); } synchronized (this) { // Move the entries. int entriesCount = tm.size(); entries = new PropertiesFileEntry[entriesCount + 1]; entries[0] = (PropertiesFileEntry) obj.getPrimaryEntry(); int index = 0; for (Map.Entry mapEntry : tm.entrySet()) { entries[++index] = mapEntry.getValue(); } } buildKeySet(); } /** * Constructs a sorted list of all keys gathered from all entries. * * @see #keyList */ protected synchronized void buildKeySet() { List keyList = new ArrayList() { public boolean equals(Object obj) { if (!(obj instanceof ArrayList)) { return false; } ArrayList list2 = (ArrayList) obj; if (this.size() != list2.size()) { return false; } for (int i = 0; i < this.size(); i++) { if (!this.contains(list2.get(i)) || !list2.contains(this.get(i))) { return false; } } return true; } }; //Create interim Set as ArrayList.contains is an expensive operation // and can cause delayes on large property files. // See: #188619 Set interimSet = new HashSet(keyList); // for all entries add all keys int entriesCount = getEntryCount(); for (int index = 0; index < entriesCount; index++) { PropertiesFileEntry entry = getNthEntry(index); if (entry != null) { PropertiesStructure ps = entry.getHandler().getStructure(); if (ps != null) { for (Iterator it = ps.allItems(); it.hasNext(); ) { Element.ItemElem item = it.next(); if (item == null) { continue; } String key = item.getKey(); if (key != null) { interimSet.add(key); } } } } } keyList.addAll(interimSet); Collections.sort(keyList, comparator); this.keyList = keyList; } /** * Collects PropertyStructure objects that are related for given design time * localization - i.e. the structure corresponding to the given file name * plus all the "parents". Sorted from the most specific. * @param localizationFile name of specific file entry (without extension) */ private PropertiesStructure[] getRelatedStructures(String localizationFile) { List list = null; for (int i=0; i < getEntryCount(); i++) { PropertiesFileEntry pfe = getNthEntry(i); if (pfe != null) { if (localizationFile.startsWith(pfe.getFile().getName()) && pfe.getHandler().getStructure() != null) { if (list == null) { list = new ArrayList(4); } list.add(pfe); } } } if (list == null) { return new PropertiesStructure[] {}; } Collections.sort(list, new Comparator() { public int compare(PropertiesFileEntry pfe1, PropertiesFileEntry pfe2) { return pfe2.getFile().getName().length() - pfe1.getFile().getName().length(); } }); PropertiesStructure[] array = new PropertiesStructure[list.size()]; for (int i=0, n=list.size(); i < n; i++) { array[i] = list.get(i).getHandler().getStructure(); } return array; } boolean isReadOnly() { boolean canWrite = false; for (int i=0; i < getEntryCount(); i++) { PropertiesFileEntry entry = getNthEntry(i); if (entry != null) canWrite |= entry.getFile().canWrite(); } return !canWrite; } /** * Registers a given listener so that it will receive notifications * about changes in a property bundle. * If the given listener is already registered, a duplicite registration * will be performed, so that it will get notifications multiple times. * * @param l listener to be registered * @see #removePropertyBundleListener */ public void addPropertyBundleListener(PropertyBundleListener l) { if (propBundleSupport == null) propBundleSupport = new PropertyBundleSupport(this); propBundleSupport.addPropertyBundleListener(l); } /** * Unregisters a given listener so that it will no more receive * notifications about changes in a property bundle. * If the given listener has been registered multiple times, * only one registration item will be removed. * * @param l the PropertyBundleListener * @see #addPropertyBundleListener */ public void removePropertyBundleListener(PropertyBundleListener l) { propBundleSupport.removePropertyBundleListener(l); } /** * Notifies registered listeners of a change of a single item * in a single file entry. * * @param struct object describing the file entry * @param item changed item (within the entry) * @see #addPropertyBundleListener */ void notifyItemChanged(PropertiesStructure struct, Element.ItemElem item) { propBundleSupport.fireItemChanged( struct.getParent().getEntry().getFile().getName(), item.getKey() ); } void notifyOneFileChanged(FileObject file) { // PENDING - events should be finer // find out whether global key table has changed and fire a change // according to that List oldKeyList = keyList; buildKeySet(); if (!keyList.equals(oldKeyList)) { propBundleSupport.fireBundleDataChanged(); } else { propBundleSupport.fireFileChanged(file.getName()); } } /** * Notifies registered listeners of a change in a single file entry. * Depending whether a list of keys has changed, either an event * for a single file is fired (if the list of keys has remained unchanged) * or a notification of a complex change is fired. * * @param handler handler of an object keeping structure of the modified * file (entry) */ void notifyOneFileChanged(StructHandler handler) { // PENDING - events should be finer // find out whether global key table has changed and fire a change // according to that List oldKeyList = keyList; buildKeySet(); if (!keyList.equals(oldKeyList)) { propBundleSupport.fireBundleDataChanged(); } else { propBundleSupport.fireFileChanged( handler.getEntry().getFile().getName()); } } /** * Notifies registered listeners of a change in a single file entry. * The Map arguments are actually list of items, * each Map entry is a pair <item key, item>. * * @param handler handler of an object keeping structure of the modified * file (entry) * @param itemsChanged list of modified items in the entry * @param itemsAdded list of items added to the entry * @param itemsDeleted list of items removed from the entry */ void notifyOneFileChanged(StructHandler handler, Map itemsChanged, Map itemsAdded, Map itemsDeleted) { // PENDING - events should be finer // find out whether global key table has changed // should use a faster algorithm of building the keyset buildKeySet(); propBundleSupport.fireBundleDataChanged(); } /** * Throws a runtime exception with a message that the list of bundle keys * has not been initialized yet. * * @exception java.lang.IllegalStateException thrown always * @see #buildKeySet */ private void notifyKeyListNotInitialized() { throw new IllegalStateException( "Resource Bundles: KeyList not initialized"); //NOI18N } /** * Throws a runtime exception with a message that the entries * have not been initialized yet. * * @exception java.lang.IllegalStateException thrown always * @see #updateEntries */ private void notifyEntriesNotInitialized() { throw new IllegalStateException( "Resource Bundles: Entries not initialized"); //NOI18N } PropertiesFileEntry[] getEntries () { synchronized (this) { if (entries == null) { return new PropertiesFileEntry[0]; } else { return Arrays.copyOf(entries, entries.length); } } } /** * Comparator which compares keys according which locale (column in table was selected). */ private final class KeyComparator implements Comparator { /** Index of column to compare with. */ private int index; /** Flag if ascending order should be performed. */ private boolean ascending; /** Constructor. */ public KeyComparator() { this.index = -1; ascending = false; } /** * Setter for index property. * ascending -> descending -> primary file key order -> .... * * @param index interval 0 .. entry count */ public void setIndex(int index) { if (index == -1) { throw new IllegalArgumentException(); } // if same column toggle order if (this.index == index) { if (ascending) { ascending = false; } else { // sort as in properties file index = -1; ascending = true; } } else { ascending = true; } this.index = index; } /** * Getter for index property. * * @return -1..entry count, -1 means unsorted * */ public int getIndex() { return index; } /** Getter for ascending property. */ public boolean isAscending() { return ascending; } /** * It's strange as it access just being compared list */ public int compare(String o1, String o2) { String str1; String str2; // sort as in default properties file if (index < 0) { Element.ItemElem item1 = getItem(0, o1); Element.ItemElem item2 = getItem(0, o2); if (item1 != null && item2 != null) { int i1 = item1.getBounds().getBegin().getOffset(); int i2 = item2.getBounds().getBegin().getOffset(); return i1 - i2; } else if (item1 != null) { return -1; } else if (item2 != null) { return 1; } else { /* * None of the keys is in the default (primary) properties * file. Order the files by name. */ str1 = o1; str2 = o2; } } // key column if (index == 0) { str1 = o1; str2 = o2; } else { Element.ItemElem item1 = getItem(index - 1, o1); Element.ItemElem item2 = getItem(index - 1, o2); if (item1 == null) { if (item2 == null) { return 0; } else { return ascending ? 1 : -1; } } else { if (item2 == null) { return ascending ? -1 : 1; } } str1 = item1.getValue(); str2 = item2.getValue(); } if (str1 == null) { if (str2 == null) { return 0; } else { return ascending ? 1 : -1; } } else if (str2 == null) { return ascending ? -1 : 1; } int res = str1.compareToIgnoreCase(str2); return ascending ? res : -res; } } // End of inner class KeyComparator. }