jarrodhroberson
7/16/2013 - 7:17 PM

More advanced and feature rich Preferences object than java.util.Preferences, and one that is compatible with Google App Engine.

More advanced and feature rich Preferences object than java.util.Preferences, and one that is compatible with Google App Engine.

package com.vertigrated.prefs;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.gm.gbrd.FormattedRuntimeException;
import com.gm.gbrd.collections.StringArrayList;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;

/**
 * This class is based heavily on the java.util.Preferences class, but with specific enhancements
 * to overcome some implementation specific limitations of the Preferences class, which should have been
 * an interface instead of an abstract class.
 */
@JsonSerialize(using = PreferencesSerializer.class)
@JsonDeserialize(using = PreferencesDeserializer.class)
public class Preferences
{
    private static Preferences fromFile(@Nonnull final File f)
    {
        try
        {
            final Preferences p = fromInputStream(new FileInputStream(f));
            p.setFlushHandler(new FlushHandler() {
                @Override
                public void flush()
                {
                    try
                    {
                        final FileOutputStream fos = new FileOutputStream(f);
                        final BufferedOutputStream bos = new BufferedOutputStream(fos);
                        bos.write(this.toString().getBytes());
                    }
                    catch (final FileNotFoundException e)
                    {
                        throw new RuntimeException(e);
                    }
                    catch (final IOException e)
                    {
                        throw new RuntimeException(e);
                    }
                }
            });
            p.setSyncHandler(new SyncHandler() {
                @Override
                public void sync()
                {
                    try
                    {
                        final FileInputStream fis = new FileInputStream(f);
                        final Preferences np = fromInputStream(fis);
                        p.merge(np);
                    }
                    catch (final FileNotFoundException e)
                    {
                        throw new RuntimeException(e);
                    }
                }
            });
            return p;
        }
        catch (FileNotFoundException e)
        {
            return new Preferences();
        }
    }

    private static Preferences fromInputStream(@Nonnull final InputStream is)
    {
        try
        {
            final ObjectMapper mapper = new ObjectMapper();
            if (is instanceof  BufferedInputStream) { return mapper.readValue(is, Preferences.class); }
            else { return mapper.readValue(new BufferedInputStream(is), Preferences.class); }
        }
        catch (IOException e)
        {
            return new Preferences();
        }
    }

    private static Preferences fromString(@Nonnull final String json)
    {
        try
        {
            final ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(json, Preferences.class);
        }
        catch (IOException e)
        {
            return new Preferences();
        }
    }

    public static Preferences getApplicationRoot(@Nonnull final String application)
    {
        final Preferences classPath = fromInputStream(ClassLoader.getSystemResourceAsStream(application));
        final String OS = System.getProperty("os.name").toLowerCase();
        final Preferences system;
        if (OS.contains("nix") || OS.contains("nux"))
        {
            system = fromFile(new File("/etc/" + application));
            final Preferences var = fromFile(new File("/var/" + application));
            system.merge(var);
        }
        else { system = new Preferences(); }
        final Preferences userHome = fromFile(new File(System.getProperty("user.home") + "/" + application));
        final Preferences currentDir = fromFile(new File(System.getProperty("user.dir") + "/" + application));
        final Preferences merged = new Preferences();
        merged.merge(classPath, system, userHome, currentDir);
        return merged;
    }

    private final SimpleDateFormat dateFormat;
    private final SimpleDateFormat timeFormat;
    private final SimpleDateFormat timestampFormat;
    private final String name;
    private final Map<String, String> prefs;
    private final Map<String, Preferences> children;
    private final List<NodeChangeListener> nodeChangeListeners;
    private final List<PreferenceChangeListener> preferenceChangeListeners;
    private Preferences parent;
    private FlushHandler flusher;
    private SyncHandler syncer;

    Preferences()
    {
        this(null, "/");
    }

