All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.peterphi.std.io.PropertyFile Maven / Gradle / Ivy

There is a newer version: 10.1.5
Show newest version
package com.peterphi.std.io;

import com.peterphi.std.io.properties.IMergeConflictResolver;
import com.peterphi.std.util.HexHelper;
import com.peterphi.std.util.ListUtility;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class PropertyFile
{
	private static final transient Logger log = Logger.getLogger(PropertyFile.class);
	private static final String NEWLINE = "\n";
	private static final String COMMENT_INST = "#-- ";
	private static final char COMMENT_CHAR = '#';

	// The file to use for load() and save() methods
	protected File f;
	protected boolean readOnly = false;
	protected boolean caseSensitive = true;
	protected boolean forceNameValueDelimiterWhitespace = false;

	// Subclasses shouldn't have access to these
	protected List entries = new ArrayList();
	protected Map vars = new HashMap();


	public static PropertyFile find()
	{
		return find("service.properties");
	}


	public static PropertyFile find(final String fileName)
	{
		// Loading exact files
		try
		{
			if (fileName.startsWith("/"))
			{
				return PropertyFile.readOnly(new File(fileName));
			}
		}
		catch (IOException e)
		{
			throw new IllegalArgumentException("Property file could not be loaded: " + fileName);
		}

		return find(PropertyFile.class.getClassLoader(), fileName);
	}


	/**
	 * Find a property file
	 *
	 * @param classloader
	 * @param fileName
	 *
	 * @return
	 */
	public static PropertyFile find(final ClassLoader classloader, final String... fileNames)
	{
		URL resolvedResource = null;
		String resolvedFile = null;

		for (String fileName : fileNames)
		{
			if (fileName.charAt(0) == '/')
			{
				File file = new File(fileName);

				if (file.exists())
				{
					try
					{
						return PropertyFile.readOnly(file);
					}
					catch (IOException e)
					{
						throw new IllegalArgumentException("Error loading property file: " +
						                                   fileName +
						                                   ". Error: " +
						                                   e.getMessage(), e);
					}
				}
			}
			else
			{
				// Try to resolve the filename (for logging any errors)
				final URL resource = classloader.getResource(fileName);

				if (resource != null)
				{
					resolvedFile = fileName;
					resolvedResource = resource;
					break;
				}
			}
		}

		if (resolvedFile == null)
		{
			if (fileNames.length == 1)
				throw new IllegalArgumentException("Error finding property file in classpath: " + fileNames[0]);
			else
				throw new IllegalArgumentException("Error finding property files in classpath: " + Arrays.asList(fileNames));
		}
		else if (log.isInfoEnabled())
			log.info("{find} Loading properties from " + resolvedFile);

		return openResource(classloader, resolvedResource, resolvedFile);
	}


	public static PropertyFile fromString(final String contents)
	{
		return fromString(contents, "unknown");
	}


	public static PropertyFile fromString(final String contents, final String filename)
	{
		PropertyFile props = new PropertyFile();
		try
		{
			props.load(new StringReader(contents));
		}
		catch (IOException e)
		{

			throw new IllegalArgumentException("Error loading property file from string. Error: " + e.getMessage(), e);
		}

		return props;
	}


	public static PropertyFile openResource(final ClassLoader classloader, final URL resource, final String fileName)
	{
		try
		{
			if (log.isTraceEnabled())
				log.trace("{find} Resource search results: " + resource);

			if (resource.getProtocol().equalsIgnoreCase("file"))
			{
				final String fileComponent = resource.getFile(); // Get the raw file component from the URL
				final String filePath = URLDecoder.decode(fileComponent, "UTF8"); // Decode any URL encoded values (e.g. spaces)

				final File file = new File(filePath);

				return PropertyFile.readOnly(file);
			}
			else
			{
				log.trace("{openResource} Falling back to opening resource as stream: PropertyFile filename will remain unknown");
				InputStream is = classloader.getResourceAsStream(fileName);

				PropertyFile props = new PropertyFile();
				props.load(is);

				return props;
			}
		}
		catch (IOException e)
		{
			throw new IllegalArgumentException("Error finding/loading property file: " + fileName + ". Error: " + e.getMessage(),
			                                   e);
		}
	}


	/**
	 * Find all property files by the name name by searching the classloader that owns PropertyFile
	 *
	 * @param name
	 *
	 * @return
	 */
	public static PropertyFile[] findAll(final String name)
	{
		return findAll(PropertyFile.class.getClassLoader(), name);
	}


	/**
	 * Find all property files by the name name by searching the specified classloader
	 *
	 * @param loader
	 * @param name
	 *
	 * @return
	 */
	public static PropertyFile[] findAll(ClassLoader loader, final String name)
	{
		try
		{
			final Enumeration urls = loader.getResources(name);

			List files = new ArrayList();

			for (URL url : ListUtility.iterate(urls))
			{
				InputStream is = null;
				try
				{
					is = url.openStream();
					final PropertyFile file = new PropertyFile(is);

					files.add(file);
				}
				catch (IOException e)
				{
					throw new RuntimeException("Error loading properties from " +
					                           url +
					                           " for name: " +
					                           name +
					                           ": " +
					                           e.getMessage(), e);
				}
				finally
				{
					IOUtils.closeQuietly(is);
				}
			}

			return files.toArray(new PropertyFile[files.size()]);
		}
		catch (IOException e)
		{
			throw new RuntimeException("Error loading properties for name: " + name + ": " + e.getMessage(), e);
		}
	}

	// ///////////////////////
	// CONSTRUCTORS
	// ///////////////


	public PropertyFile(File fileName) throws IOException
	{
		this(fileName, true);
	}


	public PropertyFile(File fileName, boolean caseSensitive) throws IOException
	{
		setFile(fileName);
		this.caseSensitive = caseSensitive;

		load();
	}


	public PropertyFile()
	{
	}


	public PropertyFile(URL url) throws IOException
	{
		this(url.openStream());
	}


	public PropertyFile(InputStream is, boolean caseSensitive) throws IOException
	{
		this.caseSensitive = caseSensitive;

		load(is);
	}


	public PropertyFile(Reader r) throws IOException
	{
		this(r, true);
	}


	public PropertyFile(Reader r, boolean caseSensitive) throws IOException
	{
		this.caseSensitive = caseSensitive;

		load(r);
	}


	public PropertyFile(InputStream is) throws IOException
	{
		this(is, true);
	}


	public PropertyFile(Properties p)
	{
		this(p, false);
	}


	public PropertyFile(Properties p, boolean caseSensitive)
	{
		this.caseSensitive = caseSensitive;

		load(p);
	}


	public PropertyFile(final Map map)
	{
		load(map);
	}


	/**
	 * Converts the name/value pairs stored in this PropertyFile to the Java Properties propertyfile type
	 *
	 * @return a Properties representing the data (but not the comments, etc) in this file
	 */
	public Properties toProperties()
	{
		Properties p = new Properties();

		for (NameValuePair nvp : vars.values())
		{
			p.put(nvp.name, nvp.value);
		}

		return p;
	}


	/**
	 * Converts the name/value pairs stored in this PropertyFile to a Map of key/value pairs
	 *
	 * @return a Map representing the data (but not the comments, etc) in this file
	 */
	public Map toMap()
	{
		Map map = new HashMap<>(vars.size());

		for (NameValuePair nvp : vars.values())
			map.put(nvp.name, nvp.value);

		return map;
	}


	public boolean getCaseSensitive()
	{
		return caseSensitive;
	}


	public File getFile()
	{
		return f;
	}


	public void setFile(File file)
	{
		this.f = file;
	}


	public void setForceNameValueDelimiterWhitespace(boolean value)
	{
		this.forceNameValueDelimiterWhitespace = value;
	}


	public boolean getForceNameValueDelimiterWhitespace()
	{
		return this.forceNameValueDelimiterWhitespace;
	}

	// //////////////////////////////
	// READING AND WRITING
	// //////////////////////


	public void load() throws IOException
	{
		assert (f != null);

		load(f);
	}


	public void load(Map map)
	{
		for (Map.Entry entry : map.entrySet())
			set(entry.getKey(), entry.getValue());
	}


	public void load(Properties p)
	{
		try
		{
			StringWriter sw = new StringWriter();
			p.store(sw, null);

			StringBuffer sb = sw.getBuffer();

			StringReader sr = new StringReader(sb.toString());

			load(sr);
		}
		catch (IOException e)
		{
			throw new IOError(e);
		}
	}


	public void load(File file) throws IOException
	{
		assert (file != null);

		load(new FileReader(file));
	}


	public void load(InputStream is) throws IOException
	{
		load(new InputStreamReader(is));
	}


	public void save() throws IOException
	{
		save(f, null);
	}


	public void save(File f, String comment) throws IOException
	{
		assert (f != null);

		FileWriter fw = new FileWriter(f);
		try
		{
			save(comment, fw);
		}
		finally
		{
			fw.close();
		}
	}


	public void save(OutputStream os) throws IOException
	{
		save(null, new OutputStreamWriter(os));
	}


	public void save(String comment) throws IOException
	{
		save(f, comment);
	}


	public void save(String comment, OutputStream os) throws IOException
	{
		save(comment, new OutputStreamWriter(os));
	}

	// ///////////////////////////////
	// MANIPULATION
	// ////////////////////////////


	/**
	 * Merges another PropertyFile into this PropertyFile, overwriting any conflicting properties with the value from
	 * other
	 *
	 * @param other
	 * 		the other property file
	 */
	public void merge(PropertyFile other)
	{
		merge(other, null);
	}


	/**
	 * Merges another PropertyFile into this PropertyFile, using an optional merge conflict resolver
* If no merge conflict resolver is specified then the default will be that the properties from other will * overwrite the local properties * * @param other * the other property file * @param conflictResolver */ public void merge(PropertyFile other, IMergeConflictResolver conflictResolver) { hook_merge_begin(); try { for (String varName : other.keySet()) { String varVal = other._get(varName, null); if (varVal != null) { if (conflictResolver != null && this._contains(varName)) { varVal = conflictResolver.resolveConflict(varName, _get(varName, null), varVal); } _set(varName, varVal); } } } finally { hook_merge_complete(); } } /** * Merges another PropertyFile into this PropertyFile, overwriting any conflicting properties with the value from * other * * @param other */ public void merge(Properties other) { merge(other, null); } /** * Merges another PropertyFile into this PropertyFile, using an optional merge conflict resolver
* If no merge conflict resolver is specified then the default will be that the properties from other will * overwrite the local properties * * @param other * the other property file * @param conflictResolver */ public void merge(final Properties other, final IMergeConflictResolver conflictResolver) { PropertyFile otherAsPropFile = new PropertyFile(other); merge(otherAsPropFile, conflictResolver); } /** * Make this PropertyFile object read-only */ public void makeReadOnly() { this.readOnly = true; } public boolean containsKey(final String name) { return _contains(name); } public String get(final String name) { return get(name, null); } public String get(final String name, final String defaultValue) { return _get(name, defaultValue); } public InetAddress getIP(final String name, final InetAddress defaultValue) { String value = get(name); if (value != null) { value = value.trim(); if (value.isEmpty()) throw new IllegalArgumentException("Missing value for IP address field " + name + ": contents appears to be an empty string (or just whitespace?)"); try { return InetAddress.getByName(value); } catch (UnknownHostException e) { throw new RuntimeException("Error parsing IP: " + e.getMessage(), e); } } else { return defaultValue; } } public int get(final String name, final int defaultValue) { return Integer.parseInt(get(name, Integer.toString(defaultValue)).trim()); } public long get(final String name, final long defaultValue) { return Long.parseLong(get(name, Long.toString(defaultValue)).trim()); } public boolean get(final String name, final boolean defaultValue) { return Boolean.parseBoolean(get(name, Boolean.toString(defaultValue)).trim()); } public int getInteger(final String name, final int defaultValue) { return Integer.parseInt(get(name, Integer.toString(defaultValue)).trim()); } public boolean getBoolean(final String name, final boolean defaultValue) { return parseBoolean(get(name, Boolean.toString(defaultValue)).trim()); } private static boolean parseBoolean(final String value) { if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("on")) return true; else if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("no") || value.equalsIgnoreCase("off")) return false; else throw new IllegalArgumentException("Cannot parse to a boolean value: " + value); } public long getLong(final String name, final long defaultValue) { return Long.parseLong(get(name, Long.toString(defaultValue)).trim()); } public URI getURI(final String name, final String defaultValue) { String uri = get(name, null); if (uri == null && defaultValue == null) return null; else if (uri == null) uri = defaultValue; try { return new URI(uri.trim()); } catch (URISyntaxException e) { throw new IllegalArgumentException("Illegal URI: " + uri, e); } } public Class getClass(final String name, final Class defaultValue) { final String className = get(name, null); if (className == null) { return defaultValue; } else { try { return Class.forName(className.trim()); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("ClassNotFoundException caught: " + className, e); } } } /** * Get a value which is Base64 encoded * * @param name * * @return */ public byte[] getBase64(final String name) { final String encoded = get(name); if (encoded == null) return null; else return Base64.getDecoder().decode(encoded.trim()); } /** * Get a value which is Base64 encoded and has a default value * * @param name * @param defaultValue * * @return */ public byte[] getBase64(final String name, final byte[] defaultValue) { byte[] value = getBase64(name); if (value != null) return value; else return defaultValue; } /** * Get a value which has been encoded in hexidecimal; The encoding may optionally include : delimiters, but no other non-hex * characters are permitted * * @param name * * @return */ public byte[] getHex(final String name) { String value = get(name, null); if (value == null) return null; else { // Remove any : separators while (value.indexOf(':') != -1) value = value.replace(":", ""); return HexHelper.fromHex(value.trim()); } } public String set(final String name, final String value) { return _set(name, value); } public String setBase64(final String name, final byte[] value) { final String encoded = Base64.getEncoder().encodeToString(value); return set(name, encoded); } public String setHex(final String name, final byte[] value) { return set(name, HexHelper.toHex(value)); } public String set(final String name, final long value) { return set(name, Long.toString(value)); } public String set(final String name, final int value) { return set(name, Integer.toString(value)); } public void remove(final String name) { _rm(name); } public void clear() { _clear(); } // ////////////////////// // ITERATORS // //////////////// public Set keySet() { return vars.keySet(); } public Iterator keyIterator() { return vars.keySet().iterator(); } public int getLineCount() { return entries.size(); } public int getVariableCount() { return vars.size(); } // //////////////////////// // CALLBACKS // /////////////// protected boolean dirty = false; protected void hook_changed(final String name, final String oldValue, final String newValue) { dirty = true; } protected void hook_added(final String name, final String newValue) { dirty = true; } protected void hook_removed(final String name) { dirty = true; } protected void hook_cleared() { dirty = true; } protected void hook_merge_begin() { } protected void hook_merge_complete() { } protected void hook_loaded() { dirty = false; } protected void hook_saved() { dirty = false; } // //////////////////////////////////////// // LOADING AND SAVING PRIMITIVES // ///////////////////////////////// public void load(final Reader r) throws IOException { try (BufferedReader in = new BufferedReader(r)) { entries.clear(); vars.clear(); String line = in.readLine(); while (line != null) { // Parse the line if (line.length() == 0) { entries.add(new BlankLine()); } else { char c = line.charAt(0); if (c == COMMENT_CHAR) { if (!line.startsWith(COMMENT_INST)) { // Don't load instance comments entries.add(new Comment(line.substring(1))); } } else if (c == ' ') { // If it starts with a space let's call it a blank line entries.add(new BlankLine()); } else { // Name=Value pair // TODO - handle escaped characters (notably, \= and \n) final int equalsIndex = line.indexOf('='); if (equalsIndex != -1) { String name = line.substring(0, equalsIndex).trim(); String value = line.substring(equalsIndex + 1); NameValuePair nvp = new NameValuePair(name, value); if (vars.containsKey(nvp.name)) { log.warn("{load} duplicate entry '" + nvp.name + "': overwriting previous value"); } entries.add(nvp); vars.put(caseSensitive ? nvp.name : nvp.name.toLowerCase(), nvp); } else { log.error("Malformed line in property file: " + line); } } } line = in.readLine(); } hook_loaded(); } } public void save(final String comment, final Writer w) throws IOException { // Write all properties to the file try { if (comment != null) { w.append(COMMENT_INST); w.append(comment); w.append(NEWLINE); w.append(COMMENT_INST); w.append("at "); w.append(Calendar.getInstance().getTime().toString()); w.append(NEWLINE); } int sz = entries.size(); for (int i = 0; i < sz; i++) { entries.get(i).append(w, this); } hook_saved(); } finally { if (w != null) w.close(); } } // /////////////////////////////////////////////// // INTERNAL MANIPULATION PRIMITIVES // //////////////////////////////////// protected void _clear() { if (readOnly) throw new UnsupportedOperationException("Cannot modify a read-only collection"); entries.clear(); vars.clear(); hook_cleared(); } protected String _set(String name, final String value) { if (readOnly) throw new UnsupportedOperationException("Cannot modify a read-only collection"); assert (name != null); name = name.trim(); NameValuePair nvp = vars.get(caseSensitive ? name : name.toLowerCase()); if (nvp != null) { String oldValue = nvp.value; nvp.value = value; hook_changed(name, oldValue, value); return oldValue; } else { // nvp == null // Add a new nvp nvp = new NameValuePair(name, value); entries.add(nvp); vars.put(caseSensitive ? name : name.toLowerCase(), nvp); hook_added(name, value); return null; // no previous value } } protected String _get(String name, final String defaultValue) { assert (name != null); name = name.trim(); return _get_core(name, defaultValue); } protected String _get_core(final String name, final String defaultValue) { NameValuePair nvp = vars.get(caseSensitive ? name : name.toLowerCase()); if (nvp == null) { return defaultValue; } else { if (forceNameValueDelimiterWhitespace && nvp.value.length() > 0 && nvp.value.charAt(0) == ' ') return nvp.value.substring(1); else return nvp.value; } } protected boolean _contains(String name) { assert (name != null); name = name.trim(); return vars.containsKey(caseSensitive ? name : name.toLowerCase()); } protected void _rm(String name) { assert (name != null); name = name.trim(); NameValuePair nvp = vars.get(caseSensitive ? name : name.toLowerCase()); vars.remove(caseSensitive ? name : name.toLowerCase()); entries.remove(nvp); hook_removed(name); } /** * Creates a read-only union of a number of property files
* If any property file is null or the file it points to does not exist then it is ignored * * @param filenames * * @return * * @throws IOException * if an unexpected error occurs while loading a file */ public static PropertyFile readOnlyUnion(final File... filenames) throws IOException { final PropertyFile props = new PropertyFile(); for (final File f : filenames) { if (f != null && f.exists()) { props.merge(new PropertyFile(f)); } } props.readOnly = true; return props; } /** * Construct a new read-only PropertyFile which merges the contents of a number of other PropertyFile objects
* Null PropertyFiles are ignored. * * @param files * * @return */ public static PropertyFile readOnlyUnion(final PropertyFile... files) { final PropertyFile props = new PropertyFile(); for (PropertyFile file : files) { if (file != null) props.merge(file); } props.makeReadOnly(); return props; } /** * Creates a read-only version of a property file
* Fails if the file does not exist. * * @param filename * the filename to load * * @return * * @throws IOException * if the file cannot be loaded */ public static PropertyFile readOnly(final File filename) throws IOException { final PropertyFile props = new PropertyFile(filename); props.readOnly = true; return props; } //////////////////////////////////// // Property File entry types //////////////////////////////////// protected abstract class Entry { public abstract void append(Writer w, PropertyFile p) throws IOException; } protected class BlankLine extends Entry { @Override public void append(Writer w, PropertyFile p) throws IOException { w.append(NEWLINE); } } protected class Comment extends Entry { public String data; public Comment(String data) { this.data = data; } @Override public void append(Writer w, PropertyFile p) throws IOException { w.append(COMMENT_CHAR); w.append(data); w.append(NEWLINE); } } protected class NameValuePair extends Entry { private boolean unusualName; public String name; public String value; private void checkName() { if (name.contains("=")) { unusualName = true; } else { unusualName = false; } } public NameValuePair(String name, String value) { this.name = name; this.value = value; checkName(); } @Override public void append(Writer w, PropertyFile p) throws IOException { if (!unusualName) { w.append(name); } else { // TODO - escape the name log.warn("[NameValuePair] {append} name needs escapingbut this implementation doesn't escape"); w.append(name); } if (forceNameValueDelimiterWhitespace && !name.endsWith(" ")) w.append(" "); w.append("="); if (forceNameValueDelimiterWhitespace && !value.startsWith(" ")) w.append(" "); w.append(value); w.append(NEWLINE); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy