Ini.java

/*
 * Ini
 *
 * $Id$
 * $URL$
 */
package gov.usgs.util;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;

import java.util.Collections;
import java.util.Iterator;
import java.util.Properties;
import java.util.HashMap;

/**
 * Ini is a Properties that supports sections.
 *
 * Format Rules:
 * <ul>
 * <li>Empty lines are ignored.
 * <li>Leading and trailing white space are ignored.
 * <li>Comments must be on separate lines, and begin with '#' or ';'.
 * <li>Properties are key value pairs delimited by an equals: key = value
 * <li>Section Names are on separate lines, begin with '[' and end with ']'. Any
 * whitespace around the brackets is ignored.
 * <li>Any properties before the first section are in the "null" section
 * </ul>
 *
 * Format Example:
 *
 * <pre>
 * #comment about the global
 * global = value
 *
 * # comment about this section
 * ; another comment about this section
 * [ Section Name ]
 * section = value
 * </pre>
 *
 */
public class Ini extends Properties {

	/** Serialization version. */
	private static final long serialVersionUID = 1L;

	/** Section names map to Section properties. */
	private HashMap<String, Properties> sections = new HashMap<String, Properties>();

	/** String for representing a comment start */
	public static final String COMMENT_START = ";";
	/** String for representing an alternate comment start */
	public static final String ALTERNATE_COMMENT_START = "#";

	/** String to represent a section start */
	public static final String SECTION_START = "[";
	/** String to represent a section end */
	public static final String SECTION_END = "]";
	/** String to delimit properties */
	public static final String PROPERTY_DELIMITER = "=";

	/**
	 * Same as new Ini(null).
	 */
	public Ini() {
		this(null);
	}

	/**
	 * Construct a new Ini with defaults.
	 *
	 * @param properties
	 *            a Properties or Ini object with defaults. If an Ini object,
	 *            also makes a shallow copy of sections.
	 */
	public Ini(final Properties properties) {
		super(properties);

		if (properties instanceof Ini) {
			sections.putAll(((Ini) properties).getSections());
		}
	}

	/**
	 * @return the section properties map.
	 */
	public HashMap<String, Properties> getSections() {
		return sections;
	}

	/**
	 * Get a section property.
	 *
	 * @param section
	 *            the section, if null calls getProperty(key).
	 * @param key
	 *            the property name.
	 * @return value or property, or null if no matching property found.
	 */
	public String getSectionProperty(String section, String key) {
		if (section == null) {
			return getProperty(key);
		} else {
			Properties props = sections.get(section);
			if (props != null) {
				return props.getProperty(key);
			} else {
				return null;
			}
		}
	}

	/**
	 * Set a section property.
	 *
	 * @param section
	 *            the section, if null calls super.setProperty(key, value).
	 * @param key
	 *            the property name.
	 * @param value
	 *            the property value.
	 * @return any previous value for key.
	 */
	public Object setSectionProperty(String section, String key, String value) {
		if (section == null) {
			return setProperty(key, value);
		} else {
			Properties props = sections.get(section);
			if (props == null) {
				// new section
				props = new Properties();
				sections.put(section, props);
			}
			return props.setProperty(key, value);
		}
	}

	/**
	 * Read an Ini input stream.
	 *
	 * @param inStream
	 *            the input stream to read.
	 * @throws IOException
	 *         if unable to parse input stream.
	 */
	public void load(InputStream inStream) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(inStream));

		// keep track of current line number
		int lineNumber = 0;
		// line being parsed
		String line;
		// section being parsed
		Properties section = null;

		while ((line = br.readLine()) != null) {
			lineNumber = lineNumber + 1;
			line = line.trim();

			// empty line or comment
			if (line.length() == 0 || line.startsWith(COMMENT_START)
					|| line.startsWith(ALTERNATE_COMMENT_START)) {
				// ignore
				continue;
			}

			// section
			else if (line.startsWith(SECTION_START)
					&& line.endsWith(SECTION_END)) {
				// remove brackets
				line = line.replace(SECTION_START, "");
				line = line.replace(SECTION_END, "");
				line = line.trim();

				// store all properties in section
				section = new Properties();
				getSections().put(line, section);
			}

			// parse as property
			else {
				int index = line.indexOf("=");
				if (index == -1) {
					throw new IOException("Expected " + PROPERTY_DELIMITER
							+ " on line " + lineNumber + ": '" + line + "'");
				} else {
					String[] parts = line.split(PROPERTY_DELIMITER, 2);
					String key = parts[0].trim();
					String value = parts[1].trim();
					if (section != null) {
						section.setProperty(key, value);
					} else {
						setProperty(key, value);
					}
				}
			}

		}

		br.close();
	}

	/**
	 * Write an Ini format to a PrintWriter.
	 *
	 * @param props
	 *            properties to write.
	 * @param writer
	 *            the writer that writes.
	 * @param header
	 *            an optioal header that will appear in comments at the start of
	 *            the ini format.
	 * @throws IOException
	 *         if unable to write output.
	 */
	@SuppressWarnings("unchecked")
	public static void write(final Properties props, final PrintWriter writer,
			String header) throws IOException {

		if (header != null) {
			// write the header
			writer.write(new StringBuffer(COMMENT_START).append(" ").append(
					header.trim().replace("\n", "\n" + COMMENT_START + " "))
					.append("\n").toString());
		}

		// write properties
		Iterator<String> iter = (Iterator<String>) Collections.list(
				props.propertyNames()).iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			writer.write(new StringBuffer(key).append(PROPERTY_DELIMITER)
					.append(props.getProperty(key)).append("\n").toString());
		}

		// write sections
		if (props instanceof Ini) {
			Ini ini = (Ini) props;
			iter = ini.getSections().keySet().iterator();
			while (iter.hasNext()) {
				String sectionName = iter.next();
				writer.write(new StringBuffer(SECTION_START)
						.append(sectionName).append(SECTION_END).append("\n")
						.toString());
				write(ini.getSections().get(sectionName), writer, null);
			}
		}

		// flush, but don't close
		writer.flush();
	}

	/**
	 * Calls write(new PrintWriter(out), header).
	 */
	public void store(OutputStream out, String header) throws IOException {
		write(this, new PrintWriter(out), header);
	}

	/**
	 * Write properties to an OutputStream.
	 *
	 * @param out
	 *            the OutputStream used for writing.
	 */
	public void save(final OutputStream out) {
		try {
			store(out, null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Write properties to a PrintStream.
	 *
	 * @param out
	 *            the PrintStream used for writing.
	 */
	public void list(PrintStream out) {
		try {
			store(out, null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Write properties to a PrintWriter.
	 *
	 * @param out
	 *            the PrintWriter used for writing.
	 */
	public void list(PrintWriter out) {
		try {
			write(this, out, null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}