/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2009 Pentaho Corporation. All rights reserved.
*/
package org.pentaho.reporting.designer.core.settings.prefs;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.designer.core.util.exceptions.UncaughtExceptionsModel;
import org.pentaho.reporting.libraries.base.util.DebugLog;
/**
* Todo: Document Me
*
* @author Thomas Morgner
*/
public class BinaryPreferences extends AbstractPreferences
{
private static final Log logger = LogFactory.getLog(BinaryPreferences.class);
private Properties properties;
private BinaryPreferences parent;
private long lastModificationTime;
private String rootPath;
public BinaryPreferences(final String rootPath)
{
super(null, "");
this.rootPath = rootPath;
}
public BinaryPreferences(final BinaryPreferences parent, final String name)
{
super(parent, name);
this.parent = parent;
this.properties = new Properties();
}
protected void putSpi(final String key, final String value)
{
initCache();
properties.setProperty(key, value);
lastModificationTime = Math.max (lastModificationTime + 1, System.currentTimeMillis());
try
{
flush();
}
catch (BackingStoreException e)
{
if (logger.isInfoEnabled())
{
logger.info("Failed to flush configuration changes");//NON-NLS
}
else if (logger.isDebugEnabled())
{
logger.info("Failed to flush configuration changes",e);//NON-NLS
}
}
}
private void initCache()
{
if (lastModificationTime == 0)
{
final File pathForNode = new File(getPathForNode());
try
{
load(pathForNode);
}
catch (BackingStoreException e)
{
logger.warn("Failed to load data", e);//NON-NLS
// ignored
}
}
}
protected String getSpi(final String key)
{
initCache();
return properties.getProperty(key);
}
protected void removeSpi(final String key)
{
initCache();
properties.remove(key);
lastModificationTime = Math.max (lastModificationTime + 1, System.currentTimeMillis());
try
{
flush();
}
catch (BackingStoreException e)
{
if (logger.isInfoEnabled())
{
logger.info("Failed to flush configuration changes");//NON-NLS
}
else if (logger.isDebugEnabled())
{
logger.info("Failed to flush configuration changes", e);//NON-NLS
}
}
}
public long getLastModificationTime()
{
return lastModificationTime;
}
protected String[] keysSpi() throws BackingStoreException
{
initCache();
return (String[]) properties.keySet().toArray(new String[properties.size()]);
}
protected String[] childrenNamesSpi() throws BackingStoreException
{
final ArrayList<String> result = new ArrayList<String>();
final File pathForNode = new File(getPathForNode());
final File[] dirContents = pathForNode.listFiles();
if (dirContents != null)
{
for (int i = 0; i < dirContents.length; i++)
{
if (dirContents[i].isDirectory())
{
result.add(decodePath(dirContents[i].getName()));
}
}
}
return result.toArray(new String[result.size()]);
}
protected AbstractPreferences childSpi(final String name)
{
if (name == null || name.length() == 0)
{
throw new IllegalArgumentException();
}
return new BinaryPreferences(this, name);
}
protected void syncSpi() throws BackingStoreException
{
final File pathForNode = new File(getPathForNode());
if (pathForNode.exists() == false && properties.isEmpty())
{
return;
}
load(pathForNode);
if (pathForNode.exists() == false)
{
if (pathForNode.mkdirs() == false)
{
throw new BackingStoreException("Failed to write config " + pathForNode); //$NON-NLS-1$
}
}
final File target = new File(pathForNode, "prefs.properties");//NON-NLS
if (target.exists() == false || target.lastModified() < lastModificationTime)
{
try
{
final OutputStream out = new BufferedOutputStream(new FileOutputStream(target));
try
{
properties.store(out, "");
}
finally
{
out.close();
}
}
catch (final Exception e)
{
throw new BackingStoreException("Failed to write config " + target); //$NON-NLS-1$
}
}
}
private void load(final File pathForNode)
throws BackingStoreException
{
if (pathForNode.exists())
{
// load ..
final File target = new File(pathForNode, "prefs.properties");//NON-NLS
if (target.lastModified() > lastModificationTime)
{
if (target.exists())
{
try
{
final InputStream out = new BufferedInputStream(new FileInputStream(target));
try
{
properties.clear();
properties.load(out);
lastModificationTime = Math.max (lastModificationTime + 1, System.currentTimeMillis());
}
finally
{
out.close();
}
}
catch (final Exception e)
{
UncaughtExceptionsModel.getInstance().addException(e);
throw new BackingStoreException("Failed to write config " + target); //$NON-NLS-1$
}
}
}
}
}
protected void flushSpi() throws BackingStoreException
{
// no-op
syncSpi();
}
protected void removeNodeSpi() throws BackingStoreException
{
// delete the directory ..
final File pathForNode = new File(getPathForNode());
if (pathForNode.exists())
{
final File target = new File(pathForNode, "prefs.properties");//NON-NLS
if (target.delete() == false)
{
throw new BackingStoreException("Unable to delete node-backend");
}
if (pathForNode.delete() == false)
{
throw new BackingStoreException("Unable to delete node-backend");
}
}
}
private String getPathForNode()
{
if (parent != null)
{
return parent.getPathForNode() + File.separatorChar + encodePath(name());
}
return rootPath;
}
/**
* Encodes the given configuration path. All non-ascii characters get replaced by an escape sequence.
*
* @param path the path.
* @return the translated path.
* @throws java.util.prefs.BackingStoreException if something goes wrong.
*/
private static String decodePath(final String path) throws BackingStoreException
{
try
{
final char[] data = path.toCharArray();
final StringBuffer encoded = new StringBuffer(path.length());
int seenDollarIndex = -1;
for (int i = 0; i < data.length; i++)
{
if (seenDollarIndex > -1)
{
if (data[i] == '$')
{
encoded.append('$');
seenDollarIndex = -1;
continue;
}
if (i - seenDollarIndex == 4)
{
final int c = Integer.parseInt(path.substring(seenDollarIndex + 1, i + 1), 16);
encoded.append((char) c);
seenDollarIndex = -1;
continue;
}
else
{
continue;
}
}
if (data[i] == '$')
{
seenDollarIndex = i;
}
else
{
encoded.append(data[i]);
}
}
return encoded.toString();
}
catch (NumberFormatException nfe)
{
nfe.printStackTrace();
throw new BackingStoreException("Failed to decode name: " + path);
}
}
/**
* Encodes the given configuration path. All non-ascii characters get replaced by an escape sequence.
*
* @param path the path.
* @return the translated path.
*/
private static String encodePath(final String path)
{
final char[] data = path.toCharArray();
final StringBuffer encoded = new StringBuffer(path.length());
for (int i = 0; i < data.length; i++)
{
if (data[i] == '$')
{
// double quote
encoded.append('$');
encoded.append('$');
}
else if (Character.isJavaIdentifierPart(data[i]) == false)
{
// padded hex string
encoded.append('$');
final String hex = Integer.toHexString(data[i]);
for (int x = hex.length(); x < 4; x++)
{
encoded.append('0');
}
encoded.append(hex);
}
else
{
encoded.append(data[i]);
}
}
return encoded.toString();
}
}