    Preferences(@Nullable final Preferences parent, @Nonnull final String name)
    {
        this.dateFormat = new SimpleDateFormat("yyyy/MM/dd");
        this.timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
        this.timestampFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS Z");
        this.timestampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        this.parent = parent;
        if (this.parent != null && name.contains("/")) { throw new IllegalArgumentException("name can not contain \"/\""); }
        this.name = name;
        this.prefs = new TreeMap<String, String>();
        this.children = new TreeMap<String, Preferences>();
        this.nodeChangeListeners = new ArrayList<NodeChangeListener>();
        this.preferenceChangeListeners = new ArrayList<PreferenceChangeListener>();
    }

    Preferences getRoot()
    {
        if (this.parent != null )
        {
            return this.parent().getRoot();
        }
        else
        {
            return this;
        }
    }

    private void setFlushHandler(@Nonnull final FlushHandler flusher) { this.flusher = flusher; }

    private void setSyncHandler(@Nonnull final SyncHandler syncer) { this.syncer = syncer; }

    public Preferences merge(@Nonnull final Preferences ... prefs)
    {
        for (final Preferences p : prefs)
        {
            this.merge(p);
        }
        return this;
    }

    public void put(final String key, final String value)
    {
        this.prefs.put(key, value);
        this.notifyPreferenceChangeListeners(key, value, this);
    }

    public String get(@Nonnull final String key)
    {
        return this.get(key, null);
    }

    public String get(@Nonnull final String key, @Nullable final String value)
    {
        if (this.prefs.containsKey(key))
        { return this.prefs.get(key); }
        else
        { return value; }
    }

    public void remove(final String key)
    {
        this.prefs.remove(key);
        this.notifyPreferenceChangeListeners(key, null, this);
    }

    public void clear()
    {
        this.prefs.clear();
        this.notifyPreferenceChangeListeners(this.name, null, this);
    }

    public void putInt(final String key, final Integer value)
    {
        this.prefs.put(key, value.toString());
        this.notifyPreferenceChangeListeners(key, value.toString(), this);
    }

    public int getInt(final String key, final Integer value)
    {
        if (this.prefs.containsKey(key))
        { return Integer.parseInt(this.prefs.get(key)); }
        else
        { return value; }
    }

    public void putDate(@Nonnull final String key, @Nonnull final Date date)
    {
        this.prefs.put(key, this.dateFormat.format(date));
        this.notifyPreferenceChangeListeners(key, this.prefs.get(key), this);
    }

    public Date getDate(@Nonnull String key)
    {
        try
        {
            return this.dateFormat.parse(this.prefs.get(key));
        }
        catch (ParseException e)
        {
            throw new RuntimeException(e);
        }
    }

    public void putTime(@Nonnull final String key, @Nonnull final Date date)
    {
        this.prefs.put(key, this.timeFormat.format(date));
        this.notifyPreferenceChangeListeners(key, this.prefs.get(key), this);
    }

    public Date getTime(@Nonnull final String key)
    {
        try
        {
            return this.timeFormat.parse(this.prefs.get(key));
        }
        catch (ParseException e)
        {
            throw new RuntimeException(e);
        }
    }

    public void putTimeStamp(@Nonnull final String key, @Nonnull final Date date)
    {
        this.prefs.put(key, this.timestampFormat.format(date));
        this.notifyPreferenceChangeListeners(key, this.prefs.get(key), this);
    }

    public Date getTimeStamp(@Nonnull final String key)
    {
        try
        {
            return this.timestampFormat.parse(this.prefs.get(key));
        }
        catch (ParseException e)
        {
            throw new RuntimeException(e);
        }
    }

    public void putLong(final String key, final Long value)
    {
        this.prefs.put(key, value.toString());
        this.notifyPreferenceChangeListeners(key, value.toString(), this);
    }

    public long getLong(final String key, final Long value)
    {
        if (this.prefs.containsKey(key))
        { return Long.parseLong(this.prefs.get(key)); }
        else
        { return value; }
    }

