Introduce ISettings
diff --git a/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/Example.java b/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/Example.java
index 4a60b74..7fea85b 100644
--- a/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/Example.java
+++ b/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/Example.java
@@ -12,8 +12,8 @@
 
 import org.eclipse.userstorage.IBlob;
 import org.eclipse.userstorage.IStorage;
+import org.eclipse.userstorage.StorageFactory;
 import org.eclipse.userstorage.internal.util.IOUtil;
-import org.eclipse.userstorage.spi.StorageFactory;
 import org.eclipse.userstorage.util.FileStorageCache;
 
 import java.io.FileOutputStream;
diff --git a/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/StorageTests.java b/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/StorageTests.java
index 1bf0e94..8eb7a69 100644
--- a/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/StorageTests.java
+++ b/org.eclipse.userstorage.tests/src/org/eclipse/userstorage/tests/StorageTests.java
@@ -15,11 +15,12 @@
 import org.eclipse.userstorage.IBlob;
 import org.eclipse.userstorage.IStorage;
 import org.eclipse.userstorage.IStorageService;
+import org.eclipse.userstorage.StorageFactory;
 import org.eclipse.userstorage.internal.Activator;
 import org.eclipse.userstorage.internal.Session;
 import org.eclipse.userstorage.internal.util.IOUtil;
 import org.eclipse.userstorage.internal.util.StringUtil;
-import org.eclipse.userstorage.spi.StorageFactory;
+import org.eclipse.userstorage.spi.ISettings;
 import org.eclipse.userstorage.tests.util.FixedCredentialsProvider;
 import org.eclipse.userstorage.tests.util.USSServer;
 import org.eclipse.userstorage.tests.util.USSServer.NOOPLogger;
@@ -96,14 +97,19 @@
       final String serviceURI = "http://localhost:" + port;
       service = IStorageService.Registry.INSTANCE.addService("Test Service", StringUtil.newURI(serviceURI));
 
-      factory = new StorageFactory()
+      factory = new StorageFactory(new ISettings()
       {
         @Override
-        protected String getPreferredServiceURI(String applicationToken)
+        public String getValue(String key) throws Exception
         {
           return serviceURI;
         }
-      };
+
+        @Override
+        public void setValue(String key, String value) throws Exception
+        {
+        }
+      });
     }
 
     IOUtil.deleteFiles(CACHE);
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/IBlob.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/IBlob.java
index 4b15f92..d2dc1b1 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/IBlob.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/IBlob.java
@@ -11,7 +11,6 @@
 package org.eclipse.userstorage;
 
 import org.eclipse.userstorage.spi.StorageCache;
-import org.eclipse.userstorage.spi.StorageFactory;
 import org.eclipse.userstorage.util.ConflictException;
 import org.eclipse.userstorage.util.ProtocolException;
 
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorage.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorage.java
index 6a3d480..c375ed8 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorage.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorage.java
@@ -11,7 +11,6 @@
 package org.eclipse.userstorage;
 
 import org.eclipse.userstorage.spi.StorageCache;
