/*
* RHQ Management Platform
* Copyright (C) 2005-2012 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* 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 General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.core.util;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.Properties;
/**
* This utility helps update one or more properties in a .properties file without losing the ordering of existing
* properties or comment lines. You can update changes to existing properties or add new properties. Currently, there is
* no way to remove properties from a properties file (but you can set their values to an empty string).
*
* <p>Note that this utility only works on simple properties files where each name=value pair exists on single lines
* (i.e. they do not span multiple lines). But it can handle #-prefixed lines (i.e. comments are preserved).</p>
*
* <p>This utility takes care to read and write using the ISO-8859-1 character set since that is what {@link Properties}
* uses to load and store properties, too.</p>
*
* @author John Mazzitelli
*/
public class PropertiesFileUpdate {
private static final String CHAR_ENCODING_8859_1 = "8859_1";
private File file;
/**
* Constructor given the full path to the .properties file.
*
* @param location location of the file
*/
public PropertiesFileUpdate(String location) {
this.file = new File(location);
}
/**
* Constructor given the .properties file.
*
* @param the properties file
*/
public PropertiesFileUpdate(File file) {
this.file = file;
}
/**
* Updates the properties file so it will contain the key with the value. If value is <code>null</code>, an empty
* string will be used in the properties file. If the property does not yet exist in the properties file, it will be
* appended to the end of the file.
*
* @param key the property name whose value is to be updated
* @param value the new property value
*
* @throws IOException if an error occurs reading or writing the properties file
*/
public boolean update(String key, String value) throws IOException {
if (value == null) {
value = "";
}
Properties existingProps = loadExistingProperties();
// if the given property is new (doesn't exist in the file yet) just append it and return
// if the property exists, update the value in place (ignore if the value isn't really changing)
if (!existingProps.containsKey(key)) {
boolean appendNewlineBeforeAppendingProperty = (file.exists() && (file.length() != 0) &&
!isFileLineSeparatorTerminated());
FileOutputStream fos = new FileOutputStream(file, true);
try {
PrintStream ps = new PrintStream(fos, true, CHAR_ENCODING_8859_1);
try {
if (appendNewlineBeforeAppendingProperty) {
ps.println();
}
ps.println(key + "=" + value);
} finally {
ps.close();
}
} finally {
fos.close();
}
} else if (!value.equals(existingProps.getProperty(key))) {
Properties newProps = new Properties();
newProps.setProperty(key, value);
update(newProps);
}
return existingProps.containsKey(key);
}
/**
* Updates the existing properties file with the new properties. If a property is in <code>newProps</code> that
* already exists in the properties file, the existing property is updated in place. Any new properties found in
* <code>newProps</code> that does not yet exist in the properties file will be added. Currently existing properties
* in the properties file that are not found in <code>newProps</code> will remain as-is.
*
* @param newProps properties that are to be added or updated in the file
*
* @throws IOException
*/
public void update(Properties newProps) throws IOException {
// make our own copy - we will eventually empty out our copy (also avoids concurrent mod exceptions later)
Properties propsToUpdate = new Properties();
propsToUpdate.putAll(newProps);
// load these in so we don't have to parse out the =value ourselves
Properties existingProps = loadExistingProperties();
// Immediately eliminate new properties whose values are the same as the existing properties.
// Once we finish this, we are assured all new properties are always different than existing properties.
for (Map.Entry<Object, Object> entry : newProps.entrySet()) {
if (entry.getValue().equals(existingProps.get(entry.getKey()))) {
propsToUpdate.remove(entry.getKey());
}
}
// Now go line-by-line in the properties file, updating property values as we go along.
// When we get to the end of the existing file, append any new props that didn't exist before.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream out = new PrintStream(baos, true, CHAR_ENCODING_8859_1);
InputStreamReader isr = new InputStreamReader(new FileInputStream(file), CHAR_ENCODING_8859_1);
BufferedReader in = new BufferedReader(isr);
for (String line = in.readLine(); line != null; line = in.readLine()) {
int equalsSign = line.indexOf('=');
// echo lines that are not name=value property lines;
// this includes blank lines, comments or lines that do not have an = character
if (line.startsWith("#") || (line.trim().length() == 0) || (equalsSign < 0)) {
out.println(line);
} else {
String existingKey = line.substring(0, equalsSign);
existingKey = trimString(existingKey, false, true);
if (!propsToUpdate.containsKey(existingKey)) {
out.println(line); // property that is not being updated; leave it alone and write it out as-is
} else {
out.println(existingKey + "=" + propsToUpdate.getProperty(existingKey));
propsToUpdate.remove(existingKey); // done with it so we can remove it from our copy
}
}
}
// done reading the file, we can close it now
in.close();
// append to the output any new properties that did not exist before
for (Map.Entry<Object, Object> entry : propsToUpdate.entrySet()) {
out.println(entry.getKey() + "=" + entry.getValue());
}
// done with building the contents of the updated properties file
out.close();
// now we can take the new contents of the file and overwrite the contents of the old file
FileOutputStream fos = new FileOutputStream(file, false);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
return;
}
/**
* Loads and returns the properties that exist currently in the properties file.
*
* @return properties that exist in the properties file
*
* @throws IOException
*/
public Properties loadExistingProperties() throws IOException {
Properties props = new Properties();
if (file.exists() && (file.length() != 0)) {
FileInputStream is = new FileInputStream(file);
try {
props.load(is);
} finally {
is.close();
}
}
return props;
}
private String trimString(String str, boolean trimStart, boolean trimEnd) {
int start = 0;
int end = str.length();
if (trimStart) {
while ((start < end) && (str.charAt(start) == ' ')) {
start++;
}
}
if (trimEnd) {
while ((start < end) && (str.charAt(end - 1) == ' ')) {
end--;
}
}
return ((start > 0) || (end < str.length())) ? str.substring(start, end) : str;
}
private boolean isFileLineSeparatorTerminated() throws IOException {
if (!file.exists() || (file.length() == 0)) {
return false;
}
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
int lastByteOfFile;
try {
randomAccessFile.seek(file.length() - 1);
lastByteOfFile = randomAccessFile.read();
} finally {
randomAccessFile.close();
}
boolean fileIsLineSeparatorTerminated = false;
if ((lastByteOfFile == '\n') ||
((lastByteOfFile == '\r') && "\r".equals(System.getProperty("line.separator")))) {
fileIsLineSeparatorTerminated = true;
}
return fileIsLineSeparatorTerminated;
}
}