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