    public void putBoolean(final String key, final Boolean value)
    {
        this.prefs.put(key, value.toString());
        this.notifyPreferenceChangeListeners(key, value.toString(), this);
    }

    public boolean getBoolean(final String key, final Boolean value)
    {
        if (this.prefs.containsKey(key))
        { return Boolean.parseBoolean(this.prefs.get(key)); }
        else
        { return value; }
    }

    public void putFloat(final String key, final Float value)
    {
        this.prefs.put(key, value.toString());
        this.notifyPreferenceChangeListeners(key, value.toString(), this);
    }

    public float getFloat(final String key, final Float value)
    {
        if (this.prefs.containsKey(key))
        { return Float.parseFloat(this.prefs.get(key)); }
        else
        { return value; }
    }

    public void putDouble(final String key, final Double value)
    {
        this.prefs.put(key, value.toString());
        this.notifyPreferenceChangeListeners(key, value.toString(), this);
    }

    public double getDouble(final String key, final Double value)
    {
        if (this.prefs.containsKey(key))
        { return Double.parseDouble(this.prefs.get(key)); }
        else
        { return value; }
    }

    public void putByteArray(final String key, final byte[] bytes)
    {
        final BASE64Encoder b64e = new BASE64Encoder();
        this.prefs.put(key, b64e.encode(bytes));
    }

