// // PreferenceStore.java // Thud // // Created by Anthony Parker on Sat Dec 22 2001. // Copyright (c) 2001-2006 Anthony Parker & the THUD team. // All rights reserved. See LICENSE.TXT for more information. // package net.sourceforge.btthud.util; import net.sourceforge.btthud.data.MUPrefs; import net.sourceforge.btthud.data.MUHost; import java.awt.Point; import java.awt.Color; import java.util.ArrayList; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.prefs.Preferences; import java.util.prefs.BackingStoreException; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; public class PreferenceStore { /** * Load preferences. */ static public void load (MUPrefs prefs) { final Preferences store = getPrefStore(); for (Field field: MUPrefs.class.getFields()) { final TypeHandler handler = typeHandlerMap.get(field.getType()); if (handler == null) { System.err.println("Load error: Preference '" + field.getName() + "' is of unsupported type '" + field.getType() + "'."); continue; } handler.load(store, prefs, field); } } /** * Save preferences. */ static public void save (MUPrefs prefs) { final Preferences store = getPrefStore(); for (Field field: MUPrefs.class.getFields()) { final TypeHandler handler = typeHandlerMap.get(field.getType()); if (handler == null) { System.err.println("Save error: Preference '" + field.getName() + "' is of unsupported type '" + field.getType() + "'."); continue; } handler.save(store, prefs, field); } } // TODO: If we use preference listeners, we can do fancy things like have // multiple running copies of Thud not clobber each other's preferences. static private Preferences getPrefStore () { // Initialize the type handler map the first time it's needed. if (typeHandlerMap == null) { typeHandlerMap = new java.util.HashMap<Type, TypeHandler> (); registerTypeHandlers(); } // Because we don't want net/sourceforge/btthud/data, or ui, or etc. return Preferences.userRoot().node("net/sourceforge/btthud"); } static private Map<Type, TypeHandler> typeHandlerMap = null; static private void registerTypeHandlers() { // boolean typeHandlerMap.put(boolean.class, new BooleanTypeHandler ()); // int typeHandlerMap.put(int.class, new IntegerTypeHandler ()); // float typeHandlerMap.put(float.class, new FloatTypeHandler ()); // double typeHandlerMap.put(double.class, new DoubleTypeHandler ()); // String typeHandlerMap.put(String.class, new StringTypeHandler ()); // Point typeHandlerMap.put(Point.class, new PointTypeHandler ()); // Color typeHandlerMap.put(Color.class, new ColorTypeHandler ()); // Color[] typeHandlerMap.put(Color[].class, new ColorArrayTypeHandler ()); // ArrayList<MUHost> typeHandlerMap.put(ArrayList.class, new HostListTypeHandler ()); } } /** * Base class for TypeHandlers. Each type handler needs to provide machinery * to convert values to and from strings. More complicated types may require * more elaborate storage. */ abstract class TypeHandler { /** * Load MUPrefs field from preferences backing store. */ abstract public void load (final Preferences store, final MUPrefs prefs, final Field field); /** * Store MUPrefs field to preferences backing store. */ public void save (final Preferences store, final MUPrefs prefs, final Field field) { final Object objValue = getPrefField(prefs, field); if (objValue == null) return; savePref(store, field, objValue.toString()); } /** * Utility method for getting preference field. */ static protected Object getPrefField (final MUPrefs prefs, final Field field) { try { return field.get(prefs); } catch (IllegalAccessException e) { System.err.print("Save error: Preference '" + field.getName() + "' inaccessible."); return null; } } /** * Utility method for setting preference field. */ static protected void setPrefField (final MUPrefs prefs, final Field field, final Object value) { try { field.set(prefs, value); } catch (IllegalAccessException e) { System.err.print("Load error: Preference '" + field.getName() + "' inaccessible."); } } /** * Utility method for getting preference as a string. Returns null if * the preference does not exist. (Use the default value.) */ static protected String loadPref (final Preferences store, final Field field) { return store.get(field.getName(), null); } /** * Utility method for setting preference as a string. */ static protected void savePref (final Preferences store, final Field field, final String value) { store.put(field.getName(), value); } } /** * Sequence-type preference reader. Sequences are stored as * baseName/subName.<index>, where <index> ranges consecutively * from 0 to N - 1 (with N being the number of elements in the list). * * Note that since the sequence is stored in a subnode, names should be chosen * as not to conflict with future package names that may also want to use the * preference store. It's unlikely this will be a problem for Thud. * * The constructor may throw a BackingStoreException if it can't communicate * with the backing store, or an IllegalStateException if the sequence node * doesn't exist. */ class SequenceLoader { private final Preferences store; private final int size; public SequenceLoader (final Preferences store, final String baseName) throws BackingStoreException { if (!store.nodeExists(baseName)) { throw new IllegalStateException (baseName + " doesn't exist"); } this.store = store.node(baseName); // The "size" of this sequence is the maximum consecutive index of any // of the subnames. In other words, if we sort the keys, we should be // able to count consecutively from 0 to <size> for at least one // subname. Note that some subnames may not be valid for the full // range; this eases compatibility. final Map<String,SortedSet<Integer>> maxIndex = new java.util.HashMap<String,SortedSet<Integer>> (); for (final String key: this.store.keys()) { // Explode key. final String[] keySplit = key.split("\\.", 2); if (keySplit.length != 2) { // Not an index key, ignore. System.err.println("Load warning: Preference '" + baseName + "[]': Unindexed element '" + key + "'."); continue; } int tmpIndex; try { tmpIndex = Integer.parseInt(keySplit[0]); } catch (NumberFormatException e) { // Bad index. System.err.println("Load warning: Preference '" + baseName + "[]': Unindexed element '" + key + "'."); continue; } // Tally index. SortedSet<Integer> indexSet = maxIndex.get(keySplit[1]); if (indexSet == null) { indexSet = new java.util.TreeSet<Integer> (); maxIndex.put(keySplit[1], indexSet); } indexSet.add(tmpIndex); } // Compute size. int tmpSize = 0; for (final SortedSet<Integer> indexSet: maxIndex.values()) { int lastIndex = -1; for (int index: indexSet) { if (index != lastIndex + 1) break; lastIndex = index; } if (tmpSize <= lastIndex) tmpSize = lastIndex + 1; } size = tmpSize; } public int size () { return size; } public List<String> loadSequence (final String subName) { final List<String> valueList = new java.util.LinkedList<String> (); for (int ii = 0; ii < size(); ii++) { final String value = store.get(ii + "." + subName, null); if (value == null) break; valueList.add(value); } return valueList; } } /** * Sequence-type preference writer. Sequences are stored as * baseName/subName.<index>, where <index> ranges consecutively * from 0 to N - 1 (with N being the number of elements in the list). * * Note that since the sequence is stored in a subnode, names should be chosen * as not to conflict with future package names that may also want to use the * preference store. It's unlikely this will be a problem for Thud. * * The constructor may throw a BackingStoreException if it can't communicate * with the backing store. * * The constructor clears the entire contents of the associated preference * node. */ class SequenceStorer { private final Preferences store; public SequenceStorer (final Preferences store, final String baseName) throws BackingStoreException { this.store = store.node(baseName); // TODO: Instead of clearing all preferences, we might just want to // clear those keys which match the <index>.subkey template. this.store.clear(); } public void saveSequence (final String subName, final List<String> valueList) { int ii = 0; for (String value: valueList) { assert value != null; store.put(ii++ + "." + subName, value); } } } /** * Boolean type handler. */ class BooleanTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String strValue = loadPref(store, field); if (strValue == null) return; setPrefField(prefs, field, new Boolean (strValue)); } } /** * Integer type handler. */ class IntegerTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String strValue = loadPref(store, field); if (strValue == null) return; setPrefField(prefs, field, new Integer (strValue)); } } /** * Float type handler. */ class FloatTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String strValue = loadPref(store, field); if (strValue == null) return; setPrefField(prefs, field, new Float (strValue)); } } /** * Double type handler. */ class DoubleTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String strValue = loadPref(store, field); if (strValue == null) return; setPrefField(prefs, field, new Double (strValue)); } } /** * String type handler. */ class StringTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String strValue = loadPref(store, field); if (strValue == null) return; setPrefField(prefs, field, strValue); } } /** * Point compound type handler. */ class PointTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String xStrValue = store.get(field.getName() + ".x", null); final String yStrValue = store.get(field.getName() + ".y", null); if (xStrValue == null || yStrValue == null) return; final int xValue = Integer.valueOf(xStrValue); final int yValue = Integer.valueOf(yStrValue); setPrefField(prefs, field, new Point (xValue, yValue)); } public void save (final Preferences store, final MUPrefs prefs, final Field field) { final Point value = (Point)getPrefField(prefs, field); if (value == null) return; store.put(field.getName() + ".x", Integer.toString(value.x)); store.put(field.getName() + ".y", Integer.toString(value.y)); } } /** * Color compound type handler. This class shows a bit of the problem with the * whole approach of creating our own type handlers, as color has a complex * internal representation. We could serialize out to the preferences store, * but we'll just assume we always use 8-bit sRGB color with alpha. * * Serializing also has the disadvantage that it makes it hard for humans to * edit the preferences. */ class ColorTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { final String rStrValue = store.get(field.getName() + ".r", null); final String gStrValue = store.get(field.getName() + ".g", null); final String bStrValue = store.get(field.getName() + ".b", null); final String aStrValue = store.get(field.getName() + ".a", null); if (rStrValue == null || gStrValue == null || bStrValue == null || aStrValue == null) return; final int rValue = Integer.valueOf(rStrValue); final int gValue = Integer.valueOf(gStrValue); final int bValue = Integer.valueOf(bStrValue); final int aValue = Integer.valueOf(aStrValue); setPrefField(prefs, field, new Color (rValue, gValue, bValue, aValue)); } public void save (final Preferences store, final MUPrefs prefs, final Field field) { final Color value = (Color)getPrefField(prefs, field); if (value == null) return; store.put(field.getName() + ".r", Integer.toString(value.getRed())); store.put(field.getName() + ".g", Integer.toString(value.getGreen())); store.put(field.getName() + ".b", Integer.toString(value.getBlue())); store.put(field.getName() + ".a", Integer.toString(value.getAlpha())); } } /** * Terrain colors Color[] array type handler. * * TODO: Make this more generic, or the type more specific. */ class ColorArrayTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { SequenceLoader colorLoader; try { colorLoader = new SequenceLoader (store, field.getName()); } catch (BackingStoreException e) { // Can't get node keys. System.err.println("Load error: Preference '" + field.getName() + "[]': " + e); return; } catch (IllegalStateException e) { // Missing node. return; } final List<String> rList = colorLoader.loadSequence("r"); final List<String> gList = colorLoader.loadSequence("g"); final List<String> bList = colorLoader.loadSequence("b"); final List<String> aList = colorLoader.loadSequence("a"); if (rList.size() != gList.size() || rList.size() != bList.size() || rList.size() != aList.size()) { System.err.println("Load error: Preference '" + field.getName() + "[]' has mismatched elements."); return; } // FIXME: Generalize the array length check. // We assume we can upgrade if there are more terrain colors later. if (rList.size() > prefs.terrainColors.length) { System.err.println("Load error: Preference '" + field.getName() + "' has too many elements."); return; } // FIXME: Generalize copy. final Color[] colorArray = prefs.terrainColors.clone(); final Iterator<String> rIter = rList.iterator(); final Iterator<String> gIter = gList.iterator(); final Iterator<String> bIter = bList.iterator(); final Iterator<String> aIter = aList.iterator(); for (int ii = 0; ii < rList.size(); ii++) { try { final int r = Integer.parseInt(rIter.next()); final int g = Integer.parseInt(gIter.next()); final int b = Integer.parseInt(bIter.next()); final int a = Integer.parseInt(aIter.next()); colorArray[ii] = new Color (r, g, b, a); } catch (NumberFormatException e) { System.err.println("Load error: Preference '" + field.getName() + "' has malformed element."); return; } } setPrefField(prefs, field, colorArray); } public void save (final Preferences store, final MUPrefs prefs, final Field field) { final Color[] colorArray = (Color[])getPrefField(prefs, field); if (colorArray == null) return; SequenceStorer colorStorer; try { colorStorer = new SequenceStorer (store, field.getName()); } catch (BackingStoreException e) { // Can't clear removed node. System.err.println("Save error: Preference '" + field.getName() + "[]': " + e); return; } final List<String> rList = new java.util.LinkedList<String> (); final List<String> gList = new java.util.LinkedList<String> (); final List<String> bList = new java.util.LinkedList<String> (); final List<String> aList = new java.util.LinkedList<String> (); for (Color color: colorArray) { rList.add(Integer.toString(color.getRed())); gList.add(Integer.toString(color.getGreen())); bList.add(Integer.toString(color.getBlue())); aList.add(Integer.toString(color.getAlpha())); } colorStorer.saveSequence("r", rList); colorStorer.saveSequence("g", gList); colorStorer.saveSequence("b", bList); colorStorer.saveSequence("a", aList); } } /** * ArrayList<MUHost> type handler. * * TODO: Make this more geneirc, or the type more specific. */ class HostListTypeHandler extends TypeHandler { public void load (final Preferences store, final MUPrefs prefs, final Field field) { SequenceLoader hostLoader; try { hostLoader = new SequenceLoader (store, field.getName()); } catch (BackingStoreException e) { // Can't get node keys. System.err.println("Load error: Preference '" + field.getName() + "[]': " + e); return; } catch (IllegalStateException e) { // Missing node. return; } final List<String> hostList = hostLoader.loadSequence("host"); final List<String> portList = hostLoader.loadSequence("port"); if (hostList.size() != portList.size()) { System.err.println("Load error: Preference '" + field.getName() + "[]' has mismatched elements."); return; } final ArrayList<MUHost> hostArray = new ArrayList<MUHost> (); final Iterator<String> hostIter = hostList.iterator(); final Iterator<String> portIter = portList.iterator(); for (int ii = 0; ii < hostList.size(); ii++) { try { final String host = hostIter.next(); final int port = Integer.parseInt(portIter.next()); hostArray.add(new MUHost (host, port)); } catch (NumberFormatException e) { System.err.println("Load error: Preference '" + field.getName() + "' has malformed element."); return; } } setPrefField(prefs, field, hostArray); } public void save (final Preferences store, final MUPrefs prefs, final Field field) { final ArrayList<MUHost> hostArray = (ArrayList<MUHost>)getPrefField(prefs, field); if (hostArray == null) return; SequenceStorer hostStorer; try { hostStorer = new SequenceStorer (store, field.getName()); } catch (BackingStoreException e) { // Can't clear removed node. System.err.println("Save error: Preference '" + field.getName() + "[]': " + e); return; } final List<String> hostList = new java.util.LinkedList<String> (); final List<String> portList = new java.util.LinkedList<String> (); for (MUHost host: hostArray) { hostList.add(host.getHost()); portList.add(Integer.toString(host.getPort())); } hostStorer.saveSequence("host", hostList); hostStorer.saveSequence("port", portList); } }