-import org.eclipse.userstorage.spi.StorageFactory;
 import org.eclipse.userstorage.util.BadKeyException;
 
 /**
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/StorageFactory.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/StorageFactory.java
new file mode 100644
index 0000000..3f0be4d
--- /dev/null
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/StorageFactory.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Eike Stepper - initial API and implementation
+ */
+package org.eclipse.userstorage;
+
+import org.eclipse.userstorage.IStorageService.Registry;
+import org.eclipse.userstorage.internal.Activator;
+import org.eclipse.userstorage.internal.Storage;
+import org.eclipse.userstorage.internal.util.StringUtil;
+import org.eclipse.userstorage.spi.ISettings;
+import org.eclipse.userstorage.spi.StorageCache;
+import org.eclipse.userstorage.util.BadApplicationTokenException;
+import org.eclipse.userstorage.util.Settings;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Creates {@link IStorage storages}.
+ *
+ * @author Eike Stepper
+ */
+public final class StorageFactory
+{
+  public static final StorageFactory DEFAULT = new StorageFactory(Settings.DEFAULT);
+
+  private final ISettings settings;
+
+  public StorageFactory(ISettings settings)
+  {
+    this.settings = settings;
+  }
+
+  /**
+   * Constructs this storage factory.
+   */
+  public StorageFactory()
+  {
+    this(new Settings.MemorySettings());
+  }
+
+  /**
+   * Returns the settings of this factory.
+   *
+   * @return the settings of this factory, never <code>null</code>.
+   */
+  public ISettings getSettings()
+  {
+    return settings;
+  }
+
+  /**
+   * Creates a storage for the application identified by the given application token.
+   * <p>
+   * Calling this method is identical to calling <code>create(applicationToken, null)</code>.
+   * <p>
+   *
+   * @param applicationToken the application token that identifies the application of the storage to be created.
+   *        Minimal {@link BadApplicationTokenException#validate(String) lexical validation} is performed on the passed application token.<p>
+   * @return the newly created storage, never <code>null</code>.<p>
+   * @throws NoSuchElementException if the {@link Registry storage registry} is empty and, hence, there is no default storage available.<p>
+   * @throws BadApplicationTokenException if {@link BadApplicationTokenException#validate(String) lexical validation} of the passed application token fails.<p>
+   *
+   * @see #create(String, StorageCache)
+   */
+  public IStorage create(String applicationToken) throws NoSuchElementException, BadApplicationTokenException
+  {
+    return create(applicationToken, null);
+  }
+
+  /**
+   * Creates a storage for the application identified by the given application token and associates it with a given {@link StorageCache storage cache}.
+   * <p>
+   * @param applicationToken the application token that identifies the application of the storage to be created.
+   *        Minimal {@link BadApplicationTokenException#validate(String) lexical validation} is performed on the passed application token.<p>
+   * @param cache a local storage cache to be used as a locally persistent optimization, or <code>null</code> if local caching is not wanted.<p>
+   * @return the newly created storage, never <code>null</code>.<p>
+   * @throws NoSuchElementException if the {@link Registry storage registry} is empty and, hence, there is no default storage available.<p>
+   * @throws BadApplicationTokenException if {@link BadApplicationTokenException#validate(String) lexical validation} of the passed application token fails.<p>
+   *
+   * @see #create(String)
+   * @see StorageCache
+   */
+  public IStorage create(String applicationToken, StorageCache cache) throws NoSuchElementException, BadApplicationTokenException
+  {
+    IStorageService service = getService(applicationToken);
+
+    Storage storage = new Storage(this, applicationToken, cache);
+    storage.setService(service);
+    return storage;
+  }
+
+  private IStorageService getService(String applicationToken) throws NoSuchElementException
+  {
+    if (!StringUtil.isEmpty(applicationToken))
+    {
+      IStorageService service = getPreferredService(applicationToken);
+      if (service != null)
+      {
+        return service;
+      }
+    }
+
+    IStorageService service = getPreferredService(null);
+    if (service != null)
+    {
+      return service;
+    }
+
+    IStorageService[] storages = IStorageService.Registry.INSTANCE.getServices();
+    if (storages.length != 0)
+    {
+      return storages[0];
+    }
+
+    throw new NoSuchElementException("No service registered");
+  }
+
+  private IStorageService getPreferredService(String applicationToken) throws NoSuchElementException
+  {
+    if (StringUtil.isEmpty(applicationToken))
+    {
+      applicationToken = "<default>";
+    }
+
+    try
+    {
+      String serviceURI = getPreferredServiceURI(applicationToken);
+      if (serviceURI != null)
+      {
+        return IStorageService.Registry.INSTANCE.getService(StringUtil.newURI(serviceURI));
+      }
+    }
+    catch (Exception ex)
+    {
+      //$FALL-THROUGH$
+    }
+
+    return null;
+  }
+
+  private String getPreferredServiceURI(String applicationToken)
+  {
+    try
+    {
+      return settings.getValue(applicationToken);
+    }
+    catch (Exception ex)
+    {
+      Activator.log(ex);
+    }
+
+    return null;
+  }
+}
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/DefaultStorageFactory.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/DefaultStorageFactory.java
deleted file mode 100644
index 9de4955..0000000
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/DefaultStorageFactory.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *    Eike Stepper - initial API and implementation
- */
-package org.eclipse.userstorage.internal;
-
-import org.eclipse.userstorage.spi.ISettings;
-import org.eclipse.userstorage.spi.StorageFactory;
-
-import org.eclipse.core.runtime.Platform;
-import org.eclipse.core.runtime.preferences.IEclipsePreferences;
-
-import org.osgi.service.prefs.BackingStoreException;
-import org.osgi.service.prefs.Preferences;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author Eike Stepper
- */
-public final class DefaultStorageFactory extends StorageFactory
-{
-  private final ISettings settings = createSettings();
-
-  public DefaultStorageFactory()
-  {
-  }
-
-  @Override
-  protected String getPreferredServiceURI(String applicationToken)
-  {
-    try
-    {
-      return settings.getValue(applicationToken);
-    }
-    catch (Exception ex)
-    {
-      Activator.log(ex);
-    }
-
-    return null;
-  }
-
-  @Override
-  protected void setPreferredServiceURI(String applicationToken, String serviceURI)
-  {
-    try
-    {
-      settings.setValue(applicationToken, serviceURI);
-    }
-    catch (Exception ex)
-    {
-      Activator.log(ex);
-    }
-  }
-
-  private static ISettings createSettings()
-  {
-    String property = System.getProperty(StorageProperties.SETTINGS, null);
-    if (property != null)
-    {
-      try
-      {
-        @SuppressWarnings("unchecked")
-        Class<ISettings> c = (Class<ISettings>)Class.forName(property);
-
-        return c.newInstance();
-      }
-      catch (Throwable ex)
-      {
-        Activator.log(ex);
-      }
-    }
-
-    if (Activator.PLATFORM_RUNNING)
-    {
-      try
-      {
-        return new EclipseSettings("instance");
-      }
-      catch (BackingStoreException ex)
-      {
-        //$FALL-THROUGH$
-      }
-
-      try
-      {
-        return new EclipseSettings("configuration");
-      }
-      catch (BackingStoreException ex)
-      {
-        //$FALL-THROUGH$
-      }
-    }
-
-    return new StandaloneSettings();
-  }
-
-  /**
-   * @author Eike Stepper
-   */
-  private static final class EclipseSettings implements ISettings
-  {
-    private final Preferences node;
-
-    public EclipseSettings(String scope) throws BackingStoreException
-    {
-      IEclipsePreferences rootNode = Platform.getPreferencesService().getRootNode();
-      if (!rootNode.nodeExists(scope))
-      {
-        throw new BackingStoreException("Invalid scope: " + scope);
-      }
-
-      node = rootNode.node(scope).node(Activator.PLUGIN_ID);
-    }
-
-    @Override
-    public String getValue(String key) throws Exception
-    {
-      return node.get(key, null);
-    }
-
-    @Override
-    public void setValue(String key, String value) throws Exception
-    {
-      if (value == null)
-      {
-        node.remove(key);
-      }
-      else
-      {
-        node.put(key, value);
-      }
-
-      node.flush();
-    }
-  }
-
-  /**
-   * @author Eike Stepper
-   */
-  private static final class StandaloneSettings implements ISettings
-  {
-    private final Map<String, String> map = new HashMap<String, String>();
-
-    public StandaloneSettings()
-    {
-    }
-
-    @Override
-    public String getValue(String key) throws Exception
-    {
-      return map.get(key);
-    }
-
-    @Override
-    public void setValue(String key, String value) throws Exception
-    {
-      if (value == null)
-      {
-        map.remove(key);
-      }
-      else
-      {
-        map.put(key, value);
-      }
-    }
-  }
-}
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/InternalStorageFactory.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/InternalStorageFactory.java
deleted file mode 100644
index f4d7270..0000000
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/InternalStorageFactory.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *    Eike Stepper - initial API and implementation
- */
-package org.eclipse.userstorage.internal;
-
-import org.eclipse.userstorage.IStorage;
-import org.eclipse.userstorage.IStorageService;
-import org.eclipse.userstorage.IStorageService.Registry;
-import org.eclipse.userstorage.internal.util.StringUtil;
-import org.eclipse.userstorage.spi.StorageCache;
-import org.eclipse.userstorage.util.BadApplicationTokenException;
-
-import java.util.NoSuchElementException;
-
-/**
- * @author Eike Stepper
- */
-public abstract class InternalStorageFactory
-{
-  private static final Registry REGISTRY = IStorageService.Registry.INSTANCE;
-
-  public InternalStorageFactory()
-  {
-  }
-
-  public IStorage create(String applicationToken, StorageCache cache) throws NoSuchElementException, BadApplicationTokenException
-  {
-    IStorageService service = getService(applicationToken);
-
-    Storage storage = new Storage(this, applicationToken, cache);
-    storage.setService(service);
-    return storage;
-  }
-
-  private IStorageService getService(String applicationToken) throws NoSuchElementException
-  {
-    try
-    {
-      String serviceURI = getPreferredServiceURI(applicationToken);
-      if (serviceURI != null)
-      {
-        IStorageService service = REGISTRY.getService(StringUtil.newURI(serviceURI));
-        if (service != null)
-        {
-          return service;
-        }
-      }
-    }
-    catch (Exception ex)
-    {
-      //$FALL-THROUGH$
-    }
-
-    try
-    {
-      String serviceURI = getPreferredServiceURI(null);
-      if (serviceURI != null)
-      {
-        IStorageService service = REGISTRY.getService(StringUtil.newURI(serviceURI));
-        if (service != null)
-        {
-          return service;
-        }
-      }
-    }
-    catch (Exception ex)
-    {
-      //$FALL-THROUGH$
-    }
-
-    IStorageService[] storages = REGISTRY.getServices();
-    if (storages.length != 0)
-    {
-      return storages[0];
-    }
-
-    throw new NoSuchElementException("No service registered");
-  }
-
-  protected abstract String getPreferredServiceURI(String applicationToken);
-
-  protected abstract void setPreferredServiceURI(String applicationToken, String serviceURI);
-}
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/Storage.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/Storage.java
index 66c40c2..97e8800 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/Storage.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/Storage.java
@@ -13,8 +13,10 @@
 import org.eclipse.userstorage.IBlob;
 import org.eclipse.userstorage.IStorage;
 import org.eclipse.userstorage.IStorageService;
+import org.eclipse.userstorage.StorageFactory;
 import org.eclipse.userstorage.internal.util.IOUtil.TeeInputStream;
 import org.eclipse.userstorage.internal.util.StringUtil;
+import org.eclipse.userstorage.spi.ISettings;
 import org.eclipse.userstorage.spi.StorageCache;
 import org.eclipse.userstorage.util.BadApplicationTokenException;
 import org.eclipse.userstorage.util.ConflictException;
@@ -34,7 +36,7 @@
 {
   private final String applicationToken;
 
-  private final InternalStorageFactory factory;
+  private final StorageFactory factory;
 
   private final InternalStorageCache cache;
 
@@ -42,7 +44,7 @@
 
   private StorageService service;
 
-  public Storage(InternalStorageFactory factory, String applicationToken, InternalStorageCache cache) throws BadApplicationTokenException
+  public Storage(StorageFactory factory, String applicationToken, InternalStorageCache cache) throws BadApplicationTokenException
   {
     this.applicationToken = BadApplicationTokenException.validate(applicationToken);
     this.factory = factory;
@@ -75,11 +77,10 @@
       disposeBlobs();
 
       this.service = (StorageService)service;
-      factory.setPreferredServiceURI(applicationToken, service.getServiceURI().toString());
+      setPreferredServiceURI(service.getServiceURI().toString());
 
       if (cache != null)
       {
-        // TODO Should this happen later when the new storage is accessed the first time?
         cache.setService(service);
       }
     }
@@ -197,6 +198,19 @@
     return service + " (" + applicationToken + ")";
   }
 
