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