/**
* Copyright (C) 2009 Future Invent Informationsmanagement GmbH. All rights
* reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option) any
* later version.
*
* This library 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*/
package org.fuin.utils4j;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
/**
* A properties file that is capable of merging concurrent changes made by
* another JVM or another process.
*/
public class PropertiesFile {
private final File file;
private final String encoding;
private final List props;
private int tryLockMax = 3;
private long tryWaitMillis = 100;
private boolean loaded = false;
/**
* Constructor with file. Encoding is "UTF-8".
*
* @param file
* File reference.
*/
public PropertiesFile(final File file) {
this(file, "UTF-8");
}
/**
* Constructor with file and encoding.
*
* @param file
* File reference.
* @param encoding
* File encoding ("UTF-8" etc.)
*/
public PropertiesFile(final File file, final String encoding) {
super();
//Utils4J.checkNotNull("file", file);
this.file = file;
//Utils4J.checkNotNull("encoding", encoding);
this.encoding = encoding;
this.props = new ArrayList();
}
/**
* Returns the encoding of the file.
*
* @return File encoding ("UTF-8" etc.)
*/
public final String getEncoding() {
return encoding;
}
/**
* Returns if the underlying file has ever been read.
*
* @return If the file was loaded true
else false
.
*/
public final boolean isLoaded() {
return loaded;
}
/**
* Discards all properties in memory (not on disk!).
*/
public final void clear() {
props.clear();
}
/**
* Loads or reloads the content of the underlying file. Current properties
* in memory will NOT be discarded! If you want to discard the current
* values you must call clear()
before!
*
* @throws IOException
* Error reading the file.
* @throws LockingFailedException
* Locking the file failed.
* @throws MergeException
* A problem occurred when merging the properties in memory and
* from disk.
*/
public final void load() throws IOException, LockingFailedException, MergeException {
// Load new data from file
final RandomAccessFileInputStream in = new RandomAccessFileInputStream(file, "rw");
try {
final FileLock lock = in.lock(tryLockMax, tryWaitMillis);
try {
// TODO Anyone knows a better/faster solution?
// Just checking "file.lastModified()" failed here on a
// irregular base so always merging seems to be the best
// alternative...
merge(in);
} finally {
lock.release();
}
} finally {
in.close();
}
}
private void load(final RandomAccessFileInputStream in, final File file, final List props,
final String encoding) throws IOException {
final LineNumberReader reader = new LineNumberReader(new InputStreamReader(
new BufferedInputStream(in), encoding));
String line;
while ((line = reader.readLine()) != null) {
final int p = line.indexOf('=');
if (p > -1) {
final String key = line.substring(0, p);
final String value = line.substring(p + 1);
props.add(new Property(key, value, value));
}
}
// We don't close the reader because this will be done by the caller
loaded = true;
}
private void merge(final RandomAccessFileInputStream in) throws MergeException, IOException {
final List problems = new ArrayList();
final List currentProps = new ArrayList();
load(in, file, currentProps, encoding);
for (int i = 0; i < currentProps.size(); i++) {
final Property currentProp = (Property) currentProps.get(i);
final int idx = props.indexOf(currentProp);
if (idx == -1) {
// New property from file
props.add(currentProp);
} else {
final Property prop = (Property) props.get(idx);
if (prop.hasChanged()) {
if (prop.isNew()) {
// New property
if (!prop.getValue().equals(currentProp.getValue())) {
problems.add(new MergeException.Problem(
"Same new property in file with a different value!", prop,
currentProp));
}
} else {
if (prop.isDeleted()) {
// Deleted property
if (!prop.getInitialValue().equals(currentProp.getValue())) {
problems.add(new MergeException.Problem(
"Modified property in file we want to delete!", prop,
currentProp));
}
} else {
// Changed property
if (!prop.getInitialValue().equals(currentProp.getValue())) {
problems.add(new MergeException.Problem(
"Same property modified in file but different value!",
prop, currentProp));
}
}
}
} else {
// No change, simply replace with file
props.set(idx, currentProp);
}
}
}
if (problems.size() > 0) {
throw new MergeException(file, (MergeException.Problem[]) problems
.toArray(new MergeException.Problem[0]));
}
}
/**
* Save the content from memory to disk.
*
* @param sortByKey
* Sort the properties by key before saving?
*
* @throws IOException
* Error writing the file.
* @throws MergeException
* One or more properties were modified concurrently.
* @throws LockingFailedException
* Locking the file failed.
*/
public final void save(final boolean sortByKey) throws IOException, MergeException,
LockingFailedException {
save(new String[] {}, sortByKey);
}
/**
* Save the content from memory to disk.
*
* @param comment
* Comment to prepend (Should not include the "#" comment sign -
* It will be prepended automatically).
* @param sortByKey
* Sort the properties by key before saving?
*
* @throws IOException
* Error writing the file.
* @throws MergeException
* One or more properties were modified concurrently.
* @throws LockingFailedException
* Locking the file failed.
*/
public final void save(final String comment, final boolean sortByKey) throws IOException,
MergeException, LockingFailedException {
save(new String[] { comment }, sortByKey);
}
/**
* Save the content from memory to disk.
*
* @param comments
* Comments to prepend (Should not include the "#" comment sign -
* It will be prepended automatically).
* @param sortByKey
* Sort the properties by key before saving?
*
* @throws IOException
* Error writing the file.
* @throws MergeException
* One or more properties were modified concurrently.
* @throws LockingFailedException
* Locking the file failed.
*/
public final void save(final String[] comments, final boolean sortByKey) throws IOException,
MergeException, LockingFailedException {
final RandomAccessFileOutputStream out = new RandomAccessFileOutputStream(file, "rw");
try {
final FileLock lock = out.lock(tryLockMax, tryWaitMillis);
try {
// TODO Anyone knows a better/faster solution?
// Just checking "file.lastModified()" failed here on a
// irregular base so always merging seems to be the best
// alternative...
merge(new RandomAccessFileInputStream(out));
out.seek(0);
out.resetCounter();
// Sort?
if (sortByKey) {
Collections.sort(props);
}
// Write the data to disk
final BufferedOutputStream bout = new BufferedOutputStream(out);
final Writer writer = new OutputStreamWriter(bout, encoding);
final String lf = System.getProperty("line.separator");
// Write comment
for (int i = 0; i < comments.length; i++) {
writer.write("# ");
writer.write(comments[i]);
writer.write(lf);
}
// Save all values
for (int i = 0; i < props.size(); i++) {
final Property prop = (Property) props.get(i);
if (!prop.isDeleted()) {
writer.write(prop.toKeyValue());
writer.write(lf);
// Replace the property with the new status
props.set(i, new Property(prop.getKey(), prop.getValue(), prop.getValue()));
}
}
writer.flush();
out.truncate();
out.flush();
} finally {
lock.release();
}
} finally {
out.close();
}
// Remove all deleted entries
for (int i = props.size() - 1; i >= 0; i--) {
final Property prop = (Property) props.get(i);
if (prop.isDeleted()) {
props.remove(i);
}
}
}
private Property find(final String key) {
for (int i = 0; i < props.size(); i++) {
final Property prop = (Property) props.get(i);
if (prop.getKey().equals(key)) {
return prop;
}
}
return null;
}
/**
* Returns a value for a given key.
*
* @param key
* Key to find.
*
* @return Value or null
if the key is unknown.
*/
public final String get(final String key) {
final Property prop = find(key);
if (prop == null) {
return null;
}
return prop.getValue();
}
/**
* Returns a status text for a given key.
*
* @param key
* Key to find.
*
* @return Status text or null
if the key is unknown.
*/
public final String getStatus(final String key) {
final Property prop = find(key);
if (prop == null) {
return null;
}
return prop.getStatus();
}
/**
* Set a value for a property. If a property with the key is already known
* the value will be changed. Otherwise a new property will be created.
*
* @param key
* Key to set.
* @param value
* Value to set.
*/
public final void put(final String key, final String value) {
final Property prop = find(key);
if (prop == null) {
props.add(new Property(key, null, value));
} else {
prop.setValue(value);
}
}
/**
* Remove the property with the given key. The internal property object is
* not deleted itself but it's value is set to null
and the
* method isDeleted()
will return true
.
*
* @param key
* Key for the property to remove.
*/
public final void remove(final String key) {
final Property prop = find(key);
if (prop != null) {
prop.setValue(null);
}
}
/**
* Returns if a property has been deleted.
*
* @param key
* Key for the property to check.
*
* @return If the property is unknown or has been deleted true
* else false
.
*/
public final boolean isRemoved(final String key) {
final Property prop = find(key);
if (prop == null) {
return true;
}
return prop.isDeleted();
}
/**
* Number of properties.
*
* @return All known properties including deleted ones.
*/
public final int size() {
return props.size();
}
/**
* Returns the underlying file.
*
* @return Properties file reference.
*/
public final File getFile() {
return file;
}
/**
* Returns a list of all known keys including the deleted ones.
*
* @return List of keys.
*/
public final List getKeyList() {
final List keys = new ArrayList();
final Iterator it = keyIterator();
while (it.hasNext()) {
keys.add(it.next());
}
return keys;
}
/**
* Returns an array of all known keys including the deleted ones.
*
* @return Array of keys.
*/
public final String[] getKeyArray() {
return (String[]) getKeyList().toArray(new String[0]);
}
/**
* Returns a key iterator.
*
* @return Iterates over all keys including the deleted ones.
*/
public final Iterator keyIterator() {
return new Iterator() {
private final Iterator it = props.iterator();
public boolean hasNext() {
return it.hasNext();
}
public Object next() {
final Property prop = (Property) it.next();
return prop.getKey();
}
public void remove() {
it.remove();
}
};
}
/**
* Returns a copy of all properties.
*
* @return All key/values without deleted ones.
*/
public final Properties toProperties() {
final Properties retVal = new Properties();
for (int i = 0; i < props.size(); i++) {
final Property prop = (Property) props.get(i);
if (!prop.isDeleted()) {
retVal.put(prop.getKey(), prop.getValue());
}
}
return retVal;
}
/**
* Returns the number of tries to lock before throwing an exception.
*
* @return Number of tries (default=3).
*/
public final int getTryLockMax() {
return tryLockMax;
}
/**
* Sets the number of tries to lock before throwing an exception.
*
* @param tryLockMax
* Number of tries (default=3).
*/
public final void setTryLockMax(final int tryLockMax) {
this.tryLockMax = tryLockMax;
}
/**
* Returns the milliseconds to sleep between retries.
*
* @return Milliseconds.
*/
public final long getTryWaitMillis() {
return tryWaitMillis;
}
/**
* Sets the milliseconds to sleep between retries.
*
* @param tryWaitMillis
* Milliseconds.
*/
public final void setTryWaitMillis(final long tryWaitMillis) {
this.tryWaitMillis = tryWaitMillis;
}
/**
* Determines if the underlying file already exists.
*
* @return If the file exists true
else false
*/
public final boolean exists() {
return file.exists();
}
/**
* Tries to delete the underlying file. The properties in memory remain
* unchanged. If you want also to remove the properties in memory call
* clear()
.
*
* @return If the files was deleted true
else
* false
*/
public final boolean delete() {
return file.delete();
}
}