/* * 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 java.util.prefs; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeSet; import libcore.io.Base64; import libcore.util.EmptyArray; /** * This abstract class is a partial implementation of the abstract class * Preferences, which can be used to simplify {@code Preferences} provider's * implementation. This class defines nine abstract SPI methods, which must be * implemented by a preference provider. * * @since 1.4 * @see Preferences */ public abstract class AbstractPreferences extends Preferences { /* * ----------------------------------------------------------- * Class fields * ----------------------------------------------------------- */ /** the unhandled events collection */ private static final List events = new LinkedList(); /** the event dispatcher thread */ private static final EventDispatcher dispatcher = new EventDispatcher("Preference Event Dispatcher"); /* * ----------------------------------------------------------- * Class initializer * ----------------------------------------------------------- */ static { dispatcher.setDaemon(true); dispatcher.start(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { Preferences uroot = Preferences.userRoot(); Preferences sroot = Preferences.systemRoot(); try { uroot.flush(); } catch (BackingStoreException e) { // ignore } try { sroot.flush(); } catch (BackingStoreException e) { // ignore } } }); } /* * ----------------------------------------------------------- * Instance fields (package-private) * ----------------------------------------------------------- */ /** true if this node is in user preference hierarchy */ boolean userNode; /* * ----------------------------------------------------------- * Instance fields (private) * ----------------------------------------------------------- */ /** * The object used to lock this node. */ protected final Object lock; /** * This field is true if this node is created while it doesn't exist in the * backing store. This field's default value is false, and it is checked * when the node creation is completed, and if it is true, the node change * event will be fired for this node's parent. */ protected boolean newNode; /** cached child nodes */ private Map cachedNode; //the collections of listeners private List nodeChangeListeners; private List preferenceChangeListeners; //this node's name private String nodeName; //handler to this node's parent private AbstractPreferences parentPref; //true if this node has been removed private boolean isRemoved; //handler to this node's root node private AbstractPreferences root; /* * ----------------------------------------------------------- * Constructors * ----------------------------------------------------------- */ /** * Constructs a new {@code AbstractPreferences} instance using the given * parent node and node name. * * @param parent * the parent node of the new node or {@code null} to indicate * that the new node is a root node. * @param name * the name of the new node or an empty string to indicate that * this node is called "root". * @throws IllegalArgumentException * if the name contains a slash character or is empty if {@code * parent} is not {@code null}. */ protected AbstractPreferences(AbstractPreferences parent, String name) { if ((parent == null ^ name.length() == 0) || name.indexOf("/") >= 0) { throw new IllegalArgumentException(); } root = (parent == null) ? this : parent.root; nodeChangeListeners = new LinkedList(); preferenceChangeListeners = new LinkedList(); isRemoved = false; cachedNode = new HashMap(); nodeName = name; parentPref = parent; lock = new Object(); userNode = root.userNode; } /* * ----------------------------------------------------------- * Methods * ----------------------------------------------------------- */ /** * Returns an array of all cached child nodes. * * @return the array of cached child nodes. */ protected final AbstractPreferences[] cachedChildren() { return cachedNode.values().toArray(new AbstractPreferences[cachedNode.size()]); } /** * Returns the child node with the specified name or {@code null} if it * doesn't exist. Implementers can assume that the name supplied to this * method will be a valid node name string (conforming to the node naming * format) and will not correspond to a node that has been cached or * removed. * * @param name * the name of the desired child node. * @return the child node with the given name or {@code null} if it doesn't * exist. * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected AbstractPreferences getChild(String name) throws BackingStoreException { synchronized (lock) { checkState(); AbstractPreferences result = null; String[] childrenNames = childrenNames(); for (String childrenName : childrenNames) { if (childrenName.equals(name)) { result = childSpi(name); break; } } return result; } } /** * Returns whether this node has been removed by invoking the method {@code * removeNode()}. * * @return {@code true}, if this node has been removed, {@code false} * otherwise. */ protected boolean isRemoved() { synchronized (lock) { return isRemoved; } } /** * Flushes changes of this node to the backing store. This method should * only flush this node and should not include the descendant nodes. Any * implementation that wants to provide functionality to flush all nodes * at once should override the method {@link #flush() flush()}. * * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected abstract void flushSpi() throws BackingStoreException; /** * Returns the names of all of the child nodes of this node or an empty * array if this node has no children. The names of cached children are not * required to be returned. * * @return the names of this node's children. * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected abstract String[] childrenNamesSpi() throws BackingStoreException; /** * Returns the child preference node with the given name, creating it * if it does not exist. The caller of this method should ensure that the * given name is valid and that this node has not been removed or cached. * If the named node has just been removed, the implementation * of this method must create a new one instead of reactivating the removed * one. *

* The new creation is not required to be persisted immediately until the * flush method will be invoked. *

* * @param name * the name of the child preference to be returned. * @return the child preference node. */ protected abstract AbstractPreferences childSpi(String name); /** * Puts the given key-value pair into this node. Caller of this method * should ensure that both of the given values are valid and that this * node has not been removed. * * @param name * the given preference key. * @param value * the given preference value. */ protected abstract void putSpi(String name, String value); /** * Gets the preference value mapped to the given key. The caller of this * method should ensure that the given key is valid and that this node has * not been removed. This method should not throw any exceptions but if it * does, the caller will ignore the exception, regarding it as a {@code * null} return value. * * @param key * the given key to be searched for. * @return the preference value mapped to the given key. */ protected abstract String getSpi(String key); /** * Returns an array of all preference keys of this node or an empty array if * no preferences have been found. The caller of this method should ensure * that this node has not been removed. * * @return the array of all preference keys. * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected abstract String[] keysSpi() throws BackingStoreException; /** * Removes this node from the preference hierarchy tree. The caller of this * method should ensure that this node has no child nodes, which means the * method {@link Preferences#removeNode() Preferences.removeNode()} should * invoke this method multiple-times in bottom-up pattern. The removal is * not required to be persisted until after it is flushed. * * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected abstract void removeNodeSpi() throws BackingStoreException; /** * Removes the preference with the specified key. The caller of this method * should ensure that the given key is valid and that this node has not been * removed. * * @param key * the key of the preference that is to be removed. */ protected abstract void removeSpi(String key); /** * Synchronizes this node with the backing store. This method should only * synchronize this node and should not include the descendant nodes. An * implementation that wants to provide functionality to synchronize all * nodes at once should override the method {@link #sync() sync()}. * * @throws BackingStoreException * if the backing store is unavailable or causes an operation * failure. */ protected abstract void syncSpi() throws BackingStoreException; /* * ----------------------------------------------------------- * Methods inherited from Preferences * ----------------------------------------------------------- */ @Override public String absolutePath() { if (parentPref == null) { return "/"; } else if (parentPref == root) { return "/" + nodeName; } return parentPref.absolutePath() + "/" + nodeName; } @Override public String[] childrenNames() throws BackingStoreException { synchronized (lock) { checkState(); TreeSet result = new TreeSet(cachedNode.keySet()); String[] names = childrenNamesSpi(); for (int i = 0; i < names.length; i++) { result.add(names[i]); } return result.toArray(new String[result.size()]); } } @Override public void clear() throws BackingStoreException { synchronized (lock) { for (String key : keys()) { remove(key); } } } @Override public void exportNode(OutputStream ostream) throws IOException, BackingStoreException { if (ostream == null) { throw new NullPointerException("ostream == null"); } checkState(); XMLParser.exportPrefs(this, ostream, false); } @Override public void exportSubtree(OutputStream ostream) throws IOException, BackingStoreException { if (ostream == null) { throw new NullPointerException("ostream == null"); } checkState(); XMLParser.exportPrefs(this, ostream, true); } @Override public void flush() throws BackingStoreException { synchronized (lock) { flushSpi(); } AbstractPreferences[] cc = cachedChildren(); int i; for (i = 0; i < cc.length; i++) { cc[i].flush(); } } @Override public String get(String key, String deflt) { if (key == null) { throw new NullPointerException("key == null"); } String result = null; synchronized (lock) { checkState(); try { result = getSpi(key); } catch (Exception e) { // ignored } } return (result == null ? deflt : result); } @Override public boolean getBoolean(String key, boolean deflt) { String result = get(key, null); if (result == null) { return deflt; } if ("true".equalsIgnoreCase(result)) { return true; } else if ("false".equalsIgnoreCase(result)) { return false; } else { return deflt; } } @Override public byte[] getByteArray(String key, byte[] deflt) { String svalue = get(key, null); if (svalue == null) { return deflt; } if (svalue.length() == 0) { return EmptyArray.BYTE; } try { byte[] bavalue = svalue.getBytes(StandardCharsets.US_ASCII); if (bavalue.length % 4 != 0) { return deflt; } return Base64.decode(bavalue); } catch (Exception e) { return deflt; } } @Override public double getDouble(String key, double deflt) { String result = get(key, null); if (result == null) { return deflt; } try { return Double.parseDouble(result); } catch (NumberFormatException e) { return deflt; } } @Override public float getFloat(String key, float deflt) { String result = get(key, null); if (result == null) { return deflt; } try { return Float.parseFloat(result); } catch (NumberFormatException e) { return deflt; } } @Override public int getInt(String key, int deflt) { String result = get(key, null); if (result == null) { return deflt; } try { return Integer.parseInt(result); } catch (NumberFormatException e) { return deflt; } } @Override public long getLong(String key, long deflt) { String result = get(key, null); if (result == null) { return deflt; } try { return Long.parseLong(result); } catch (NumberFormatException e) { return deflt; } } @Override public boolean isUserNode() { return root == Preferences.userRoot(); } @Override public String[] keys() throws BackingStoreException { synchronized (lock) { checkState(); return keysSpi(); } } @Override public String name() { return nodeName; } @Override public Preferences node(String name) { AbstractPreferences startNode = null; synchronized (lock) { checkState(); validateName(name); if (name.isEmpty()) { return this; } else if ("/".equals(name)) { return root; } if (name.startsWith("/")) { startNode = root; name = name.substring(1); } else { startNode = this; } } try { return startNode.nodeImpl(name, true); } catch (BackingStoreException e) { // should not happen return null; } } private void validateName(String name) { if (name.endsWith("/") && name.length() > 1) { throw new IllegalArgumentException("Name cannot end with '/'"); } if (name.indexOf("//") >= 0) { throw new IllegalArgumentException("Name cannot contain consecutive '/' characters"); } } private AbstractPreferences nodeImpl(String path, boolean createNew) throws BackingStoreException { String[] names = path.split("/"); AbstractPreferences currentNode = this; AbstractPreferences temp; for (String name : names) { synchronized (currentNode.lock) { temp = currentNode.cachedNode.get(name); if (temp == null) { temp = getNodeFromBackend(createNew, currentNode, name); } } currentNode = temp; if (currentNode == null) { break; } } return currentNode; } private AbstractPreferences getNodeFromBackend(boolean createNew, AbstractPreferences currentNode, String name) throws BackingStoreException { if (name.length() > MAX_NAME_LENGTH) { throw new IllegalArgumentException("Name '" + name + "' too long"); } AbstractPreferences temp; if (createNew) { temp = currentNode.childSpi(name); currentNode.cachedNode.put(name, temp); if (temp.newNode && currentNode.nodeChangeListeners.size() > 0) { currentNode.notifyChildAdded(temp); } } else { temp = currentNode.getChild(name); } return temp; } @Override public boolean nodeExists(String name) throws BackingStoreException { if (name == null) { throw new NullPointerException("name == null"); } AbstractPreferences startNode = null; synchronized (lock) { if (isRemoved()) { if (name.isEmpty()) { return false; } throw new IllegalStateException("This node has been removed"); } validateName(name); if (name.isEmpty() || "/".equals(name)) { return true; } if (name.startsWith("/")) { startNode = root; name = name.substring(1); } else { startNode = this; } } try { Preferences result = startNode.nodeImpl(name, false); return (result != null); } catch(IllegalArgumentException e) { return false; } } @Override public Preferences parent() { checkState(); return parentPref; } private void checkState() { if (isRemoved()) { throw new IllegalStateException("This node has been removed"); } } @Override public void put(String key, String value) { if (key == null) { throw new NullPointerException("key == null"); } else if (value == null) { throw new NullPointerException("value == null"); } if (key.length() > MAX_KEY_LENGTH || value.length() > MAX_VALUE_LENGTH) { throw new IllegalArgumentException(); } synchronized (lock) { checkState(); putSpi(key, value); } notifyPreferenceChange(key, value); } @Override public void putBoolean(String key, boolean value) { put(key, String.valueOf(value)); } @Override public void putByteArray(String key, byte[] value) { put(key, Base64.encode(value)); } @Override public void putDouble(String key, double value) { put(key, Double.toString(value)); } @Override public void putFloat(String key, float value) { put(key, Float.toString(value)); } @Override public void putInt(String key, int value) { put(key, Integer.toString(value)); } @Override public void putLong(String key, long value) { put(key, Long.toString(value)); } @Override public void remove(String key) { synchronized (lock) { checkState(); removeSpi(key); } notifyPreferenceChange(key, null); } @Override public void removeNode() throws BackingStoreException { if (root == this) { throw new UnsupportedOperationException("Cannot remove root node"); } synchronized (parentPref.lock) { removeNodeImpl(); } } private void removeNodeImpl() throws BackingStoreException { synchronized (lock) { checkState(); String[] childrenNames = childrenNamesSpi(); for (String childrenName : childrenNames) { if (cachedNode.get(childrenName) == null) { AbstractPreferences child = childSpi(childrenName); cachedNode.put(childrenName, child); } } final Collection values = cachedNode.values(); final AbstractPreferences[] children = values.toArray(new AbstractPreferences[values.size()]); for (AbstractPreferences child : children) { child.removeNodeImpl(); } removeNodeSpi(); isRemoved = true; parentPref.cachedNode.remove(nodeName); } if (parentPref.nodeChangeListeners.size() > 0) { parentPref.notifyChildRemoved(this); } } @Override public void addNodeChangeListener(NodeChangeListener ncl) { if (ncl == null) { throw new NullPointerException("ncl == null"); } checkState(); synchronized (nodeChangeListeners) { nodeChangeListeners.add(ncl); } } @Override public void addPreferenceChangeListener(PreferenceChangeListener pcl) { if (pcl == null) { throw new NullPointerException("pcl == null"); } checkState(); synchronized (preferenceChangeListeners) { preferenceChangeListeners.add(pcl); } } @Override public void removeNodeChangeListener(NodeChangeListener ncl) { checkState(); synchronized (nodeChangeListeners) { int pos; if ((pos = nodeChangeListeners.indexOf(ncl)) == -1) { throw new IllegalArgumentException(); } nodeChangeListeners.remove(pos); } } @Override public void removePreferenceChangeListener(PreferenceChangeListener pcl) { checkState(); synchronized (preferenceChangeListeners) { int pos; if ((pos = preferenceChangeListeners.indexOf(pcl)) == -1) { throw new IllegalArgumentException(); } preferenceChangeListeners.remove(pos); } } @Override public void sync() throws BackingStoreException { synchronized (lock) { checkState(); syncSpi(); } for (AbstractPreferences child : cachedChildren()) { child.sync(); } } @Override public String toString() { return (isUserNode() ? "User" : "System") + " Preference Node: " + absolutePath(); } private void notifyChildAdded(Preferences child) { NodeChangeEvent nce = new NodeAddEvent(this, child); synchronized (events) { events.add(nce); events.notifyAll(); } } private void notifyChildRemoved(Preferences child) { NodeChangeEvent nce = new NodeRemoveEvent(this, child); synchronized (events) { events.add(nce); events.notifyAll(); } } private void notifyPreferenceChange(String key, String newValue) { PreferenceChangeEvent pce = new PreferenceChangeEvent(this, key, newValue); synchronized (events) { events.add(pce); events.notifyAll(); } } private static class EventDispatcher extends Thread { EventDispatcher(String name){ super(name); } @Override public void run() { while (true) { EventObject event; try { event = getEventObject(); } catch (InterruptedException e) { e.printStackTrace(); continue; } AbstractPreferences pref = (AbstractPreferences) event.getSource(); if (event instanceof NodeAddEvent) { dispatchNodeAdd((NodeChangeEvent) event, pref.nodeChangeListeners); } else if (event instanceof NodeRemoveEvent) { dispatchNodeRemove((NodeChangeEvent) event, pref.nodeChangeListeners); } else if (event instanceof PreferenceChangeEvent) { dispatchPrefChange((PreferenceChangeEvent) event, pref.preferenceChangeListeners); } } } private EventObject getEventObject() throws InterruptedException { synchronized (events) { if (events.isEmpty()) { events.wait(); } EventObject event = events.get(0); events.remove(0); return event; } } private void dispatchPrefChange(PreferenceChangeEvent event, List preferenceChangeListeners) { synchronized (preferenceChangeListeners) { for (EventListener preferenceChangeListener : preferenceChangeListeners) { ((PreferenceChangeListener) preferenceChangeListener).preferenceChange(event); } } } private void dispatchNodeRemove(NodeChangeEvent event, List nodeChangeListeners) { synchronized (nodeChangeListeners) { for (EventListener nodeChangeListener : nodeChangeListeners) { ((NodeChangeListener) nodeChangeListener).childRemoved(event); } } } private void dispatchNodeAdd(NodeChangeEvent event, List nodeChangeListeners) { synchronized (nodeChangeListeners) { for (EventListener nodeChangeListener : nodeChangeListeners) { NodeChangeListener ncl = (NodeChangeListener) nodeChangeListener; ncl.childAdded(event); } } } } private static class NodeAddEvent extends NodeChangeEvent { //The base class is NOT serializable, so this class isn't either. private static final long serialVersionUID = 1L; public NodeAddEvent(Preferences p, Preferences c) { super(p, c); } } private static class NodeRemoveEvent extends NodeChangeEvent { //The base class is NOT serializable, so this class isn't either. private static final long serialVersionUID = 1L; public NodeRemoveEvent(Preferences p, Preferences c) { super(p, c); } } }