Skip to content

Instantly share code, notes, and snippets.

@jarrodhroberson
Last active December 19, 2015 20:09
Show Gist options
  • Save jarrodhroberson/6011734 to your computer and use it in GitHub Desktop.
Save jarrodhroberson/6011734 to your computer and use it in GitHub Desktop.
More advanced and feature rich Preferences object than java.util.Preferences, and one that is compatible with Google App Engine.
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());
}
}
}
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);
}
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 PreferenceChangeListener
{
public void preferenceChange(@Nonnull final PreferenceChangeEvent event);
}
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 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.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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment