/**
* Copyright 2013 by dueni.ch
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dueni.prefs;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* <code>JsfXmlPreferences</code> implements {@link Preferences} handling systemRoot on application
* scope and userRoot on session scope persisting to one XML file per preferences tree (1 systemRoot
* tree and 1 userRoot tree per user).
*
* @author hampidu@gmail.com
*/
public class XmlFilePreferences extends AbstractPreferences {
public static final String SCOPE_PREFERENCES_KEY = XmlFilePreferences.class.getName();
private Map<String, String> valueMap;
private Map<String, XmlFilePreferences> childPrefs;
private Root root;
// to trigger load on first access to childrenNamesSpi() or keysSpi() on Root Preferences this is set to true in setRoot() method
private boolean loaded = true;
private static final String FILE_NAME_PREFIX = "JsfXmlPreferences-";
private static final String FILE_NAME_SUFFIX = ".xml";
public enum Root {
system, user
}
protected XmlFilePreferences(XmlFilePreferences parent, String name) {
super(parent, name);
}
private void ensureLoadedSilent() {
if (root != null && !loaded) {
try {
// try to load preferences from file - may silently fail if there a
ensureLoaded();
} catch (BackingStoreException bse) {
bse.printStackTrace();
}
}
}
private void ensureLoaded() throws BackingStoreException {
if (!loaded) {
loaded = true;
PreferencesContext ctx = PreferencesContext.getCurrentInstance();
importPreferencesTree(ctx, getRoot());
}
}
void setRoot(Root root) {
this.root = root;
loaded = false;
}
Root getRoot() {
return root;
}
@Override
protected void putSpi(String key, String value) {
if (valueMap == null) {
valueMap = new HashMap<String, String>();
}
valueMap.put(key, value);
}
@Override
protected String getSpi(String key) {
ensureLoadedSilent();
return (valueMap == null) ? null : valueMap.get(key);
}
@Override
protected void removeSpi(String key) {
if (valueMap != null) {
valueMap.remove(key);
}
}
@Override
protected void removeNodeSpi() throws BackingStoreException {
((XmlFilePreferences)parent()).childPrefs.remove(this.name());
}
@Override
protected String[] keysSpi() throws BackingStoreException {
ensureLoaded();
if (valueMap == null) {
return new String[0];
}
return valueMap.keySet().toArray(new String[0]);
}
@Override
public Preferences node(String path) {
ensureLoadedSilent();
return super.node(path);
}
@Override
protected String[] childrenNamesSpi() throws BackingStoreException {
ensureLoaded();
if (childPrefs == null) {
return new String[0];
}
return childPrefs.keySet().toArray(new String[0]);
}
@Override
protected AbstractPreferences childSpi(String name) {
if (childPrefs == null) {
childPrefs = new HashMap<String, XmlFilePreferences>();
}
XmlFilePreferences child = childPrefs.get(name);
if (child == null) {
child = new XmlFilePreferences(this, name);
childPrefs.put(name, child);
}
return child;
}
@Override
public void sync() throws BackingStoreException {
XmlFilePreferences p = this;
while (p.parent() != null) {
p = (XmlFilePreferences)p.parent();
}
PreferencesContext ctx = PreferencesContext.getCurrentInstance();
Root type = p.getRoot();
if (ctx.getToSave().contains(type)) {
// flush() pending on this root, abort sync() to not lose not-yet saved changes
return;
}
if (Root.user.equals(type)) {
Map<String, Object> sessionMap = ctx.getUserScope();
sessionMap.remove(SCOPE_PREFERENCES_KEY);
getUserRoot();
} else {
Map<String, Object> appScope = ctx.getAppScope();
appScope.remove(SCOPE_PREFERENCES_KEY);
getSystemRoot();
}
}
@Override
protected void syncSpi() throws BackingStoreException {
throw new UnsupportedOperationException(
"Since this implementation manages full tree at once sync() method most be overridden!");
}
@Override
public void flush() throws BackingStoreException {
XmlFilePreferences p = this;
while (p.parent() != null) {
p = (XmlFilePreferences)p.parent();
}
Root type = p.getRoot();
// prevent load trigger - we are flushing now!
p.loaded = true;
PreferencesContext prefsCtx = PreferencesContext.getCurrentInstance();
if (!prefsCtx.isWritable()){
throw new BackingStoreException("access to backing store not possible!");
}
prefsCtx.addToSave(type);
//storePreferencesTree(type);
}
public synchronized static void storePreferencesTree(Root root) {
try {
PreferencesContext ctx = PreferencesContext.getCurrentInstance();
File storeFile = ensureStoreFile(ctx, root);
XmlFilePreferences prefs =
Root.user == root ? XmlFilePreferences.getUserRoot() : XmlFilePreferences.getSystemRoot();
if (!storeFile.canWrite()) {
storeFile.setWritable(true);
}
if (!storeFile.exists()) {
storeFile.createNewFile();
}
FileOutputStream os = new FileOutputStream(storeFile);
prefs.exportSubtree(os);
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void flushSpi() throws BackingStoreException {
throw new UnsupportedOperationException(
"Since this implementation manages full tree at once flush() method most be overridden!");
}
public static XmlFilePreferences getSystemRoot() {
PreferencesContext ctx = PreferencesContext.getCurrentInstance();
Map<String, Object> appScope = ctx.getAppScope();
XmlFilePreferences systemRoot = (XmlFilePreferences)appScope.get(SCOPE_PREFERENCES_KEY);
if (systemRoot == null) {
systemRoot = new XmlFilePreferences(null, "");
systemRoot.setRoot(Root.system);
appScope.put(SCOPE_PREFERENCES_KEY, systemRoot);
//importPreferencesTree(ctx, Root.system);
}
return systemRoot;
}
public static XmlFilePreferences getUserRoot() {
PreferencesContext ctx = PreferencesContext.getCurrentInstance();
Map<String, Object> sessionMap = ctx.getUserScope();
XmlFilePreferences userRoot = (XmlFilePreferences)sessionMap.get(SCOPE_PREFERENCES_KEY);
if (userRoot == null) {
userRoot = new XmlFilePreferences(null, "");
userRoot.setRoot(Root.user);
sessionMap.put(SCOPE_PREFERENCES_KEY, userRoot);
//importPreferencesTree(ctx, Root.user);
}
return userRoot;
}
static void importPreferencesTree(PreferencesContext ctx, Root root) throws BackingStoreException {
if (!ctx.isReadable()) {
throw new BackingStoreException("access to backing store not possible!");
}
try {
File storeFile = ensureStoreFile(ctx, root);
if (storeFile.canRead()) {
InputStream in = new FileInputStream(storeFile);
Preferences.importPreferences(in);
}
} catch (Exception x) {
x.printStackTrace();
}
}
private static File ensureStoreFile(PreferencesContext ctx, Root root) {
String storePath = ctx.getStorePath();
if (storePath == null) {
try {
storePath = File.createTempFile("dummy", "txt").getParent();
} catch (IOException e) {
storePath = "/temp";
}
}
File storeDir = new File(storePath, "prefs-store");
if (!storeDir.exists()) {
storeDir.mkdirs();
}
String name = root.toString();
if (root == Root.user) {
name = name + "-" + ctx.getUserName();
}
String fileName = FILE_NAME_PREFIX + name + FILE_NAME_SUFFIX;
File xmlFile = new File(storeDir, fileName);
if (!xmlFile.exists()) {
// TODO: copy initial state file into store-dir
}
return xmlFile;
}
}