/* * RHQ Management Platform * Copyright (C) 2005-2008 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.enterprise.agent; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; /** * This utility helps update one or more environment script files without losing the ordering of existing * variables or comment lines. An "environment script" is a script file that simply contains lines that * set environment variables (i.e. "RHQ_AGENT_MY_VAR=some.value" or "set NAME=VALUE" for Windows scripts). * * <p>You can update changes to existing environment variables or add new ones.</p> * * <p>Note that this utility only works on simple environment script files where each name=value * pair exists on single lines (i.e. they do not span multiple lines). But it can handle * commented lines (i.e. comments are preserved).</p> * * @author John Mazzitelli */ public abstract class EnvironmentScriptFileUpdate { private File file; /** * Factory method that creates an update object that is appropriate * to update the given file. * This creates update options for files that have these extensions: * * <ul> * <li>.conf/inc = {@link JavaServiceWrapperConfigurationFileUpdate}</li> * <li>.env = {@link JavaServiceWrapperEnvironmentScriptFileUpdate}</li> * <li>.bat/cmd = {@link WindowsEnvironmentScriptFileUpdate}</li> * <li>anything else = {@link UnixEnvironmentScriptFileUpdate}</li> * </ul> * * @param location location of the script file * * @return the update object that is appropriate to update the file */ public static EnvironmentScriptFileUpdate create(String location) { if (location.endsWith(".env")) { return new JavaServiceWrapperEnvironmentScriptFileUpdate(location); } else if (location.endsWith(".inc") || location.endsWith(".conf")) { return new JavaServiceWrapperConfigurationFileUpdate(location); } else if (location.endsWith(".bat") || location.endsWith(".cmd")) { return new WindowsEnvironmentScriptFileUpdate(location); } else { return new UnixEnvironmentScriptFileUpdate(location); } } /** * Constructor given the full path to script file. * * @param location location of the file */ public EnvironmentScriptFileUpdate(String location) { this.file = new File(location); } /** * Updates the script file so it will contain the key with the value (where the key is the * name of the environment variable). If value is <code>null</code>, an empty * string will be used in file. If the variable does not yet exist in the properties file, it will be * appended to the end of the file. * * @param key the env var name whose value is to be updated * @param value the new env var value * * @throws IOException */ public void update(NameValuePair nvp) throws IOException { if (nvp.value == null) { nvp.value = ""; } List<NameValuePair> existingList = loadExisting(); Properties existing = convertNameValuePairListToProperties(existingList); // if the given env var is new (doesn't exist in the file yet) just append it and return. // if it does exist, update the value in place (ignore if the value isn't really changing) if (!existing.containsKey(nvp.name)) { PrintStream ps = new PrintStream(new FileOutputStream(file, true), true); ps.println(); ps.println(createEnvironmentVariableLine(nvp)); ps.flush(); ps.close(); } else if (!nvp.value.equals(existing.getProperty(nvp.name))) { List<NameValuePair> newList = new ArrayList<NameValuePair>(); newList.add(nvp); update(newList, false); } return; } /** * Updates the existing script file with the new name/value settings. If an env var is in <code>newValues</code> that * already exists in the file, the existing setting is updated in place. Any new setting found in * <code>newValues</code> that does not yet exist in the file will be added. Currently existing settings * in the script file that are not found in <code>newValues</code> will remain as-is. * * @param newValuesList environment variable settings that are added or updated in the file * @param deleteMissing if <code>true</code>, any settings found in the existing file that are missing * from the given <code>newValues</code> will be removed from the existing file. * if <code>false</code>, then <code>newValues</code> is assumed to be only a subset * of the settings that can go in the file and thus any settings found in the * existing file but are missing from the new values will not be deleted. * @throws IOException */ public void update(List<NameValuePair> newValuesList, boolean deleteMissing) throws IOException { Properties newValues = convertNameValuePairListToProperties(newValuesList); // make our own copy - we will eventually empty out our copy (also avoids concurrent mod exceptions later) Properties settingsToUpdate = new Properties(); settingsToUpdate.putAll(newValues); // prepare the in-memory buffer where we will store the new file contents. // yes this means we expect to load the entire file contents in memory, but these // files are very small and this should not be a problem. ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream out = new PrintStream(baos, true); if (file.exists()) { // load these in so we don't have to parse out the =value ourselves List<NameValuePair> existingList = loadExisting(); Properties existing = convertNameValuePairListToProperties(existingList); // Immediately eliminate new settings 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 : newValues.entrySet()) { if (entry.getValue().equals(existing.get(entry.getKey()))) { settingsToUpdate.remove(entry.getKey()); } } // Now go line-by-line in the script file, updating name=value settings as we go along. // When we get to the end of the existing file, append any new settings that didn't exist before. InputStreamReader isr = new InputStreamReader(new FileInputStream(file)); BufferedReader in = new BufferedReader(isr); for (String line = in.readLine(); line != null; line = in.readLine()) { NameValuePair nameValue = parseEnvironmentVariableLine(line); // echo lines that are not name=value settings; // this includes blank lines, comments, etc if (nameValue == null) { out.println(line); } else { String existingKey = nameValue.name; if (!settingsToUpdate.containsKey(existingKey)) { if (!deleteMissing || newValues.getProperty(existingKey) != null) { out.println(line); // property that is not being updated or removed; leave it alone and write it out as-is } } else { NameValuePair newNvp = new NameValuePair(existingKey, settingsToUpdate.getProperty(existingKey)); out.println(createEnvironmentVariableLine(newNvp)); settingsToUpdate.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 : settingsToUpdate.entrySet()) { NameValuePair nvp = new NameValuePair(entry.getKey().toString(), entry.getValue().toString()); out.println(createEnvironmentVariableLine(nvp)); } // 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. * If the file does not exist, an empty set of properties is returned. * * @return properties that exist in the properties file * * @throws IOException */ public List<NameValuePair> loadExisting() throws IOException { List<NameValuePair> props = new ArrayList<NameValuePair>(); if (file.exists()) { BufferedReader in = new BufferedReader(new FileReader(file)); try { String line = in.readLine(); while (line != null) { NameValuePair nvp = parseEnvironmentVariableLine(line); if (nvp != null) { props.add(nvp); } line = in.readLine(); } } finally { in.close(); } } return props; } public Properties convertNameValuePairListToProperties(List<NameValuePair> list) { // converting the list to a properties loses the ordering, but sometimes you don't care Properties props = new Properties(); if (list != null) { for (NameValuePair nvp : list) { props.setProperty(nvp.name, nvp.value); } } return props; } public List<NameValuePair> convertPropertiesToNameValuePairList(Properties props) { // the ordering if the list is indeterminate - but sometimes you'd like a list object instead of a properties List<NameValuePair> list = new ArrayList<NameValuePair>(); if (props != null) { for (Map.Entry<Object, Object> entry : props.entrySet()) { list.add(new NameValuePair(entry.getKey().toString(), entry.getValue().toString())); } } return list; } public static class NameValuePair implements Serializable { private static final long serialVersionUID = 1L; public String name; public String value; public NameValuePair(String n, String v) { if (n == null) { throw new IllegalArgumentException("n == null"); } name = n; value = v; } @Override public boolean equals(Object obj) { if (obj instanceof NameValuePair) { return name.equals(((NameValuePair) obj).name); } return false; } @Override public int hashCode() { return name.hashCode(); } } /** * Creates a line that defines an environment variable name and its value. * * @param nvp the environment variable definition * * @return the line that is to be used in the script file to define the environment variable */ abstract protected String createEnvironmentVariableLine(NameValuePair nvp); /** * Parses the given string that is a line from a environment script file. * If this is not a line that defines an environment variable, <code>null</code> * is returned, otherwise, the environment variable name and value is returned. * * @param line the line from the environment script file * * @return the name, value of the environment that is defined in the line, or <code>null</code> * if the line doesn't define an env var. */ abstract protected NameValuePair parseEnvironmentVariableLine(String line); }