diff --git a/source/net/filebot/util/prefs/FilePreferences.java b/source/net/filebot/util/prefs/FilePreferences.java
index b3f83e4d..11ffa0b1 100644
--- a/source/net/filebot/util/prefs/FilePreferences.java
+++ b/source/net/filebot/util/prefs/FilePreferences.java
@@ -1,216 +1,91 @@
-// https://github.com/sonatype/nexus/blob/2f0e154ec565969b4fd8698883ab76a461210f4f/nexus/nexus-test-harness/nexus-it-helper-plugin/src/main/java/org/sonatype/nexus/rt/prefs/FilePreferences.java
-
package net.filebot.util.prefs;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.TreeMap;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * Preferences implementation that stores to a user-defined file. See FilePreferencesFactory. Modified by cstamas,
- * switched to SLF4J logging, and exposed preferences file property.
- *
- * Modified to use '/' as path separator and not '.' because it breaks when keys containing '.' are used.
- *
- * @author David Croft (www.davidc.net)
- * @version $Id: FilePreferences.java 283 2009-06-18 17:06:58Z david $
- */
public class FilePreferences extends AbstractPreferences {
- private static final Logger log = LoggerFactory.getLogger(FilePreferences.class.getName());
+ protected PropertyFileBackingStore store;
- private Map root;
+ public FilePreferences(PropertyFileBackingStore store) {
+ super(null, "");
- private Map children;
-
- private boolean isRemoved = false;
-
-
- public FilePreferences(AbstractPreferences parent, String name) {
- super(parent, name);
-
- log.debug("Instantiating node {}", name);
-
- root = new TreeMap();
- children = new TreeMap();
-
- try {
- sync();
- } catch (BackingStoreException e) {
- log.error("Unable to sync on creation of node " + name, e);
- }
+ this.store = store;
}
+ protected FilePreferences(FilePreferences parent, String name) {
+ super(parent, name);
+
+ this.store = parent.store;
+ }
+
+ protected String getNodeKey() {
+ return absolutePath().substring(1);
+ }
@Override
protected void putSpi(String key, String value) {
- root.put(key, value);
- try {
- flush();
- } catch (BackingStoreException e) {
- log.error("Unable to flush after putting " + key, e);
- }
+ store.setValue(getNodeKey(), key, value);
}
-
@Override
protected String getSpi(String key) {
- return root.get(key);
+ return store.getValue(getNodeKey(), key);
}
-
@Override
protected void removeSpi(String key) {
- root.remove(key);
- try {
- flush();
- } catch (BackingStoreException e) {
- log.error("Unable to flush after removing " + key, e);
- }
+ store.removeValue(getNodeKey(), key);
}
-
@Override
protected void removeNodeSpi() throws BackingStoreException {
- isRemoved = true;
- flush();
+ store.removeNode(getNodeKey());
}
-
@Override
protected String[] keysSpi() throws BackingStoreException {
- return root.keySet().toArray(new String[root.keySet().size()]);
+ return store.getKeys(getNodeKey());
}
-
@Override
protected String[] childrenNamesSpi() throws BackingStoreException {
- return children.keySet().toArray(new String[children.keySet().size()]);
+ return store.getChildren(getNodeKey());
}
-
@Override
protected FilePreferences childSpi(String name) {
- FilePreferences child = children.get(name);
- if (child == null || child.isRemoved()) {
- child = new FilePreferences(this, name);
- children.put(name, child);
- }
- return child;
+ return new FilePreferences(this, name);
}
+ @Override
+ public void sync() throws BackingStoreException {
+ // if the backing store naturally syncs an entire subtree at once, the implementer is encouraged to override sync(), rather than merely overriding syncSpi()
+ syncSpi();
+ }
@Override
protected void syncSpi() throws BackingStoreException {
- if (isRemoved()) {
- return;
- }
-
- final File file = FilePreferencesFactory.getPreferencesFile();
-
- if (!file.exists()) {
- return;
- }
-
- synchronized (file) {
- Properties p = new Properties();
- try {
- p.load(new FileInputStream(file));
-
- StringBuilder sb = new StringBuilder();
- getPath(sb);
- String path = sb.toString();
-
- final Enumeration> pnen = p.propertyNames();
- while (pnen.hasMoreElements()) {
- String propKey = (String) pnen.nextElement();
- if (propKey.startsWith(path)) {
- String subKey = propKey.substring(path.length());
- // Only load immediate descendants
- if (subKey.indexOf('/') == -1) {
- root.put(subKey, p.getProperty(propKey));
- }
- }
- }
- } catch (IOException e) {
- throw new BackingStoreException(e);
- }
+ try {
+ store.sync();
+ } catch (Exception e) {
+ throw new BackingStoreException(e);
}
}
-
- private void getPath(StringBuilder sb) {
- final FilePreferences parent = (FilePreferences) parent();
- if (parent == null) {
- return;
- }
-
- parent.getPath(sb);
- sb.append(name()).append('/');
+ @Override
+ public void flush() throws BackingStoreException {
+ // if the backing store naturally flushes an entire subtree at once, the implementer is encouraged to override flush(), rather than merely overriding flushSpi()
+ flushSpi();
}
-
@Override
protected void flushSpi() throws BackingStoreException {
- final File file = FilePreferencesFactory.getPreferencesFile();
-
- synchronized (file) {
- Properties p = new Properties();
- try {
-
- StringBuilder sb = new StringBuilder();
- getPath(sb);
- String path = sb.toString();
-
- if (file.exists()) {
- p.load(new FileInputStream(file));
-
- List toRemove = new ArrayList();
-
- // Make a list of all direct children of this node to be removed
- final Enumeration> pnen = p.propertyNames();
- while (pnen.hasMoreElements()) {
- String propKey = (String) pnen.nextElement();
- if (propKey.startsWith(path)) {
- String subKey = propKey.substring(path.length());
- // Only do immediate descendants
- if (subKey.indexOf('/') == -1) {
- toRemove.add(propKey);
- }
- }
- }
-
- // Remove them now that the enumeration is done with
- for (String propKey : toRemove) {
- p.remove(propKey);
- }
- }
-
- // If this node hasn't been removed, add back in any values
- if (!isRemoved) {
- for (String s : root.keySet()) {
- p.setProperty(path + s, root.get(s));
- }
- }
-
- p.store(new FileOutputStream(file), "FilePreferences");
- } catch (IOException e) {
- throw new BackingStoreException(e);
- }
+ try {
+ store.flush();
+ } catch (Exception e) {
+ throw new BackingStoreException(e);
}
}
+
}
diff --git a/source/net/filebot/util/prefs/FilePreferencesFactory.java b/source/net/filebot/util/prefs/FilePreferencesFactory.java
index cac1e5a1..7ac5e29d 100644
--- a/source/net/filebot/util/prefs/FilePreferencesFactory.java
+++ b/source/net/filebot/util/prefs/FilePreferencesFactory.java
@@ -1,65 +1,51 @@
-// https://github.com/sonatype/nexus/blob/2f0e154ec565969b4fd8698883ab76a461210f4f/nexus/nexus-test-harness/nexus-it-helper-plugin/src/main/java/org/sonatype/nexus/rt/prefs/FilePreferencesFactory.java
-
package net.filebot.util.prefs;
-
-import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.prefs.PreferencesFactory;
-
-/**
- * PreferencesFactory implementation that stores the preferences in a user-defined file. To use it, set the system
- * property java.util.prefs.PreferencesFactory to net.filebot.util.pref.FilePreferencesFactory
- *
- * The file defaults to [user.home]/.fileprefs, but may be overridden with the system property
- * net.filebot.util.pref.FilePreferencesFactory.file. Modified by cstamas, switched to SLF4J logging, and
- * exposed preferences file property.
- *
- * @author David Croft (www.davidc.net)
- * @version $Id: FilePreferencesFactory.java 282 2009-06-18 17:05:18Z david $
- */
public class FilePreferencesFactory implements PreferencesFactory {
- Preferences rootPreferences;
-
- public static final String SYSTEM_PROPERTY_FILE = "net.filebot.util.prefs.file";
-
+ private final static FilePreferences userRoot = createRootNode(getBackingStoreFile());
@Override
public Preferences systemRoot() {
- return userRoot();
+ return userRoot;
}
-
@Override
public Preferences userRoot() {
- if (rootPreferences == null) {
- rootPreferences = new FilePreferences(null, "");
- }
-
- return rootPreferences;
+ return userRoot;
}
- private static File preferencesFile;
+ public static FilePreferences createRootNode(Path backingStoreFile) {
+ FilePreferences node = new FilePreferences(new PropertyFileBackingStore(backingStoreFile));
+ // restore preferences
+ try {
+ node.sync();
+ } catch (Exception e) {
+ Logger.getLogger(FilePreferences.class.getName()).log(Level.WARNING, e, e::toString);
+ }
- public static File getPreferencesFile() {
- if (preferencesFile == null) {
- String prefsFile = System.getProperty(SYSTEM_PROPERTY_FILE);
-
- if (prefsFile == null || prefsFile.length() == 0) {
- prefsFile = System.getProperty("user.home") + File.separator + ".fileprefs";
+ // store preferences on exit
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ userRoot.flush();
+ } catch (BackingStoreException e) {
+ Logger.getLogger(FilePreferences.class.getName()).log(Level.WARNING, e, e::toString);
}
+ }));
- preferencesFile = new File(prefsFile).getAbsoluteFile();
- }
-
- return preferencesFile;
+ return node;
}
-
- public static void setPreferencesFile(File file) {
- preferencesFile = file;
+ public static Path getBackingStoreFile() {
+ return Paths.get(System.getProperty("net.filebot.util.prefs.file", "prefs.properties"));
}
+
}
diff --git a/source/net/filebot/util/prefs/PropertyFileBackingStore.java b/source/net/filebot/util/prefs/PropertyFileBackingStore.java
new file mode 100644
index 00000000..598f76cf
--- /dev/null
+++ b/source/net/filebot/util/prefs/PropertyFileBackingStore.java
@@ -0,0 +1,136 @@
+package net.filebot.util.prefs;
+
+import static java.nio.charset.StandardCharsets.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class PropertyFileBackingStore {
+
+ private Path store;
+
+ private Map> nodes = new HashMap>();
+
+ public PropertyFileBackingStore(Path store) {
+ this.store = store;
+ }
+
+ private Map newKeyValueMap(String node) {
+ return new HashMap();
+ }
+
+ public synchronized String setValue(String node, String key, String value) {
+ return nodes.computeIfAbsent(node, this::newKeyValueMap).put(key, value);
+ }
+
+ public synchronized String getValue(String node, String key) {
+ Map values = nodes.get(node);
+ if (values != null) {
+ return values.get(key);
+ }
+ return null;
+ }
+
+ public synchronized void removeValue(String node, String key) {
+ Map values = nodes.get(node);
+ if (values != null) {
+ values.remove(key);
+ }
+ }
+
+ public synchronized void removeNode(String node) {
+ nodes.remove(node);
+ }
+
+ public synchronized String[] getKeys(String node) {
+ Map values = nodes.get(node);
+ if (values != null) {
+ return values.keySet().toArray(new String[0]);
+ }
+ return new String[0];
+ }
+
+ public synchronized String[] getChildren(String node) {
+ return nodes.keySet().stream().filter(k -> k.length() > node.length() && k.indexOf('/', node.length()) < 0 && k.startsWith(node)).map(k -> k.substring(node.length())).toArray(String[]::new);
+ }
+
+ public synchronized Properties toProperties() {
+ Properties props = new Properties();
+
+ nodes.forEach((node, values) -> {
+ values.forEach((key, value) -> {
+ props.put(node + '/' + key, value);
+ });
+ });
+
+ return props;
+ }
+
+ public synchronized void mergeNodes(Map> n) {
+ n.forEach((node, values) -> {
+ nodes.merge(node, values, (m1, m2) -> {
+ Map m = newKeyValueMap(node);
+ m.putAll(m1);
+ m.putAll(m2);
+ return m;
+ });
+ });
+ }
+
+ public void sync() throws IOException {
+ if (!Files.exists(store)) {
+ return;
+ }
+
+ byte[] bytes = Files.readAllBytes(store);
+ StringReader buffer = new StringReader(new String(bytes, UTF_8));
+
+ Properties props = new Properties();
+ props.load(buffer);
+
+ Map> n = new HashMap>();
+
+ props.forEach((k, v) -> {
+ String propertyKey = k.toString();
+ int s = propertyKey.lastIndexOf('/');
+
+ String node = propertyKey.substring(0, s);
+ String key = propertyKey.substring(s + 1);
+
+ n.computeIfAbsent(node, this::newKeyValueMap).put(key, v.toString());
+ });
+
+ mergeNodes(n);
+ }
+
+ public void flush() throws IOException {
+ StringWriter buffer = new StringWriter(1024);
+ this.toProperties().store(buffer, null);
+
+ ByteBuffer data = UTF_8.encode(CharBuffer.wrap(buffer.getBuffer()));
+
+ try (FileChannel out = FileChannel.open(store, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
+ try (FileLock lock = out.lock()) {
+ out.write(data);
+ out.truncate(out.position());
+ }
+ }
+ }
+
+ @Override
+ public synchronized String toString() {
+ return nodes.toString();
+ }
+
+}