    public byte[] getByteArray(final String key, final byte[] bytes)
    {
        try
        {
            final BASE64Decoder b64d = new BASE64Decoder();
            return b64d.decodeBuffer(this.prefs.get(key));
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Set<String> keys()
    {
        return this.prefs.keySet();
    }

    public Set<String> childrenNames()
    {
        return this.children.keySet();
    }

    public Preferences parent()
    {
        return this.parent;
    }

    public synchronized Preferences node(@Nonnull final String key)
    {
        if (key.length() > 1 && key.startsWith("/")) { this.getRoot().node(key); }
        if (key.contains("/"))
        {
            Preferences p;
            final StringArrayList path = new StringArrayList(Arrays.asList(key.split("/")));
            return this.node(path);
        }
        if (this.children.containsKey(key)) { return this.children.get(key); }
        else
        {
            final Preferences node = new Preferences(this, key);
            this.children.put(key, node);
            this.notifyChildAdded(this, node);
            return node;
        }
    }

    private Preferences node(@Nonnull final List<String> path)
    {
        if (this.nodeExists(path.get(0))) { return this.node(path.subList(1,path.size()-1)); }
        throw new IllegalArgumentException(path.get(0) + "does not exist and will not be automatically created when searching by path " + path);
    }

    public synchronized void removeNode()
    {
         for (final String k : this.children.keySet())
        {
            this.children.get(k).removeNode();
            this.notifyChildRemoved(this, this.children.get(k));
        }
        this.children.clear();
        this.parent.removeNode(this.name);
        this.parent = null;
    }

    private void removeNode(@Nonnull final String key)
    {
        this.children.remove(key);
    }

    public boolean nodeExists(final String key)
    {
        return this.children.containsKey(key);
    }

    public String name()
    {
        return this.name;
    }

    public String absolutePath()
    {
        if (this.parent == null) { return "/"; }
        else { return String.format("%s/%s", this.parent.absolutePath(), this.name); }
    }

    public synchronized void flush()
    {
        if (this.flusher != null) { flusher.flush(); }
        else { throw new UnsupportedOperationException("Preferences.sync is not implemented!"); }
    }

    public synchronized void sync()
    {
        if (this.syncer != null) { syncer.sync(); }
        else { throw new UnsupportedOperationException("Preferences.flush is not implemented!"); }
    }

    public void addPreferenceChangeListener(final PreferenceChangeListener listener)
    {
        this.preferenceChangeListeners.add(listener);
    }

    public void removePreferenceChangeListener(final PreferenceChangeListener listener)
    {
        this.preferenceChangeListeners.remove(listener);
    }

    private void notifyPreferenceChangeListeners(@Nonnull final String key, @Nullable final String newValue, @Nonnull final Preferences node)
    {
        final PreferenceChangeEvent pce = new PreferenceChangeEvent(key, newValue, node);
        for (final PreferenceChangeListener pcl : this.preferenceChangeListeners)
        {
            pcl.preferenceChange(pce);
        }
    }

    public void addNodeChangeListener(final NodeChangeListener listener)
    {
        this.nodeChangeListeners.add(listener);
    }

    public void removeNodeChangeListener(final NodeChangeListener listener)
    {
        this.nodeChangeListeners.remove(listener);
    }

    private void notifyChildAdded(@Nonnull final Preferences parent, @Nonnull final Preferences child)
    {
        final NodeChangeEvent nce = new NodeChangeEvent(parent, child);
        for (final NodeChangeListener ncl : this.nodeChangeListeners)
        {
           ncl.childAdded(nce);
        }
    }

    private void notifyChildRemoved(@Nonnull final Preferences parent, @Nonnull final Preferences child)
    {
        final NodeChangeEvent nce = new NodeChangeEvent(parent, child);
        for (final NodeChangeListener ncl : this.nodeChangeListeners)
        {
            ncl.childRemoved(nce);
        }
    }

    Preferences merge(@Nonnull final Preferences p)
    {
        for (final String key : p.prefs.keySet())
        {
            this.put(key, p.prefs.get(key));
        }
        for (final String key : p.children.keySet())
        {
            this.node(key).merge(p.children.get(key));
        }
        return this;
    }

    @Override
    public boolean equals(final Object o)
    {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }

        final Preferences that = (Preferences) o;

        if (!children.equals(that.children)) { return false; }
        if (!name.equals(that.name)) { return false; }
        if (parent != null ? !parent.equals(that.parent) : that.parent != null) { return false; }
        return prefs.equals(that.prefs);

    }

    @Override
    public int hashCode()
    {
        int result = parent != null ? parent.hashCode() : 0;
        result = 31 * result + name.hashCode();
        result = 31 * result + prefs.hashCode();
        result = 31 * result + children.hashCode();
        return result;
    }

    @Override
    public String toString()
    {
        try
        {
            final ObjectMapper m = new ObjectMapper();
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            m.writerWithDefaultPrettyPrinter().writeValue(baos, this);
            return baos.toString("UTF-8");
        }
        catch (final IOException e)
        {
            throw new FormattedRuntimeException(e, "Could not convert %s to JSON because of %s", this.getClass(), e.getMessage());
        }
    }

    protected interface FlushHandler
    {
        public void flush();
    }

    protected interface SyncHandler
    {
        public void sync();
    }
}
package com.vertigrated.prefs

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

public class PreferencesSerializer extends JsonSerializer<Preferences>
{
    @Override
    public void serialize(final Preferences value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException
    {
        jgen.useDefaultPrettyPrinter();
        jgen.writeStartObject();
        for (final String key : value.keys())
        {
            jgen.writeObjectField(key, value.get(key, "null"));
        }
        for (final String cn : value.childrenNames())
        {
            jgen.writeObjectField(cn, value.node(cn));
        }
        jgen.writeEndObject();
    }
}
package com.vertigrated.prefs;


import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import javax.annotation.Nonnull;
import java.io.IOException;

public class PreferencesDeserializer extends JsonDeserializer<Preferences>
{

    @Override
    public Preferences deserialize(final JsonParser jp, final DeserializationContext context) throws IOException, JsonProcessingException
    {
        if (jp.getCurrentToken() != JsonToken.START_OBJECT)
        {
            throw new IOException("Does not appear to be a JSON Object");
        }

        return this.parse(new Preferences(), jp).getRoot();
    }

    private Preferences parse(@Nonnull final Preferences p, @Nonnull final JsonParser jp)
    {
        try
        {
            while (jp.nextToken() != JsonToken.END_OBJECT)
            {
                final String key;
                if (jp.getCurrentToken() == JsonToken.FIELD_NAME)
                {
                    key = jp.getText();
                    jp.nextToken();
                    if (jp.getCurrentToken() == JsonToken.VALUE_STRING)
                    {
                        p.put(key, jp.getValueAsString());
                        return this.parse(p, jp);
                    }
                    else if (jp.getCurrentToken() == JsonToken.START_OBJECT)
                    {
                        return this.parse(p.node(key), jp);
                    }
                }
            }
            return p.parent() == null ? p : parse(p.parent(), jp);
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }
}
package com.vertigrated.prefs;


import javax.annotation.Nonnull;

public interface PreferenceChangeListener
{
    public void preferenceChange(@Nonnull final PreferenceChangeEvent event);
}
package com.vertigrated.prefs;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.gm.gbrd.FormattedRuntimeException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.EventObject;

public class PreferenceChangeEvent extends EventObject
{
    private final String key;
    private final String newValue;
    private final Preferences node;

    public PreferenceChangeEvent(@Nonnull final String key, @Nullable final String newValue, @Nonnull final Preferences node)
    {
        super(node);
        this.key = key;
        this.newValue = newValue;
        this.node = node;
    }

    public String getKey()
    {
        return key;
    }

    public String getNewValue()
    {
        return newValue;
    }

    public Preferences getNode()
    {
        return node;
    }

    @Override
    public boolean equals(final Object o)
    {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        final PreferenceChangeEvent that = (PreferenceChangeEvent) o;
        if (!key.equals(that.key)) { return false; }
        if (newValue != null ? !newValue.equals(that.newValue) : that.newValue != null) { return false; }
        if (!node.equals(that.node)) { return false; }
        return true;
    }

    @Override
    public int hashCode()
    {
        int result = key.hashCode();
        result = 31 * result + (newValue != null ? newValue.hashCode() : 0);
        result = 31 * result + node.hashCode();
        return result;
    }

    @Override
    public String toString()
    {
        try
        {
            final ObjectMapper m = new ObjectMapper();
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            m.writeValue(baos, this);
            return baos.toString("UTF-8");
        }
        catch (final IOException e)
        {
            throw new FormattedRuntimeException(e, "Could not convert %s to JSON because of %s", this.getClass(), e.getMessage());
        }
    }
}
package com.vertigrated.prefs;

import javax.annotation.Nonnull;

public interface NodeChangeListener
{
    public void childAdded(@Nonnull final NodeChangeEvent event);

    public void childRemoved(@Nonnull final NodeChangeEvent event);
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gm.gbrd.FormattedRuntimeException;

import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.EventObject;

public class NodeChangeEvent extends EventObject
{
    private final Preferences parent;
    private final Preferences child;

    public NodeChangeEvent(@Nonnull final Preferences parent, @Nonnull final Preferences child)
    {
        super(parent);
        this.parent = parent;
        this.child = child;
    }

    public Preferences getParent()
    {
        return parent;
    }

    public Preferences getChild()
    {
        return child;
    }

    @Override
    public boolean equals(final Object o)
    {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }

        final NodeChangeEvent event = (NodeChangeEvent) o;

        if (!child.equals(event.child)) { return false; }
        if (!parent.equals(event.parent)) { return false; }

        return true;
    }

    @Override
    public int hashCode()
    {
        int result = parent.hashCode();
        result = 31 * result + child.hashCode();
        return result;
    }

    @Override
    public String toString()
    {
        try
        {
            final ObjectMapper m = new ObjectMapper();
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            m.writeValue(baos, this);
            return baos.toString("UTF-8");
        }
        catch (final IOException e)
        {
            throw new FormattedRuntimeException(e, "Could not convert %s to JSON because of %s", this.getClass(), e.getMessage());
        }
    }
}