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(); + } + +}