Created
January 15, 2016 12:29
-
-
Save lmihalkovic/5288c576692a4fc07f23 to your computer and use it in GitHub Desktop.
A small KeyValues store to replace the internal details of PreferencesMap and PreferencesData
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package processing.app; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Set; | |
/** | |
* A {@code KeyValueStore} implementation that delegates to a default store when a | |
* value is not found locally. Values added to the store are added locally leaving | |
* the defaults untouched. This is particularly usefull for implementing a tiered | |
* scheme where different levels of overrides can be read from different files, while | |
* giving user the impression of a flat persistance store. Alternatively this can | |
* also be used to implement a commit/rollback scheme where changes can be stored | |
* locally before being merged into the backing store (usefull for a apply/cancel | |
* scheme that would work accross multiple screens in a preferences dialog). | |
*/ | |
public class KeyValueDelegatingStore extends KeyValueStore { | |
private KeyValueStore defaults; | |
private final Map<String, String> store; | |
public KeyValueDelegatingStore(KeyValueStore defaults) { | |
store = new HashMap<>(); | |
setDefaults(defaults); | |
} | |
public KeyValueDelegatingStore(Map<String, String> store, KeyValueStore defaults) { | |
this.store = store; | |
setDefaults(defaults); | |
} | |
protected void setDefaults(KeyValueStore defaults) { | |
this.defaults = defaults; | |
} | |
@Override | |
protected Set<String> _keys() { | |
Set<String> keys = new HashSet<>(); | |
keys.addAll(defaults._keys()); | |
keys.addAll(store.keySet()); | |
return keys; | |
} | |
@Override | |
protected String _get(String key) { | |
if(store.containsKey(key)) return store.get(key); | |
return defaults.get(key); | |
} | |
@Override | |
protected String _put(String key, String value) { | |
return store.put(key, value); | |
} | |
@Override | |
protected boolean _has(String key) { | |
if(store.containsKey(key)) return true; | |
return defaults._has(key); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package processing.app; | |
import java.io.File; | |
import java.util.Set; | |
import java.util.TreeSet; | |
/** | |
* Base implementation for a simple Key/Value store. This class defines a low level | |
* SPI that needs to be implemented by concrete Key/Value stores. Until Java allows | |
* protected interface members, this is best implemented with a abstract class rather | |
* than using default methods on an interface. | |
*/ | |
public abstract class KeyValueStore { | |
protected abstract Set<String> _keys(); | |
protected abstract String _get(String key); | |
protected abstract String _put(String key, String value); | |
protected abstract boolean _has(String key); | |
protected int _size() { return _keys().size(); } | |
public String get(String key) { | |
return _get(key); | |
} | |
public String put(String key, String value) { | |
return _put(key, value); | |
} | |
public String get(String key, String defaultValue) { | |
String value = _get(key); | |
if (value != null) { | |
return value; | |
} | |
return defaultValue; | |
} | |
public boolean getBoolean(String key) { | |
return Boolean.valueOf(_get(key)); | |
} | |
public boolean putBoolean(String key, boolean value) { | |
String prev = _put(key, value ? "true" : "false"); | |
return Boolean.valueOf(prev); | |
} | |
public File getFile(String key) { | |
if (!_has(key)) | |
return null; | |
String path = _get(key).trim(); | |
if (path.length() == 0) | |
return null; | |
return new File(path); | |
} | |
public File getFile(String key, String subFolder) { | |
File file = getFile(key); | |
if (file == null) | |
return null; | |
return new File(file, subFolder); | |
} | |
public String toString(String indent, boolean sorted) { | |
String res = indent + "{\n"; | |
Set<String> keys = sorted ? new TreeSet<String>(_keys()) : _keys(); | |
for (String k : keys) | |
res += indent + k + " = " + _get(k) + "\n"; | |
res += indent + "}"; | |
return res; | |
} | |
public String toString(String indent) { | |
return toString(indent, false); | |
} | |
@Override | |
public String toString() { | |
return toString("", false); | |
} | |
public int size() { | |
return _size(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package processing.app; | |
import java.io.BufferedReader; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InputStreamReader; | |
import java.util.LinkedHashMap; | |
import java.util.Map; | |
import java.util.function.BiFunction; | |
import processing.app.helpers.OSUtils; | |
public class KeyValueStores { | |
private KeyValueStores() { /* CANNOT CREATE */ } | |
private static final boolean isLinux = OSUtils.isLinux(); | |
private static final boolean isWindows = OSUtils.isWindows(); | |
private static final boolean isMacOS = OSUtils.isMacOS(); | |
/*private*/ static final BiFunction<String, String, String> FILTER_ACCEPT_ALL = (key, value) -> key; | |
/*private*/ static final BiFunction<String, String, String> FILTER_ACTIVE_PLATFORM_KEYS = (key, value) -> { | |
key = conditionalSuffix(key, ".linux", isLinux); | |
key = conditionalSuffix(key, ".windows", isWindows); | |
key = conditionalSuffix(key, ".macosx", isMacOS); | |
// if (key != null) | |
return key; | |
}; | |
public static KeyValueStore fromFile(File file) throws IOException { | |
try (FileInputStream fileInputStream = new FileInputStream(file)) { | |
return fromStream(fileInputStream, null); | |
} | |
} | |
public static KeyValueStore fromStoreWithFilter(KeyValueStore store, BiFunction<String, String, String> keyMapper) { | |
Map<String, String> values = new LinkedHashMap<>(); | |
for(String key : store._keys()) { | |
String value = store.get(key); | |
String mapped = keyMapper.apply(key, value); | |
if(mapped != null) { | |
values.put(mapped, value); | |
} | |
} | |
return new KeyValueStoreImpl(values); | |
} | |
public static KeyValueStore platformSpecificFromStream(InputStream input) throws IOException { | |
return fromStream(input, FILTER_ACTIVE_PLATFORM_KEYS); | |
} | |
public static KeyValueStore fromStream(InputStream input) throws IOException { | |
return new KeyValueStoreImpl( loadValues(input, FILTER_ACCEPT_ALL) ); | |
} | |
public static KeyValueStore fromStream(InputStream input, BiFunction<String, String, String> mapper) throws IOException { | |
return new KeyValueStoreImpl( loadValues(input, mapper) ); | |
} | |
public static KeyValueStore overriding(KeyValueStore store, Map<String, String> values) { | |
// KeyValueStore def = new KeyValueStoreImpl(values); | |
return new KeyValueDelegatingStore(values, store); | |
} | |
/** | |
* Return a filter that will map a key name to a substring starting with a given {@code prefix}. | |
* The filter will return null if the key does not start with the prefix, and the remainder of | |
* the original key name without the shared prefrix in case of a positive match. | |
* | |
* <p>Given | |
* <pre> | |
* Map ( | |
* alpha = Alpha | |
* alpha.somekey = v1 | |
* alpha.some.keys = v2 | |
* alpha.other.keys = v3 | |
* beta = Beta | |
* beta.some.keys = v3 | |
* ) | |
* </pre> | |
* | |
* a call to keyPrefixMapper("alpha") will generate the following result: | |
* | |
* <pre> | |
* Map ( | |
* somekey = v1 | |
* some.keys = v2 | |
* other.keys = v3 | |
* ) | |
* </pre> | |
* | |
*/ | |
public static final BiFunction<String, String, String> keyPrefixMapper(String prefix) { | |
String filter = prefix.endsWith(".") ? prefix : prefix + "."; | |
int length = filter.length(); | |
return new BiFunction<String, String, String>() { | |
@Override | |
public String apply(String key, String value) { | |
if(key.startsWith(filter)) { | |
return key.substring(length); | |
} | |
return null; // ignore | |
} | |
}; | |
} | |
/** | |
* Return a filter that will perform the same matching as performed by {@link #keyPrefixMapper(String)}, | |
* as well a verify that the length of the remainder of the key matched the value of the {@code length} | |
* parameter (length to be understood as 'number of parts when breaking the mapped key name at each .' | |
* For e.g. passing 1 will filter out mapped name where the resulting name contains one or more '.'. | |
* | |
* <p>Given | |
* <pre> | |
* Map ( | |
* alpha = Alpha | |
* alpha.somekey = v1 | |
* alpha.some.keys = v2 | |
* alpha.other.keys = v3 | |
* beta = Beta | |
* beta.some.keys = v3 | |
* ) | |
* </pre> | |
* | |
* a call to keyPrefixMapper("alpha", 1) will generate the following result: | |
* | |
* <pre> | |
* Map ( | |
* somekey = v1 | |
* ) | |
* </pre> | |
* | |
*/ | |
public static final BiFunction<String, String, String> keyPrefixMapper(String prefix, int length) { | |
String filter = prefix.endsWith(".") ? prefix : prefix + "."; | |
int keylength = filter.length(); | |
return new BiFunction<String, String, String>() { | |
@Override | |
public String apply(String key, String value) { | |
if(key.startsWith(filter)) { | |
String mapped = key.substring(keylength); | |
if (mapped.split("\\.").length == length) { | |
return mapped; | |
} | |
} | |
return null; // ignore | |
} | |
}; | |
} | |
// ---------------------------------------------------------------------------- | |
/*public*/ static final Map<String,String> loadValues(InputStream input, BiFunction<String, String, String> mapper) throws IOException { | |
try (BufferedReader br = new BufferedReader(new InputStreamReader(input, "UTF-8"))) { | |
String[] lines = lines(br); | |
Map<String,String> values = new LinkedHashMap<>(); | |
for (String line : lines) { | |
if (line.length() == 0 || line.charAt(0) == '#') | |
continue; | |
int equals = line.indexOf('='); | |
if (equals != -1) { | |
String key = line.substring(0, equals).trim(); | |
String value = line.substring(equals + 1).trim(); | |
key = mapper.apply(key, value); | |
if (key != null) { | |
values.put(key, value); | |
} | |
} | |
} | |
return values; | |
} | |
} | |
private static String[] lines(BufferedReader br) throws IOException { | |
String lines[] = new String[100]; | |
int lineCount = 0; | |
String line = null; | |
while ((line = br.readLine()) != null) { | |
if (lineCount == lines.length) { | |
String temp[] = new String[lineCount << 1]; // double | |
System.arraycopy(lines, 0, temp, 0, lineCount); | |
lines = temp; | |
} | |
lines[lineCount++] = line; | |
} | |
if (lineCount != lines.length) { | |
// resize array to appropriate amount for these lines | |
String output[] = new String[lineCount]; | |
System.arraycopy(lines, 0, output, 0, lineCount); | |
return output; | |
} | |
return lines; | |
} | |
protected static final String conditionalSuffix(String key, String suffix, boolean filter) { | |
if (key == null) | |
return null; | |
// Key does not end with the given suffix? Process as normal | |
if (!key.endsWith(suffix)) | |
return key; | |
if (!filter) | |
return null; | |
// Strip the suffix from the key | |
return key.substring(0, key.length() - suffix.length()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment