blob: 2d2a0a8f2c8021805a14d0dc57728d8c95b63729 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013 BSI Business Systems Integration AG.
* 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:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.sdk.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.scout.commons.CompareUtility;
import org.eclipse.scout.commons.IOUtility;
import org.eclipse.scout.commons.StringUtility;
import org.eclipse.scout.commons.exception.ProcessingException;
import org.eclipse.scout.sdk.util.log.ScoutStatus;
/**
* <h3>{@link FormatPreservingProperties}</h3> Properties class that can load and store .properties files.<br>
* The difference to the one provided by the JRE ({@link Properties}) is that this class preserves comments, empty lines
* and the property order that existed in the original file when writing it again.<br>
* <br>
* All operations of this class are thread save and can directly be consumed from parallel threads. Multiple readers may
* query the properties at the same time. But there is only one writer allowed (write lock is exclusive).<br>
* <br>
* This class holds the entire .properties file in memory when loaded from a file or input stream. Therefore it should
* be used with care when dealing with large files.<br>
* <br>
* As defined in the .properties file specification the files are loaded and stored using the ISO 8859-1 encoding.
*
* @author Matthias Villiger
* @since 3.10.0 05.10.2013
* @see #ENCODING
* @see Properties
*/
public class FormatPreservingProperties implements Serializable {
private static final long serialVersionUID = 1L;
/**
* According to the .properties file specification all files are encoded using ISO 8859-1 character encoding.
*/
public static final String ENCODING = "8859_1";
private final Properties m_properties;
private final ReentrantReadWriteLock m_lock;
private final ArrayList<P_PropertyLine> m_lines;
private Map<String, String> m_origValues;
public FormatPreservingProperties() {
m_properties = new Properties();
m_lines = new ArrayList<FormatPreservingProperties.P_PropertyLine>();
m_lock = new ReentrantReadWriteLock(true);
}
/**
* Loads the content of the given properties file.<br>
* As defined in the .properties file specification the files are loaded using the ISO 8859-1 encoding.
*
* @param f
* The file to load. Must exist and be accessible and must be of the .properties file format.
* @throws CoreException
* @see {@link #ENCODING}
*/
public void load(IFile f) throws CoreException {
InputStream is = null;
try {
is = f.getContents();
load(is);
}
finally {
if (is != null) {
try {
is.close();
}
catch (IOException e) {
throw new CoreException(new ScoutStatus(e));
}
}
}
}
/**
* Loads the content of the given input stream.<br>
* As defined in the .properties file specification the files are loaded using the ISO 8859-1 encoding.
*
* @param is
* The input stream providing the data to load.
* @throws CoreException
* @see {@link #ENCODING}
*/
public void load(InputStream is) throws CoreException {
try {
load(IOUtility.getContent(is, false));
}
catch (ProcessingException e) {
throw new CoreException(new ScoutStatus(e));
}
}
/**
* Stores the properties in the given output stream.<br>
* As defined in the .properties file specification the files are stored using the ISO 8859-1 encoding.
*
* @param out
* @throws CoreException
*/
public void store(OutputStream out) throws CoreException {
BufferedWriter writer = null;
try {
m_lock.readLock().lock();
writer = new BufferedWriter(new OutputStreamWriter(out, ENCODING));
for (P_PropertyLine line : m_lines) {
if (!line.ignore) {
if (StringUtility.hasText(line.key)) {
writer.write(getLineFormatted(line.key));
}
else {
writer.write(line.comment);
}
writer.newLine();
}
}
writer.flush();
}
catch (IOException e) {
throw new CoreException(new ScoutStatus(e));
}
finally {
m_lock.readLock().unlock();
}
}
/**
* Sets a new value for a property.<br>
* If it is a new property, it is appended to the end of the file when storing it. The order in which the new
* properties are appended is the same as in which they have been added to this instance.
*
* @param key
* The key of the property.
* @param value
* The new value.
*/
public void setProperty(String key, String value) {
if (key == null || value == null) {
throw new IllegalArgumentException();
}
try {
m_lock.writeLock().lock();
boolean newKey = !m_properties.containsKey(key);
m_properties.setProperty(key, value);
if (newKey) {
P_PropertyLine newLine = new P_PropertyLine();
newLine.key = key;
m_lines.add(newLine);
}
}
finally {
m_lock.writeLock().unlock();
}
}
/**
* Gets the value of a property.
*
* @param key
* The key of the property to get.
* @return The value of the property with the given key.
*/
public String getProperty(String key) {
if (key == null) {
throw new IllegalArgumentException("null is not a valid property key");
}
try {
m_lock.readLock().lock();
return m_properties.getProperty(key);
}
finally {
m_lock.readLock().unlock();
}
}
/**
* @param key
* The key of the property to check
* @return true if a property with the given name exists, false otherwise.
*/
public boolean containsProperty(String key) {
try {
m_lock.readLock().lock();
return m_properties.containsKey(key);
}
finally {
m_lock.readLock().unlock();
}
}
/**
* Removes the property with the given key.
*
* @param key
* The key of the property to remove.
*/
public void removeProperty(String key) {
if (key == null) {
throw new IllegalArgumentException("null is not a valid property key");
}
try {
m_lock.writeLock().lock();
m_properties.remove(key);
for (Iterator<P_PropertyLine> it = m_lines.iterator(); it.hasNext();) {
P_PropertyLine line = it.next();
if (CompareUtility.equals(key, line.key)) {
it.remove();
break;
}
}
}
finally {
m_lock.writeLock().unlock();
}
}
/**
* @return Gets a copy of the key-value-pairs of all properties.
*/
public Map<String, String> getEntries() {
try {
m_lock.readLock().lock();
HashMap<String, String> result = new HashMap<String, String>(m_properties.size());
for (Entry<Object, Object> entry : m_properties.entrySet()) {
result.put((String) entry.getKey(), (String) entry.getValue());
}
return result;
}
finally {
m_lock.readLock().unlock();
}
}
/**
* @return Gets the number of properties hold.
*/
public int size() {
try {
m_lock.readLock().lock();
return m_properties.size();
}
finally {
m_lock.readLock().unlock();
}
}
/**
* Checks if this class has been modified since it was loaded the last time.
*
* @return true if the properties are different than after the last load. false otherwise.
*/
public boolean isDirty() {
try {
m_lock.readLock().lock();
if (this.size() != m_origValues.size()) {
return true;
}
for (Entry<String, String> entry : m_origValues.entrySet()) {
if (CompareUtility.notEquals(this.getProperty(entry.getKey()), entry.getValue())) {
return true;
}
}
return false;
}
finally {
m_lock.readLock().unlock();
}
}
private void load(byte[] data) {
BufferedReader reader = null;
try {
m_lock.writeLock().lock();
reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data), ENCODING));
reader.mark(data.length + 1);
// clear old values
m_lines.clear();
m_properties.clear();
// first load the properties into the map to have the right property parsing
m_properties.load(reader);
// then parse out the formatting information (comments and empty lines)
reader.reset();
String lineContent = null;
boolean lastLineEndsWithBackSlash = false;
while ((lineContent = reader.readLine()) != null) {
String lineContentTrim = lineContent.trim();
P_PropertyLine line = new P_PropertyLine();
line.ignore = lastLineEndsWithBackSlash;
if (!lastLineEndsWithBackSlash) {
if (lineContentTrim.length() < 1 || lineContentTrim.charAt(0) == '#' || lineContentTrim.charAt(0) == '!') {
// the current line does not hold a key-value-pair
line.comment = lineContent;
}
else {
String key = findKey(lineContent);
if (StringUtility.hasText(key)) {
// the current line holds the value of a key
line.key = key;
}
else {
throw new IllegalArgumentException("Invalid properties file format");
}
}
}
m_lines.add(line);
lastLineEndsWithBackSlash = lineContentTrim.length() > 0 && lineContentTrim.charAt(lineContentTrim.length() - 1) == '\\';
}
// remember the values loaded
m_origValues = getEntries();
}
catch (IOException e) {
// cannot happen
}
finally {
if (reader != null) {
try {
reader.close();
}
catch (IOException e) {
// cannot happen
}
}
m_lock.writeLock().unlock();
}
}
private String getLineFormatted(String key) {
Properties parser = new Properties();
parser.setProperty(key, m_properties.getProperty(key));
ByteArrayOutputStream buffer = null;
BufferedReader lineReader = null;
try {
buffer = new ByteArrayOutputStream();
parser.store(buffer, null);
lineReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buffer.toByteArray()), ENCODING));
lineReader.readLine();// skip the comment
return lineReader.readLine();
}
catch (IOException e) {
// cannot happen
return null;
}
finally {
if (buffer != null) {
try {
buffer.close();
}
catch (IOException e) {
// cannot happen
}
}
if (lineReader != null) {
try {
lineReader.close();
}
catch (IOException e) {
// cannot happen
}
}
}
}
private String findKey(String lineContent) {
Properties parser = new Properties();
InputStream is = null;
try {
is = new ByteArrayInputStream(lineContent.getBytes(ENCODING));
parser.load(is);
Set<Object> keySet = parser.keySet();
if (keySet.size() == 0) {
// the current line could not be parsed to a key-value-pair
return null;
}
else if (keySet.size() == 1) {
// a single key-value-pair was found
return (String) keySet.iterator().next();
}
else {
throw new IllegalArgumentException("Invalid properties file format");
}
}
catch (IOException e) {
// cannot happen with a byte input stream
return null;
}
finally {
if (is != null) {
try {
is.close();
}
catch (IOException e) {
}
}
}
}
private final static class P_PropertyLine implements Serializable {
private static final long serialVersionUID = 1L;
// always set
private boolean ignore;
// one of these is set
private String comment;
private String key;
}
}