com.opengamma.collect.io.IniFile.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.collect.io.IniFile.java

Source

/**
 * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
 * 
 * Please see distribution for license.
 */
package com.opengamma.collect.io;

import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.io.CharSource;
import com.opengamma.collect.ArgChecker;

/**
 * An INI file.
 * <p>
 * Represents an INI file together with the ability to parse it from a {@link CharSource}.
 * <p>
 * The INI file format used here is deliberately simple.
 * There are two elements - key-value pairs and sections.
 * <p>
 * The basic element is a key-value pair.
 * The key is separated from the value using the '=' symbol.
 * Duplicate keys are allowed.
 * For example 'key = value'.
 * <p>
 * All properties are grouped into named sections.
 * The section name occurs on a line by itself surrounded by square brackets.
 * Duplicate section names are not allowed.
 * For example '[section]'.
 * <p>
 * Keys, values and section names are trimmed.
 * Blank lines are ignored.
 * Whole line comments begin with hash '#' or semicolon ';'.
 * No escape format is available.
 * Lookup is case sensitive.
 * <p>
 * This example explains the format:
 * <pre>
 *  # line comment
 *  [foo]
 *  key = value
 * 
 *  [bar]
 *  key = value
 *  month = January
 * </pre>
 * <p>
 * The aim of this class is to parse the basic format.
 * Interpolation of variables is not supported.
 */
public final class IniFile {

    /**
     * Section name used for chaining.
     */
    private static final String CHAIN_SECTION = "chain";
    /**
     * Property name used for priority.
     */
    private static final String PRIORITY = "priority";
    /**
     * Property name used for chaining.
     */
    private static final String CHAIN_NEXT = "chainNextFile";
    /**
     * Property name used for removing sections.
     */
    private static final String CHAIN_REMOVE = "chainRemoveSections";

    /**
     * The INI sections.
     */
    private final ImmutableMap<String, PropertySet> sectionMap;

    //-------------------------------------------------------------------------
    /**
     * Parses the specified source as an INI file.
     * <p>
     * This parses the specified character source expecting an INI file format.
     * The resulting instance can be queried for each section in the file.
     * 
     * @param source  the INI file resource
     * @return the INI file
     * @throws UncheckedIOException if an IO error occurs
     * @throws IllegalArgumentException if the configuration is invalid
     */
    public static IniFile of(CharSource source) {
        ArgChecker.notNull(source, "source");
        try {
            Map<String, Multimap<String, String>> parsedIni = parse(source);
            ImmutableMap.Builder<String, PropertySet> builder = ImmutableMap.builder();
            parsedIni.forEach((sectionName, sectionData) -> builder.put(sectionName, PropertySet.of(sectionData)));
            return new IniFile(builder.build());
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    //-------------------------------------------------------------------------
    /**
     * Returns a single INI file that is the chained combination of the inputs.
     * <p>
     * The result of this method is formed by chaining all the specified files together.
     * The files are combined using a simple algorithm defined in the '[chain]' section.
     * Firstly, the 'priority' value is used to sort the files, higher numbers have higher priority
     * All entries in the highest priority file are used
     * <p>
     * Once data from the highest priority file is included, the 'chainNextFile' property is examined.
     * If 'chainNextFile' is 'true', then the next file in the chain is considered.
     * The 'chainRemoveSections' property can be used to ignore specific sections from the files lower in the chain.
     * The chain process continues until the 'chainNextFile' is 'false', or all files have been combined.
     * 
     * @param sources  the INI file sources to read
     * @return the combined chained INI file
     * @throws UncheckedIOException if an IO error occurs
     * @throws IllegalArgumentException if the configuration is invalid
     */
    public static IniFile ofChained(Stream<CharSource> sources) {
        ArgChecker.notNull(sources, "sources");
        List<IniFile> files = sources.map(IniFile::of).sorted(IniFile::compareByReversePriority).collect(toList());
        // combine files, based on chain flag
        Map<String, PropertySet> builder = new LinkedHashMap<>();
        for (IniFile file : files) {
            // remove everything from lower priority files if not chaining
            if (Boolean.parseBoolean(file.getSection(CHAIN_SECTION).getValue(CHAIN_NEXT)) == false) {
                builder.clear();
            } else {
                // remove sections from lower priority files
                builder.keySet().removeAll(file.getSection(CHAIN_SECTION).getValueList(CHAIN_REMOVE));
            }
            // add entries, replacing existing data
            for (String sectionName : file.asMap().keySet()) {
                if (!sectionName.equals(CHAIN_SECTION)) {
                    builder.merge(sectionName, file.getSection(sectionName), PropertySet::combinedWith);
                }
            }
        }
        return new IniFile(ImmutableMap.copyOf(builder));
    }

    // sort by priority, lowest first
    private static int compareByReversePriority(IniFile a, IniFile b) {
        int priority1 = Integer.parseInt(a.getSection(CHAIN_SECTION).getValue(PRIORITY));
        int priority2 = Integer.parseInt(b.getSection(CHAIN_SECTION).getValue(PRIORITY));
        return Integer.compare(priority1, priority2);
    }

    //-------------------------------------------------------------------------
    // parses the INI file format
    private static Map<String, Multimap<String, String>> parse(CharSource source) throws IOException {
        ImmutableList<String> lines = source.readLines();
        Map<String, Multimap<String, String>> ini = new LinkedHashMap<>();
        Multimap<String, String> currentSection = null;
        int lineNum = 0;
        for (String line : lines) {
            lineNum++;
            line = line.trim();
            if (line.length() == 0 || line.startsWith("#") || line.startsWith(";")) {
                continue;
            }
            if (line.startsWith("[") && line.endsWith("]")) {
                String sectionName = line.substring(1, line.length() - 1).trim();
                if (ini.containsKey(sectionName)) {
                    throw new IllegalArgumentException(
                            "Invalid INI file, duplicate section not allowed, line " + lineNum);
                }
                currentSection = ArrayListMultimap.create();
                ini.put(sectionName, currentSection);

            } else if (currentSection == null) {
                throw new IllegalArgumentException(
                        "Invalid INI file, properties must be within a [section], line " + lineNum);

            } else {
                int equalsPosition = line.indexOf('=');
                if (equalsPosition < 0) {
                    throw new IllegalArgumentException(
                            "Invalid INI file, expected key=value property, line " + lineNum);
                }
                String key = line.substring(0, equalsPosition).trim();
                String value = line.substring(equalsPosition + 1).trim();
                if (key.length() == 0) {
                    throw new IllegalArgumentException("Invalid INI file, empty key, line " + lineNum);
                }
                currentSection.put(key, value);
            }
        }
        return ini;
    }

    //-------------------------------------------------------------------------
    /**
     * Restricted constructor.
     * 
     * @param sectionMap  the sections
     */
    private IniFile(ImmutableMap<String, PropertySet> sectionMap) {
        this.sectionMap = sectionMap;
    }

    //-------------------------------------------------------------------------
    /**
     * Returns the set of keys of this INI file.
     * 
     * @return the set of keys
     */
    public ImmutableSet<String> keys() {
        return sectionMap.keySet();
    }

    /**
     * Returns the INI file as a map.
     * <p>
     * The iteration order of the map matches that of the original file.
     * 
     * @return the INI file sections
     */
    public ImmutableMap<String, PropertySet> asMap() {
        return sectionMap;
    }

    //-------------------------------------------------------------------------
    /**
     * Checks if this INI file contains the specified section.
     * 
     * @param name  the section name
     * @return true if the section exists
     */
    public boolean contains(String name) {
        ArgChecker.notNull(name, "name");
        return sectionMap.containsKey(name);
    }

    /**
     * Gets a single section of this INI file.
     * <p>
     * This returns the section associated with the specified name.
     * If the section does not exist an exception is thrown.
     * 
     * @param name  the section name
     * @return the INI file section
     * @throws IllegalArgumentException if the section does not exist
     */
    public PropertySet getSection(String name) {
        ArgChecker.notNull(name, "name");
        if (contains(name) == false) {
            throw new IllegalArgumentException("Unknown INI file section: " + name);
        }
        return sectionMap.get(name);
    }

    //-------------------------------------------------------------------------
    /**
     * Checks if this INI file equals another.
     * <p>
     * The comparison checks the content.
     * 
     * @param obj  the other file, null returns false
     * @return true if equal
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof IniFile) {
            return sectionMap.equals(((IniFile) obj).sectionMap);
        }
        return false;
    }

    /**
     * Returns a suitable hash code for the INI file.
     * 
     * @return the hash code
     */
    @Override
    public int hashCode() {
        return sectionMap.hashCode();
    }

    /**
     * Returns a string describing the INI file.
     * 
     * @return the descriptive string
     */
    @Override
    public String toString() {
        return sectionMap.toString();
    }

}