+  private void setPreferredServiceURI(String serviceURI)
+  {
+    try
+    {
+      ISettings settings = factory.getSettings();
+      settings.setValue(applicationToken, serviceURI);
+    }
+    catch (Exception ex)
+    {
+      Activator.log(ex);
+    }
+  }
+
   private void disposeBlobs()
   {
     for (Blob blob : blobs.values())
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/ISettings.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/ISettings.java
index 05075f9..8bad42f 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/ISettings.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/ISettings.java
@@ -12,6 +12,7 @@
 
 import org.eclipse.userstorage.IStorage;
 import org.eclipse.userstorage.IStorageService;
+import org.eclipse.userstorage.StorageFactory;
 
 /**
  * A generic key/value map used by the {@link StorageFactory#DEFAULT default storage factory}
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/StorageFactory.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/StorageFactory.java
deleted file mode 100644
index a6400c0..0000000
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/spi/StorageFactory.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *    Eike Stepper - initial API and implementation
- */
-package org.eclipse.userstorage.spi;
-
-import org.eclipse.userstorage.IStorage;
-import org.eclipse.userstorage.IStorageService.Registry;
-import org.eclipse.userstorage.internal.DefaultStorageFactory;
-import org.eclipse.userstorage.internal.InternalStorageFactory;
-import org.eclipse.userstorage.util.BadApplicationTokenException;
-
-import java.util.NoSuchElementException;
-
-/**
- * Creates {@link IStorage storages}.
- *
- * @author Eike Stepper
- */
-public class StorageFactory extends InternalStorageFactory
-{
-  public static final StorageFactory DEFAULT = new DefaultStorageFactory();
-
-  /**
-   * Constructs this storage factory.
-   */
-  public StorageFactory()
-  {
-  }
-
-  /**
-   * Creates a storage for the application identified by the given application token.
-   * <p>
-   * Calling this method is identical to calling <code>create(applicationToken, null)</code>.
-   * <p>
-   *
-   * @param applicationToken the application token that identifies the application of the storage to be created.
-   *        Minimal {@link BadApplicationTokenException#validate(String) lexical validation} is performed on the passed application token.<p>
-   * @return the newly created storage, never <code>null</code>.<p>
-   * @throws NoSuchElementException if the {@link Registry storage registry} is empty and, hence, there is no default storage available.<p>
-   * @throws BadApplicationTokenException if {@link BadApplicationTokenException#validate(String) lexical validation} of the passed application token fails.<p>
-   *
-   * @see #create(String, StorageCache)
-   */
-  public final IStorage create(String applicationToken) throws NoSuchElementException, BadApplicationTokenException
-  {
-    return super.create(applicationToken, null);
-  }
-
-  /**
-   * Creates a storage for the application identified by the given application token and associates it with a given {@link StorageCache storage cache}.
-   * <p>
-   * @param applicationToken the application token that identifies the application of the storage to be created.
-   *        Minimal {@link BadApplicationTokenException#validate(String) lexical validation} is performed on the passed application token.<p>
-   * @param cache a local storage cache to be used as a locally persistent optimization, or <code>null</code> if local caching is not wanted.<p>
-   * @return the newly created storage, never <code>null</code>.<p>
-   * @throws NoSuchElementException if the {@link Registry storage registry} is empty and, hence, there is no default storage available.<p>
-   * @throws BadApplicationTokenException if {@link BadApplicationTokenException#validate(String) lexical validation} of the passed application token fails.<p>
-   *
-   * @see #create(String)
-   * @see StorageCache
-   */
-  @Override
-  public final IStorage create(String applicationToken, StorageCache cache) throws NoSuchElementException, BadApplicationTokenException
-  {
-    return super.create(applicationToken, cache);
-  }
-
-  /**
-   * Returns the URI of the preferred service for the given application.
-   * <p>
-   * Subclasses can override this method, for example,  to implement a per-application service memory.
-   * The default implementation returns <code>null</code>.
-   * <p>
-   *
-   * @param applicationToken the application token.<p>
-   * @return the URI of the preferred service for the given application.
-   */
-  @Override
-  protected String getPreferredServiceURI(String applicationToken)
-  {
-    return null;
-  }
-
-  /**
-   * Sets the URI of the preferred service for the given application.
-   * <p>
-   * Subclasses can override this method, for example,  to implement a per-application service memory.
-   * The default implementation does nothing.
-   * <p>
-   *
-   * @param applicationToken the application token.<p>
-   * @param serviceURI the URI of the preferred service for the given application.
-   */
-  @Override
-  protected void setPreferredServiceURI(String applicationToken, String serviceURI)
-  {
-    // Do nothing.
-  }
-}
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/util/Settings.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/util/Settings.java
new file mode 100644
index 0000000..b1c978a
--- /dev/null
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/util/Settings.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Eike Stepper - initial API and implementation
+ */
+package org.eclipse.userstorage.util;
+
+import org.eclipse.userstorage.internal.Activator;
+import org.eclipse.userstorage.internal.StorageProperties;
+import org.eclipse.userstorage.internal.util.IOUtil;
+import org.eclipse.userstorage.spi.ISettings;
+
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author Eike Stepper
+ */
+public final class Settings
+{
+  public static final ISettings DEFAULT = createDefaultSettings();
+
+  private Settings()
+  {
+  }
+
+  private static ISettings createDefaultSettings()
+  {
+    String property = System.getProperty(StorageProperties.SETTINGS, null);
+    if (property != null)
+    {
+      try
+      {
+        @SuppressWarnings("unchecked")
+        Class<ISettings> c = (Class<ISettings>)Class.forName(property);
+        return c.newInstance();
+      }
+      catch (Throwable ex)
+      {
+        Activator.log(ex);
+      }
+    }
+
+    if (Activator.PLATFORM_RUNNING)
+    {
+      try
+      {
+        return new EclipseSettings("instance");
+      }
+      catch (Throwable ex)
+      {
+        //$FALL-THROUGH$
+      }
+
+      try
+      {
+        return new EclipseSettings("configuration");
+      }
+      catch (Throwable ex)
+      {
+        //$FALL-THROUGH$
+      }
+    }
+
+    return new MemorySettings();
+  }
+
+  /**
+   * @author Eike Stepper
+   */
+  public static final class NoSettings implements ISettings
+  {
+    public NoSettings()
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getValue(String key) throws Exception
+    {
+      return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setValue(String key, String value) throws Exception
+    {
+      // Do nothing.
+    }
+  }
+
+  /**
+   * @author Eike Stepper
+   */
+  public static final class MemorySettings implements ISettings
+  {
+    private final Map<String, String> map = new HashMap<String, String>();
+
+    public MemorySettings()
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getValue(String key) throws Exception
+    {
+      return map.get(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setValue(String key, String value) throws Exception
+    {
+      if (value == null)
+      {
+        map.remove(key);
+      }
+      else
+      {
+        map.put(key, value);
+      }
+    }
+  }
+
+  /**
+   * @author Eike Stepper
+   */
+  public static final class EclipseSettings implements ISettings
+  {
+    private final Preferences node;
+
+    public EclipseSettings(String scope) throws BackingStoreException
+    {
+      IEclipsePreferences rootNode = Platform.getPreferencesService().getRootNode();
+      if (!rootNode.nodeExists(scope))
+      {
+        throw new BackingStoreException("Invalid scope: " + scope);
+      }
+
+      String nodeName = getNodeName();
+      node = rootNode.node(scope).node(nodeName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getValue(String key) throws Exception
+    {
+      return node.get(key, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setValue(String key, String value) throws Exception
+    {
+      if (value == null)
+      {
+        node.remove(key);
+      }
+      else
+      {
+        node.put(key, value);
+      }
+
+      node.flush();
+    }
+
+    protected String getNodeName()
+    {
+      return Activator.PLUGIN_ID;
+    }
+  }
+
+  /**
+   * @author Eike Stepper
+   */
+  public final class FileSettings implements ISettings
+  {
+    private final File file;
+
+    public FileSettings(File file)
+    {
+      this.file = file;
+    }
+
+    public FileSettings()
+    {
+      this(new File(System.getProperty("user.home"), ".eclipse/" + Activator.PLUGIN_ID + "/.settings"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getValue(String key) throws Exception
+    {
+      Properties properties = new Properties();
+
+      if (file.isFile())
+      {
+        InputStream in = null;
+
+        try
+        {
+          in = new FileInputStream(file);
+          properties.load(in);
+        }
+        finally
+        {
+          IOUtil.close(in);
+        }
+      }
+
+      return properties.getProperty(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setValue(String key, String value) throws Exception
+    {
+      Properties properties = new Properties();
+
+      if (file.isFile())
+      {
+        InputStream in = null;
+
+        try
+        {
+          in = new FileInputStream(file);
+          properties.load(in);
+        }
+        finally
+        {
+          IOUtil.close(in);
+        }
+      }
+
+      boolean changed = false;
+      if (value == null)
+      {
+        Object oldValue = properties.remove(key);
+        if (oldValue != null)
+        {
+          changed = true;
+        }
+      }
+      else
+      {
+        Object oldValue = properties.setProperty(key, value);
+        if (oldValue != null && !oldValue.equals(value))
+        {
+          changed = true;
+        }
+      }
+
+      if (changed)
+      {
+        OutputStream out = null;
+
+        try
+        {
+          out = new FileOutputStream(file);
+          properties.store(out, null);
+        }
+        finally
+        {
+          IOUtil.close(out);
+        }
+      }
+    }
+  }
+}