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

org.tentackle.persist.DbPreferences Maven / Gradle / Ivy

There is a newer version: 21.16.1.0
Show newest version
/**
 * Tentackle - http://www.tentackle.org
 * Copyright (C) 2001-2008 Harald Krake, [email protected], +49 7722 9508-0
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


package org.tentackle.persist;

import java.io.IOException;
import java.io.OutputStream;
import java.util.EventObject;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.prefs.BackingStoreException;
import java.util.prefs.NodeChangeEvent;
import java.util.prefs.NodeChangeListener;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
import org.tentackle.common.Compare;
import org.tentackle.io.Base64;
import org.tentackle.log.Logger;
import org.tentackle.log.LoggerFactory;
import org.tentackle.pdo.PersistenceException;
import org.tentackle.pdo.Session;
import org.tentackle.prefs.PersistedPreferences;
import org.tentackle.prefs.PersistedPreferencesFactory;
import org.tentackle.prefs.PersistedPreferencesXmlSupport;



/**
 * Database backend for the {@link Preferences} API.
* * Tentackle preferences are stored in a database rather than a file(unix) or registry(windows). * The persistent classes are: *
    *
  1. {@link DbPreferencesNode}: the nodes representing the preferences hierarchy.
  2. *
  3. {@link DbPreferencesKey}: the key/value pairs corresponding to the nodes.
  4. *
* * Unlike the default JDK-backends, tentackle preferences are synchronized between * JVMs, i.e. a (flushed) change in one JVM will show up in all other JVMs. * As a consequence, NodeChangeEvents and PreferenceChangeEvents work * across JVM-boundaries. * The synchronization is based on the {@link ModificationTracker} by use of the tableSerial. * Notice that events in the local JVM are enqueued at the time of change (i.e. put(), * remove(), node() or removeNode()) while other JVMs will become aware of the changes * only after the changes are flush()ed to persistent storage. * The underlying mechanism works as follows: *
    *
  • a key is modified: the corresponding key is updated to the db and all other JVMs will identify * this by its tableSerial. If the corresponding key is already loaded in other JVMs, it is reloaded * from the db and the optional PreferenceChangeListeners invoked for its node. *
  • *
  • a key is added or removed: the key is deleted from or added to the db and its node is updated allowing * other JVMs to detect the node by its tableSerial and to update its keys. * If PreferenceChangeListeners are registered, an Event will be triggered. *
  • *
  • a node is added or removed: the node is inserted into or removed from the db and its parent is * updated to allow detection by its tableserial. NodeChangeEvents are triggered accordingly. * The fact that "some" nodes have been deleted is detected by gaps in the retrieved tableSerials. *
  • *
* * Compared to {@link java.util.prefs.AbstractPreferences} (which we can not simply extend * due to various design issues), the DbPreferences differ slightly in the following aspects: *
    *
  • PreferenceChangeEvents are only triggered if there really is a change. Thus, invoking put() without * an effective change will *not* trigger an event. *
  • *
  • there is only little difference between sync() and flush() if autoSync=true, because a sync * is automatically performed "in background" by the ModificationThread. * If autoSync=false, sync() has to be invoked explicitly to reflect changes made by other JVMs. * AutoSync is the recommended mode for most applications. *
  • *
  • nodeExists() only returns true, if the node is persistent (regardless whether we accessed it yet * or created by another JVM). However, the semantics of nodeExists("") remain, i.e. "return true if not deleted". * Thus, nodeExists("") == false just means that the node has been removed by removeNode(), * either by the current or another JVM. However, true does *NOT* mean, that the node exists, * i.e. is persistent in the database. It just means, that it has not been removed. *
  • *
* * @author harald */ public class DbPreferences extends Preferences implements PersistedPreferences { /** * the logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(DbPreferences.class); /** transaction name for flush. */ private static final String TX_FLUSH = "flush"; /** transaction name for remove node. */ private static final String TX_REMOVE_NODE = "remove node"; private String name; // name relative to the parent private String absolutePath; // absolute pathname private boolean userMode; // true if this is a user-node (speeds up) private DbPreferences parent; // parent node, null = this is the root private DbPreferences root; // the root preference (non-static because of user and system-scope) private DbPreferencesNode node; // the database node (backing store), null = deleted private Map keys; // list of keys associated with this node // (lazy if no PreferenceChangeListeners registered) private Set childIds; // IDs of all child nodes (only if NodeChangeListeners registered) private Map childPrefs; // accessed child preferences so far. // Key is the name relative to the parent. private PreferenceChangeListener[] prefListeners; // preference change listeners private NodeChangeListener[] nodeListeners; // node change listeners /** * Creates a preference node with the specified parent and the specified * name relative to its parent. Don't use this constructor for root-nodes! * * @param parent the parent of this preference node * @param name the name of this preference node, relative to its parent * @throws IllegalArgumentException if name contains a slash * ('/'), or parent is null and * name isn't "". */ public DbPreferences(DbPreferences parent, String name) { if (parent == null) { throw new IllegalArgumentException("illegal constructor for root node"); } if (name.indexOf('/') != -1) { throw new IllegalArgumentException("name '" + name + "' contains '/'"); } if (name.isEmpty()) { throw new IllegalArgumentException("illegal name: empty string"); } absolutePath = parent.parent == null ? "/" + name : parent.absolutePath() + "/" + name; this.name = name; this.parent = parent; this.userMode = parent.userMode; this.root = parent.root; initializeNode(); } /** * Special constructor for roots (both user and system). * This constructor will only be called twice by the static intializer. * Package scope! * * @param userMode true if user mode, else system mode */ public DbPreferences(boolean userMode) { this.userMode = userMode; name = ""; absolutePath = "/"; root = this; initializeNode(); } /** * Constructs DbPreferences from a DbPreferencesNode. * Only used from loadNodes(). * Package scope! * @param parent the parent preferences * @param node the node * @throws BackingStoreException if creation failed */ public DbPreferences(DbPreferences parent, DbPreferencesNode node) throws BackingStoreException { if (node.getParentId() != parent.node.getId()) { throw new BackingStoreException( "parent-ID mismatch in node '" + node + "': got ID=" + node.getParentId() + ", expected ID=" + parent.node.getId()); } this.node = node; this.absolutePath = node.getName(); this.name = node.getBaseName(); this.parent = parent; this.userMode = parent.userMode; this.root = parent.root; getFactory().updateNodeTableSerial(node.getTableSerial()); childPrefs = new TreeMap<>(); childIds = node.selectChildIds(); } /** * Gets the persistence node. * * @return the node */ public DbPreferencesNode getNode() { return node; } /** * Gets the persistence parent. * * @return the parent */ public DbPreferences getParent() { return parent; } /** * Gets the name. * * @return the name */ public String getName() { return name; } /** * Gets the absolute path. * * @return the path */ public String getAbsolutePath() { return absolutePath; } /** * Returns whether user-scope or system. * * @return true if user, else system */ public boolean isUserMode() { return userMode; } /** * Gets the root node. * * @return the root node */ public DbPreferences getRoot() { return root; } /** * Gets the child preferences. * * @return the child preferences */ public Map getChildPrefs() { return childPrefs; } /** * Gets the child IDs. * * @return the child IDs */ public Set getChildIds() { return childIds; } /** * Gets the persistence key/value pairs. * * @return the keys */ public Map getKeys() { return keys; } /** * Gets the factory singleton. * * @return the factory */ private DbPreferencesFactory getFactory() { return (DbPreferencesFactory) PersistedPreferencesFactory.getInstance(); } /** * initializes the node */ private void initializeNode() { childPrefs = new TreeMap<>(); Session session = getFactory().getSession(); node = getFactory().createNode().selectByUserAndName( userMode ? session.getSessionInfo().getUserName() : null, absolutePath); if (node == null) { // node is new childIds = new TreeSet<>(); node = getFactory().createNode(); node.setUser(userMode ? session.getSessionInfo().getUserName() : null); node.setName(absolutePath); if (parent != null) { node.setParentId(parent.node.getId()); } } else { // node already exists childIds = node.selectChildIds(); getFactory().updateNodeTableSerial(node.getTableSerial()); } getFactory().addPreferences(this); } /** * loads all keys from the backing store for the current node */ private void loadKeys() { keys = new TreeMap<>(); for (DbPreferencesKey key: getFactory().createKey().selectByNodeId(node.getId())) { keys.put(key.getKey(), key); getFactory().addKey(key); } } /** * unloads all keys */ private void unloadKeys() { if (keys != null) { for (DbPreferencesKey key: keys.values()) { getFactory().removeKey(key); } } keys = null; } /** * loads all child-nodes from the backing store. */ private void loadNodes() { List childNodes = getFactory().createNode().selectByParentId(node.getId()); // remove from maps before clear for (DbPreferences child: childPrefs.values()) { if (child.node != null) { getFactory().removeNode(child.node); } } childPrefs.clear(); childIds.clear(); for (DbPreferencesNode n: childNodes) { String basename = n.getBaseName(); try { DbPreferences childPref = getFactory().createPreferences(this, n); childPrefs.put(basename, childPref); // append to node cache childIds.add(n.getId()); // remember IDs getFactory().addPreferences(childPref); // append to maps } catch (BackingStoreException ex) { throw new IllegalStateException("loading child nodes failed for " + this, ex); } } } /** * gets a reference to a key */ private DbPreferencesKey getKey(String key) { if (keys == null) { loadKeys(); } return keys.get(key); } /** * assert that node is not deleted */ private void assertNotRemoved() throws IllegalStateException { if (node == null) { // node has been deleted throw new IllegalStateException("node '" + absolutePath + "' has been removed"); } } /** * Updates the serial and tableserial of a parent node. * The method is invoked whenever a key or child node is added or removed. * * @param node the parent node to update * @throws BackingStoreException if update failed */ protected void updateParentNode(DbPreferencesNode node) throws BackingStoreException { try { node.saveObject(); } catch (PersistenceException e) { throw new BackingStoreException(e); } } /** * update parent node to trigger other jvms */ private void updateParentNode() throws BackingStoreException { if (parent != null && parent.node != null) { // update parent to trigger events in other jvms DbPreferences parentPref = getFactory().getPreferences(node.getParentId()); if (parentPref == null) { throw new BackingStoreException("parent of " + node + " not in cache"); } DbPreferencesNode parentNode = parentPref.node; if (parentNode == null) { throw new BackingStoreException("parentpref of " + node + " has no node"); } updateParentNode(parentNode); } } /** * Recursive implementation of flush. * * @throws java.util.prefs.BackingStoreException */ private void flushImpl() throws BackingStoreException { if (node == null) { throw new BackingStoreException("node already removed"); } // saveObject node if new boolean nodeIsNew = node.isNew(); if (nodeIsNew) { // node is new if (parent != null) { // set parent node if not root node.setParentId(parent.node.getId()); // node must exist (see flush()) } try { node.saveObject(); } catch (PersistenceException e) { /** * Either somebody else already created that node or there really * is a db-error. */ throw new BackingStoreException(e); } // append new node to map getFactory().addPreferences(this); // update parent to trigger events in other jvms updateParentNode(); } // saveObject the keys (if loaded) if (keys != null) { boolean updateNode = false; for (Iterator iter=keys.values().iterator(); iter.hasNext(); ) { DbPreferencesKey key = iter.next(); if (key.isDeleted()) { // remove it key.setId(key.getId()); // set the positive ID first try { key.deleteObject(); } catch (PersistenceException e) { throw new BackingStoreException(e); } iter.remove(); // remove key physically // remove key from map getFactory().removeKey(key); updateNode = true; // some key has been deleted: update node to trigger other jvms } else if (key.isModified()) { if (key.isNew()) { // some key has been added: update node to trigger other jvms updateNode = true; } try { key.saveObject(); } catch (PersistenceException e) { /** * Either somebody else already created that key or there really * is a db-error. We can't update the key if db.isConstraintViolation() * because the backend usually terminates the transaction upon * such an error. */ throw new BackingStoreException(e); } // update keymap getFactory().addKey(key); } } if (updateNode && !nodeIsNew) { // keys have been added or deleted (and node already exists): trigger other jvms modthread updateParentNode(node); } } // process child nodes for (DbPreferences child: childPrefs.values()) { child.flushImpl(); } } /** * recover this node and all its descendents. * Necessary after BackingStoreException in flush(). * Invoked from within flush(). * * Notice: the listeners are not recovered, i.e. lost */ private void recover() { LOGGER.warning("*** recovering node " + this + " ***"); // remove the keys for this node if (keys != null) { for (DbPreferencesKey key: keys.values()) { getFactory().removeKey(key); } keys = null; } // remove this node from its parent if (parent != null) { if (node != null) { parent.childIds.remove(node.getId()); } parent.childPrefs.remove(name); } // remove the node from the global pool if (node != null) { getFactory().removeNode(node); } node = null; nodeListeners = null; prefListeners = null; // do that for all childs (need a copy cause of remove() in collection) DbPreferences[] childs = new DbPreferences[childPrefs.size()]; childPrefs.values().toArray(childs); for (DbPreferences child: childs) { child.recover(); } } /** * tokenizer contains {'/' }* */ private DbPreferences node(StringTokenizer path) { String token = path.nextToken(); if (token.equals("/")) { // Check for consecutive slashes throw new IllegalArgumentException("Consecutive slashes in path"); } DbPreferences child = childPrefs.get(token); if (child == null) { if (token.length() > MAX_NAME_LENGTH) { throw new IllegalArgumentException("Node name " + token + " too long"); } child = getFactory().createPreferences(this, token); if (child.node.isNew()) { enqueueNodeAddedEvent(child); } else { childIds.add(child.node.getId()); } childPrefs.put(token, child); } if (!path.hasMoreTokens()) { return child; } path.nextToken(); // Consume slash if (!path.hasMoreTokens()) { throw new IllegalArgumentException("Path ends with slash"); } return child.node(path); } /** * Recurive implementation for removeNode. * Invoked with locks on all nodes on path from parent of "removal root" * to this (including the former but excluding the latter). */ private void removeNodeImpl() throws BackingStoreException { if (node != null) { loadNodes(); // get all nodes loadKeys(); // and keys // recursively remove all children for (DbPreferences child: childPrefs.values()) { child.removeNodeImpl(); } try { node.deleteObject(); } catch (PersistenceException e) { throw new BackingStoreException(e); } getFactory().removeNode(node); // delete all keys for this node. if (!keys.isEmpty()) { DbPreferencesKey key = keys.values().iterator().next(); // pick the first if (key.isLoggingModification(ModificationLog.DELETE)) { // delete key by key for (DbPreferencesKey k: keys.values()) { k.deleteObject(); } } else { key.deleteByNodeId(node.getId()); } } unloadKeys(); keys = null; childPrefs = null; childIds = null; nodeListeners = null; prefListeners = null; node = null; // mark node deleted parent.enqueueNodeRemovedEvent(this); } } /** * @return true if any node listeners are registered on this node */ public boolean areNodeListenersRegistered() { return nodeListeners != null && nodeListeners.length > 0; } /** * Returns whether reference listeners are registered. * * @return true if any preferences listeners are registered on this node */ public boolean arePreferenceListenersRegistered() { return prefListeners != null && prefListeners.length > 0; } /** * Returns the string represenation of this node. * *
    *
  • if node is deleted: "<absolutePath> [deleted]"
  • *
  • user node: "<user>:<absolutePath>"
  • *
  • system node: "<system>:<absolutePath>"
  • *
* * @return the string */ @Override public String toString() { if (node == null) { return absolutePath + " [deleted]"; } if (userMode) { return node.getUser() + ":" + absolutePath; } else { return ":" + absolutePath; } } // --------------- implements Preferences -------------------------- @Override public String name() { return name; } @Override public String absolutePath() { return absolutePath; } @Override public boolean isUserNode() { return userMode; } @Override public void put(String key, String value) { if (key == null || value == null) { throw new NullPointerException(); } if (key.length() > MAX_KEY_LENGTH) { throw new IllegalArgumentException("key too long: <" + key + ">"); } if (value.length() > MAX_VALUE_LENGTH) { throw new IllegalArgumentException("value too long: <" + value + ">"); } synchronized(getFactory()) { assertNotRemoved(); if (!getFactory().isReadOnly()) { DbPreferencesKey k = getKey(key); if (k == null) { // add a new key/value-pair to the node k = getFactory().createKey(); if (!node.isIdValid()) { /** * node is not saved to disk: delay setting the nodeID * by setLazyNode(). See prepareSetFields in DbPreferencesKey. */ k.setLazyNode(node); } else { k.setNodeId(node.getId()); } k.setKey(key); keys.put(key, k); } else if (k.isDeleted()) { // was marked deleted in remove(): enable it again k.setId(-k.getId()); } String oldValue = k.getValue(); k.setValue(value); if (Compare.compare(oldValue, value) != 0) { enqueuePreferenceChangeEvent(key, value); } } } } @Override public String get(String key, String def) { if (key == null) { throw new NullPointerException("null key"); } synchronized(getFactory()) { assertNotRemoved(); DbPreferencesKey k = getKey(key); return k == null || k.isDeleted() || k.getValue() == null ? def : k.getValue(); } } @Override public void remove(String key) { synchronized(getFactory()) { assertNotRemoved(); if (!getFactory().isReadOnly()) { DbPreferencesKey k = getKey(key); if (k != null) { if (k.isIdValid()) { /** * We don't delete the key right now, but mark it as "deleted" by * negating the ID. Notice that getId() always returns a positive ID, * so the following code will always set a negative ID. */ k.setId(-k.getId()); } else { // never saved to disk: remove from keys right now keys.remove(key); } // else: already marked deleted enqueuePreferenceChangeEvent(key, null); } } } } @Override public void clear() throws BackingStoreException { if (!getFactory().isReadOnly()) { String[] keyArray = keys(); for (String keyArray1 : keyArray) { remove(keyArray1); } } } @Override public void sync() throws BackingStoreException { flush(); // flush first // explicit expiration getFactory().expireKeys(getFactory().createKey().getModificationCount()); getFactory().expireNodes(getFactory().createNode().getModificationCount()); } @Override public void flush() throws BackingStoreException { if (!getFactory().isReadOnly()) { synchronized (getFactory()) { // single transaction Session session = getFactory().getSession(); long txVoucher = session.begin(TX_FLUSH); try { /** * go up to the first parent with a new node */ DbPreferences prefs = this; while (prefs.parent != null && prefs.parent.node != null && prefs.parent.node.isNew()) { prefs = prefs.parent; } // flush starting at first non-persistent node prefs.flushImpl(); session.commit(txVoucher); } catch (BackingStoreException ex) { session.rollback(txVoucher); // some severe db-error or unique violation: rollback /** * recover the node, i.e. load from storage and invalidate all modifications made so far. This is the only way * to get a working preferences tree again. */ recover(); throw ex; } } } } @Override public void putInt(String key, int value) { put(key, Integer.toString(value)); } @Override public int getInt(String key, int def) { int result = def; try { String value = get(key, null); if (value != null) { result = Integer.parseInt(value); } } catch (NumberFormatException e) { // Ignoring exception causes specified default to be returned } return result; } @Override public void putLong(String key, long value) { put(key, Long.toString(value)); } @Override public long getLong(String key, long def) { long result = def; try { String value = get(key, null); if (value != null) { result = Long.parseLong(value); } } catch (NumberFormatException e) { // Ignoring exception causes specified default to be returned } return result; } @Override public void putBoolean(String key, boolean value) { put(key, String.valueOf(value)); } @Override public boolean getBoolean(String key, boolean def) { boolean result = def; String value = get(key, null); if (value != null) { if (value.equalsIgnoreCase("true")) { result = true; } else if (value.equalsIgnoreCase("false")) { result = false; } } return result; } @Override public void putFloat(String key, float value) { put(key, Float.toString(value)); } @Override public float getFloat(String key, float def) { float result = def; try { String value = get(key, null); if (value != null) { result = Float.parseFloat(value); } } catch (NumberFormatException e) { // Ignoring exception causes specified default to be returned } return result; } @Override public void putDouble(String key, double value) { put(key, Double.toString(value)); } @Override public double getDouble(String key, double def) { double result = def; try { String value = get(key, null); if (value != null) { result = Double.parseDouble(value); } } catch (NumberFormatException e) { // Ignoring exception causes specified default to be returned } return result; } @Override public void putByteArray(String key, byte[] value) { put(key, Base64.byteArrayToBase64(value)); } @Override public byte[] getByteArray(String key, byte[] def) { byte[] result = def; String value = get(key, null); try { if (value != null) { result = Base64.base64ToByteArray(value); } } catch (RuntimeException e) { // Ignoring exception causes specified default to be returned } return result; } @Override public String[] keys() throws BackingStoreException { synchronized(getFactory()) { assertNotRemoved(); if (keys == null) { loadKeys(); } String keyNames[] = new String[keys.size()]; keys.keySet().toArray(keyNames); return keyNames; } } @Override public String[] childrenNames() throws BackingStoreException { synchronized(getFactory()) { assertNotRemoved(); loadNodes(); // update childCache // else: nodes and childCache up to date String[] childNames = new String[childPrefs.size()]; childPrefs.keySet().toArray(childNames); return childNames; } } @Override public DbPreferences parent() { assertNotRemoved(); return parent; } @Override public DbPreferences node(String path) { synchronized(getFactory()) { assertNotRemoved(); if (path.isEmpty()) { return this; } if (path.equals("/")) { return root; } if (path.charAt(0) != '/') { // relative path return node(new StringTokenizer(path, "/", true)); } } // absolute path: start at root node return root.node(new StringTokenizer(path.substring(1), "/", true)); } /** * {@inheritDoc} *

* This implementation differs from AbstractPreferences because * it checks the persistent storage rather than the cache, i.e. * a node only created in memory but not yet written to persistent storage * is considered non-existant. As a consequence it is possible to decide * whether a system-preference has been overridden by a user-preference. * (see CompositePreferences). * Notice that "" will return true even if not written to disk to * denote "not deleted" (which does not mean "saved" -- a little confusing) */ @Override public boolean nodeExists(String pathName) throws BackingStoreException { synchronized(getFactory()) { if (pathName.isEmpty()) { // special for "" return node != null; } assertNotRemoved(); if (pathName.equals("/")) { // root return true; } if (pathName.charAt(0) != '/') { // relative path: make absolute pathName = absolutePath + "/" + pathName; } // go by absolute path DbPreferences pref = getFactory().getPreferences(node.getUser(), pathName); // check cache first if (pref != null && !pref.node.isNew()) { return true; // persistent node exists in cache } // check if node exists in storage return getFactory().createNode().selectByUserAndName(node.getUser(), pathName) != null; } } @Override public void removeNode() throws BackingStoreException { if (this == root) { throw new UnsupportedOperationException("Can't remove the root!"); } if (!getFactory().isReadOnly()) { synchronized(getFactory()) { assertNotRemoved(); Session session = getFactory().getSession(); long txVoucher = session.begin(TX_REMOVE_NODE); try { updateParentNode(); // update parent to trigger other jvms removeNodeImpl(); // remove this node and all subnodes parent.childPrefs.remove(name); session.commit(txVoucher); } catch (Exception e) { session.rollback(txVoucher); if (e instanceof BackingStoreException) { throw (BackingStoreException)e; } throw new BackingStoreException(e); } } } } /** * Registers the specified listener to receive preference change * events for this preference node. A preference change event is * generated when a preference is added to this node, removed from this * node, or when the value associated with a preference is changed. * (Preference change events are not generated by the {@link * #removeNode()} method, which generates a node change event. * Preference change events are generated by the clear * method.) * *

Events are generated even for changes made outside this JVM. For the local * JVM events are generated before the changes have been made persistent. For all * other JVMs events are generated *after* flush()/sync(). * * @param pcl The preference change listener to add. * @throws NullPointerException if pcl is null. * @throws IllegalStateException if this node (or an ancestor) has been * removed with the {@link #removeNode()} method. * @see #removePreferenceChangeListener(PreferenceChangeListener) * @see #addNodeChangeListener(NodeChangeListener) */ @Override public void addPreferenceChangeListener(PreferenceChangeListener pcl) { if (pcl == null) { throw new NullPointerException("Change listener is null."); } synchronized(getFactory()) { assertNotRemoved(); if (prefListeners == null) { prefListeners = new PreferenceChangeListener[1]; prefListeners[0] = pcl; } else { PreferenceChangeListener[] old = prefListeners; prefListeners = new PreferenceChangeListener[old.length + 1]; System.arraycopy(old, 0, prefListeners, 0, old.length); prefListeners[old.length] = pcl; } // load keys if not yet done if (keys == null) { loadKeys(); } } startEventDispatchThreadIfNecessary(); } @Override public void removePreferenceChangeListener(PreferenceChangeListener pcl) { synchronized(getFactory()) { assertNotRemoved(); if ((prefListeners == null) || (prefListeners.length == 0)) { throw new IllegalArgumentException("Listener not registered."); } // Copy-on-write PreferenceChangeListener[] newPl = new PreferenceChangeListener[prefListeners.length - 1]; int i = 0; while (i < newPl.length && prefListeners[i] != pcl) { newPl[i] = prefListeners[i++]; } if (i == newPl.length && prefListeners[i] != pcl) { throw new IllegalArgumentException("Listener not registered."); } while (i < newPl.length) { newPl[i] = prefListeners[++i]; } prefListeners = newPl; } } /** * Registers the specified listener to receive node change events * for this node. A node change event is generated when a child node is * added to or removed from this node. (A single {@link #removeNode()} * invocation results in multiple node change events, one for every * node in the subtree rooted at the removed node.) * *

Events are generated even for changes made outside this JVM. For the local * JVM events are generated before the changes have been made persistent. For all * other JVMs events are generated *after* flush()/sync(). * *

Node creation will always generate an even for the local JVM. Other * JVMs get that event only in case the node is created on disk (and not updated * in case another JVM already created that node). * * @param ncl The NodeChangeListener to add. * @throws NullPointerException if ncl is null. * @throws IllegalStateException if this node (or an ancestor) has been * removed with the {@link #removeNode()} method. * @see #removeNodeChangeListener(NodeChangeListener) * @see #addPreferenceChangeListener(PreferenceChangeListener) */ @Override public void addNodeChangeListener(NodeChangeListener ncl) { if (ncl == null) { throw new NullPointerException("Change listener is null."); } synchronized(getFactory()) { assertNotRemoved(); if (nodeListeners == null) { nodeListeners = new NodeChangeListener[1]; nodeListeners[0] = ncl; } else { NodeChangeListener[] old = nodeListeners; nodeListeners = new NodeChangeListener[old.length + 1]; System.arraycopy(old, 0, nodeListeners, 0, old.length); nodeListeners[old.length] = ncl; } } startEventDispatchThreadIfNecessary(); } @Override public void removeNodeChangeListener(NodeChangeListener ncl) { synchronized(getFactory()) { assertNotRemoved(); if ((nodeListeners == null) || (nodeListeners.length == 0)) { throw new IllegalArgumentException("Listener not registered."); } NodeChangeListener[] newNl = new NodeChangeListener[nodeListeners.length - 1]; int i = 0; while (i < nodeListeners.length && nodeListeners[i] != ncl) { newNl[i] = nodeListeners[i++]; } if (i == nodeListeners.length) { throw new IllegalArgumentException("Listener not registered."); } while (i < newNl.length) { newNl[i] = nodeListeners[++i]; } nodeListeners = newNl; } } /** * Queue of pending notification events. When a preference or node * change event for which there are one or more listeners occurs, * it is placed on this queue and the queue is notified. A background * thread waits on this queue and delivers the events. This decouples * event delivery from preference activity, greatly simplifying * locking and reducing opportunity for deadlock. */ private static final List EVENT_QUEUE = new LinkedList<>(); /** * These two classes are used to distinguish NodeChangeEvents on * eventQueue so the event dispatch thread knows whether to call * childAdded or childRemoved. */ @SuppressWarnings("serial") private static class NodeAddedEvent extends NodeChangeEvent { NodeAddedEvent(Preferences parent, Preferences child) { super(parent, child); } } @SuppressWarnings("serial") private static class NodeRemovedEvent extends NodeChangeEvent { NodeRemovedEvent(Preferences parent, Preferences child) { super(parent, child); } } /** * A single background thread ("the event notification thread") monitors * the event queue and delivers events that are placed on the queue. */ private static class EventDispatchThread extends Thread { @Override public void run() { while(true) { // Wait on eventQueue till an event is present EventObject event; synchronized(EVENT_QUEUE) { try { while (EVENT_QUEUE.isEmpty()) { EVENT_QUEUE.wait(); // wait for notify } event = EVENT_QUEUE.remove(0); } catch (InterruptedException e) { return; // terminate thread } } // Now we have event & hold no locks; deliver evt to listeners DbPreferences src = (DbPreferences) event.getSource(); if (event instanceof PreferenceChangeEvent) { PreferenceChangeEvent pce = (PreferenceChangeEvent) event; PreferenceChangeListener[] listeners = src.prefListeners(); for (PreferenceChangeListener listener : listeners) { listener.preferenceChange(pce); } } else { NodeChangeEvent nce = (NodeChangeEvent) event; NodeChangeListener[] listeners = src.nodeListeners(); if (nce instanceof NodeAddedEvent) { for (NodeChangeListener listener : listeners) { listener.childAdded(nce); } } else { for (NodeChangeListener listener : listeners) { listener.childRemoved(nce); } } } } } } private static Thread eventDispatchThread = null; /** * This method starts the event dispatch thread the first time it * is called. The event dispatch thread will be started only * if someone registers a listener. */ private static synchronized void startEventDispatchThreadIfNecessary() { if (eventDispatchThread == null) { eventDispatchThread = new EventDispatchThread(); eventDispatchThread.setDaemon(true); eventDispatchThread.start(); } } /** * Return this node's preference/node change listeners. Even though * we're using a copy-on-write lists, we use synchronized accessors to * ensure information transmission from the writing thread to the * reading thread. * * @return the array of listeners */ PreferenceChangeListener[] prefListeners() { return prefListeners; } /** * @return the array of node listeners */ NodeChangeListener[] nodeListeners() { return nodeListeners; } /** * Enqueue a preference change event for delivery to registered * preference change listeners unless there are no registered * listeners. Invoked with this.lock held. */ void enqueuePreferenceChangeEvent(String key, String newValue) { if (arePreferenceListenersRegistered()) { synchronized(EVENT_QUEUE) { EVENT_QUEUE.add(new PreferenceChangeEvent(this, key, newValue)); EVENT_QUEUE.notifyAll(); } } } /** * Enqueue a "node added" event for delivery to registered node change * listeners unless there are no registered listeners. Invoked with * this.lock held. */ void enqueueNodeAddedEvent(Preferences child) { if (areNodeListenersRegistered()) { synchronized(EVENT_QUEUE) { EVENT_QUEUE.add(new NodeAddedEvent(this, child)); EVENT_QUEUE.notifyAll(); } } } /** * Enqueue a "node removed" event for delivery to registered node change * listeners unless there are no registered listeners. Invoked with * this.lock held. */ void enqueueNodeRemovedEvent(Preferences child) { if (areNodeListenersRegistered()) { synchronized(EVENT_QUEUE) { EVENT_QUEUE.add(new NodeRemovedEvent(this, child)); EVENT_QUEUE.notifyAll(); } } } @Override public void exportNode(OutputStream os) throws IOException, BackingStoreException { PersistedPreferencesXmlSupport.export(os, this, false); } @Override public void exportSubtree(OutputStream os) throws IOException, BackingStoreException { PersistedPreferencesXmlSupport.export(os, this, true); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy