Open Source Repository

Home /jodd/jodd-3.3.2 | Repository Home



jodd/props/Props.java
// Copyright (c) 2003-2012, Jodd Team (jodd.org). All Rights Reserved.

package jodd.props;

import jodd.io.FastCharArrayWriter;
import jodd.io.FileNameUtil;
import jodd.io.FileUtil;
import jodd.io.StreamUtil;
import jodd.typeconverter.Convert;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;


/**
 * Super properties: fast, configurable, supports (ini) sections, profiles.
 *
 <p>
 * Basic parsing rules:
 <li> By default, props files are UTF8 encoded.
 <li> Leading and trailing spaces will be trimmed from section names and property names.
 <li> Leading and/or trailing spaces may be trimmed from property values.
 <li> You can use either equal sign (=) or colon (:) to assign property values
 <li> Comments begin with either a semicolon (;), or a sharp sign (#) and extend to the end of line. It doesn't have to be the first character.
 <li> A backslash (\) escapes the next character (e.g., \# is a literal #, \\ is a literal \).
 <li> If the last character of a line is backslash (\), the value is continued on the next line with new line character included.
 <li> \\uXXXX is encoded as character
 <li> \t, \r and \f are encoded as characters
 *
 <p>
 * Sections rules:
 <li> Section names are enclosed between [ and ].
 <li> Properties following a section header belong to that section. Section name is added as a prefix to section properties.
 <li> Section ends with empty section definition [] or with new section start
 *
 <p>
 * Profiles rules:
 <li> Profile names are enclosed between &lt; and &gt; in property key.
 <li> Each property key may contain zero, one or more profile definitions.
 *
 <p>
 * Macro rules:
 <li> Profile values may contain references to other properties using ${ and }
 <li> Inner references are supported
 <li> References are resolved first in the profile context and then in the base props context.
 */
public class Props implements Cloneable {

  private static final String DEFAULT_PROFILES_PROP = "@profiles";

  protected final PropsParser parser;                  // parser
  protected final PropsData data;                    // data

  protected String activeProfilesProp = DEFAULT_PROFILES_PROP;    // active profiles property
  protected String[] activeProfiles;

  /**
   * Creates new props.
   */
  public Props() {
    this(new PropsParser());
  }

  protected Props(PropsParser parser) {
    this.parser = parser;
    this.data = parser.getPropsData();
  }

  /**
   * Clones props by creating new instance and copying current configuration.
   */
  @Override
  protected Props clone() {
    PropsParser parser = this.parser.clone();
    Props p = new Props(parser);

    p.activeProfilesProp = activeProfilesProp;

    return p;
  }

  // ---------------------------------------------------------------- configuration

  /**
   * Specifies the new line string when EOL is escaped.
   * Default value is an empty string.
   */
  public void setEscapeNewLineValue(String escapeNewLineValue) {
    parser.escapeNewLineValue = escapeNewLineValue;
  }

  /**
   * Specifies should the values be trimmed from the left.
   * Default is <code>true</code>.
   */
  public void setValueTrimLeft(boolean valueTrimLeft) {
    parser.valueTrimLeft = valueTrimLeft;
  }

  /**
   * Specifies should the values be trimmed from the right.
   * Default is <code>true</code>.
   */
  public void setValueTrimRight(boolean valueTrimRight) {
    parser.valueTrimRight = valueTrimRight;
  }

  /**
   * Defines if the prefix whitespaces should be ignored when value is split into the lines.
   */
  public void setIgnorePrefixWhitespacesOnNewLine(boolean ignorePrefixWhitespacesOnNewLine) {
    parser.ignorePrefixWhitespacesOnNewLine = ignorePrefixWhitespacesOnNewLine;
  }

  /**
   * Skips empty properties.
   */
  public void setSkipEmptyProps(boolean skipEmptyProps) {
    parser.skipEmptyProps = skipEmptyProps;
  }

  /**
   * Appends duplicate props.
   */
  public void setAppendDuplicateProps(boolean appendDuplicateProps) {
    data.appendDuplicateProps = appendDuplicateProps;
  }

  // ---------------------------------------------------------------- load

  /**
   * Parses input string and loads provided properties map.
   */
  protected synchronized void parse(String data) {
    initialized = false;
    parser.parse(data);
  }

  /**
   * Loads props from the string.
   */
  public void load(String data) {
    parse(data);
  }

  /**
   * Loads props from the file. Assumes UTF8 encoding unless
   * the file ends with '.properties', than it uses ISO 8859-1.
   */
  public void load(File filethrows IOException {
    String extension = FileNameUtil.getExtension(file.getAbsolutePath());
    String data;
    if (extension.equalsIgnoreCase("properties")) {
      data = FileUtil.readString(file, StringPool.ISO_8859_1);
    else {
      data = FileUtil.readString(file);
    }
    parse(data);
  }

  /**
   * Loads properties from the file in provided encoding.
   */
  public void load(File file, String encodingthrows IOException {
    parse(FileUtil.readString(file, encoding));
  }

  /**
   * Loads properties from input stream. Stream is not closed at the end.
   */
  public void load(InputStream inthrows IOException {
    Writer out = new FastCharArrayWriter();
    StreamUtil.copy(in, out);
    parse(out.toString());
  }

  /**
   * Loads properties from input stream and provided encoding.
   * Stream is not closed at the end.
   */
  public void load(InputStream in, String encodingthrows IOException {
    Writer out = new FastCharArrayWriter();
    StreamUtil.copy(in, out, encoding);
    parse(out.toString());
  }

  /**
   * Loads base properties from the provided java properties.
   * Null values are ignored.
   */
  @SuppressWarnings({"unchecked"})
  public void load(Properties p) {
    Enumeration<String> names = (Enumeration<String>p.propertyNames();
    while (names.hasMoreElements()) {
      String name = names.nextElement();
      String value = p.getProperty(name);
      if (value == null) {
        continue;
      }
      data.putBaseProperty(name, p.getProperty(name));
    }
  }

  /**
   * Loads base properties from java properties using provided prefix.
   * Null values are ignored.
   */
  @SuppressWarnings("unchecked")
  public void load(Properties p, String prefix) {
    Enumeration<String> names = (Enumeration<String>p.propertyNames();
    prefix += '.';
    while (names.hasMoreElements()) {
      String name = names.nextElement();
      String value = p.getProperty(name);
      if (value == null) {
        continue;
      }
      data.putBaseProperty(prefix + name, value);
    }
  }

  /**
   * Loads base properties from java Map using provided prefix.
   * Null values are ignored.
   */
  @SuppressWarnings("unchecked")
  public void load(Map<?, ?> map, String prefix) {
    prefix += '.';
    for (Map.Entry entry : map.entrySet()) {
      String name = entry.getKey().toString();
      Object value = entry.getValue();
      if (value == null) {
        continue;
      }
      data.putBaseProperty(prefix + name, value.toString());
    }
  }

  /**
   * Loads system properties with given prefix.
   * If prefix is <code>null</code> it will not be ignored.
   */
  public void loadSystemProperties(String prefix) {
    Properties environmentProperties = System.getProperties();
    load(environmentProperties, prefix);
  }

  /**
   * Loads environment properties with given prefix.
   * If prefix is <code>null</code> it will not be used.
   */
  public void loadEnvironment(String prefix) {
    Map<String, String> environmentMap = System.getenv();
    load(environmentMap, prefix);
  }

  // ---------------------------------------------------------------- props

  /**
   * Counts the total number of properties, including all profiles.
   * This operation performs calculation each time and it might be
   * more time consuming then expected.
   */
  public int countTotalProperties() {
    return data.countBaseProperties() + data.countProfileProperties();
  }

  /**
   * Returns <code>string</code> value of base property.
   * Returns <code>null</code> if property doesn't exist.
   */
  @SuppressWarnings({"NullArgumentToVariableArgMethod"})
  public String getBaseValue(String key) {
    return getValue(key, null);
  }

  /**
   * Returns value of property, using active profiles.
   */
  public String getValue(String key) {
    return getValue(key, activeProfiles);
  }

  /**
   * Returns <code>string</code> value of given profiles. If key is not
   * found under listed profiles, base properties will be searched.
   * Returns <code>null</code> if property doesn't exist.
   */
  public String getValue(String key, String... profiles) {
    initialize();
    return data.lookupValue(key, profiles);
  }


  // ---------------------------------------------------------------- put

  /**
   * Sets default value.
   */
  public void setValue(String key, String value) {
    setValue(key, value, null);
  }

  /**
   * Sets value on some profile.
   */
  public void setValue(String key, String value, String profile) {
    if (profile == null) {
      data.putBaseProperty(key, value);
    else {
      data.putProfileProperty(key, value, profile);
    }
    initialized = false;
  }

  // ---------------------------------------------------------------- extract

  /**
   * Extract base props to properties.
   */
  @SuppressWarnings({"NullArgumentToVariableArgMethod"})
  public Properties extractBaseProperties() {
    return extractProperties(null);
  }

  /**
   * Extracts properties belonging to active profiles.
s   */
  public Properties extractProperties() {
    return extractProperties(activeProfiles);
  }

  /**
   * Extract props to properties.
   */
  public Properties extractProperties(String... profiles) {
    initialize();
    return data.extractProperties(profiles);
  }

  // ---------------------------------------------------------------- initialize

  protected volatile boolean initialized;

  /**
   * Initializes props by replacing macros in values with the lookup values.
   */
  protected void initialize() {
    if (initialized == false) {
      synchronized (this) {
        if (initialized == false) {

          data.resolveMacros();
          resolveActiveProfiles();

          initialized = true;
        }
      }
    }
  }

  /**
   * Resolves active profiles from property.
   */
  protected void resolveActiveProfiles() {
    if (activeProfilesProp == null) {
      activeProfiles = null;
      return;
    }
    PropsValue pv = data.getBaseProperty(activeProfilesProp);
    if (pv == null) {
      activeProfiles = null;
      return;
    }
    String value = pv.getValue();
    if (StringUtil.isBlank(value)) {
      activeProfiles = null;
      return;
    }

    activeProfiles = Convert.toStringArray(value);
    StringUtil.trimAll(activeProfiles);
  }

}