Package ca.szc.configparser

Source Code of ca.szc.configparser.Ini

/**
* Copyright 2014 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ca.szc.configparser;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ca.szc.configparser.exceptions.DuplicateOptionError;
import ca.szc.configparser.exceptions.DuplicateSectionError;
import ca.szc.configparser.exceptions.IniParserException;
import ca.szc.configparser.exceptions.InvalidLine;
import ca.szc.configparser.exceptions.MissingSectionHeaderError;
import ca.szc.configparser.exceptions.ParsingError;

/**
* A Python-compatible Java INI parser
*/
public class Ini
{
    private static final Pattern nonWhitespacePattern = Pattern.compile("\\S");

    private static final Pattern sectionPattern = Pattern.compile(templateSectionPattern());

    /**
     * Create the regular expression source for the {@link #optionPattern}
     */
    private static String templateOptionPattern(List<String> delimiters, boolean allowNoValue)
    {
        // Join delimiters with | character
        StringBuilder sb = new StringBuilder();
        String prefix = "";
        for (String delimiter : delimiters)
        {
            sb.append(prefix);
            prefix = "|";
            sb.append(Pattern.quote(delimiter));
        }
        String delimiterRegEx = sb.toString();

        // Create the option pattern
        sb = new StringBuilder();

        // Option name: any characters
        sb.append("(?<option>.*?)");

        // Zero or more whitespace
        sb.append("\\s*");

        // Open optional value group
        if (allowNoValue)
            sb.append("(?:");

        // Delimiter: one option in delimiterRegEx
        sb.append("(?<vi>");
        sb.append(delimiterRegEx);
        sb.append(")");

        // Zero or more whitespace
        sb.append("\\s*");

        // Value: all remaining characters
        sb.append("(?<value>.*)");

        // Close optional value group
        if (allowNoValue)
            sb.append(")?");

        // End of line
        sb.append("$");

        return sb.toString();
    }

    /**
     * Create the regular expression source for the {@link #sectionPattern}
     */
    private static String templateSectionPattern()
    {
        StringBuilder sb = new StringBuilder();

        // Literal [
        sb.append("\\[");

        // Header: one or more characters except literal ]
        sb.append("(?<header>[^]]+)");

        // Literal ]
        sb.append("\\]");

        return sb.toString();
    }

    private boolean allowDuplicates;
    private boolean allowNoValue;
    private List<String> commentPrefixes;
    private List<String> delimiters;
    private boolean emptyLinesInValues;
    private List<String> inlineCommentPrefixes;
    private Pattern optionPattern;

    private final Map<String, Map<String, String>> sections;

    private boolean spaceAroundDelimiters;

    /**
     * Creates an INI parser with the default configuration
     */
    public Ini()
    {
        allowDuplicates = false;

        allowNoValue = false;

        commentPrefixes = new ArrayList<>(2);
        commentPrefixes.add("#");
        commentPrefixes.add(";");

        delimiters = new ArrayList<>(2);
        delimiters.add("=");
        delimiters.add(":");

        emptyLinesInValues = true;

        inlineCommentPrefixes = new ArrayList<>(0);

        compileOptionPattern();

        sections = new LinkedHashMap<>();

        spaceAroundDelimiters = true;
    }

    /**
     * Must be called after updating attributes that {@link #templateOptionPattern(List, boolean)} depends on.
     */
    private void compileOptionPattern()
    {
        optionPattern = Pattern.compile(templateOptionPattern(delimiters, allowNoValue));
    }

    public List<String> getCommentPrefixes()
    {
        return commentPrefixes;
    }

    public List<String> getDelimiters()
    {
        return delimiters;
    }

    public List<String> getInlineCommentPrefixes()
    {
        return inlineCommentPrefixes;
    }

    public Map<String, Map<String, String>> getSections()
    {
        return sections;
    }

    public boolean isAllowDuplicates()
    {
        return allowDuplicates;
    }

    public boolean isAllowNoValue()
    {
        return allowNoValue;
    }

    public boolean isEmptyLinesInValues()
    {
        return emptyLinesInValues;
    }

    public boolean isSpaceAroundDelimiters()
    {
        return spaceAroundDelimiters;
    }

    /**
     * Parse INI text
     *
     * @param reader
     *            the {@link BufferedReader} to read the INI text from
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while reading from reader
     * @throws IniParserException
     *             When the INI text read is invalid in some way.
     */
    public Ini read(BufferedReader reader) throws IOException, IniParserException
    {
        List<ParsingError> parsingErrors = new LinkedList<>();
        Map<String, Map<String, List<String>>> unjoinedSections = new LinkedHashMap<>();
        Map<String, List<String>> currSection = null;
        String currSectionName = null;
        String currOptionName = null;
        int indentLevel = 0;
        String line = null;
        int lineNo = 0;

        while ((line = reader.readLine()) != null)
        {
            lineNo++;

            // Strip comments
            int commentStart = -1;

            // If there are any to search for, find the earliest instance of an inline comment character with a
            // whitespace character before it
            if (inlineCommentPrefixes.size() > 0)
            {
                int earliestIndex = Integer.MAX_VALUE;
                for (String prefix : inlineCommentPrefixes)
                {
                    int index = line.indexOf(prefix);
                    if (index == 0)
                    {
                        earliestIndex = 0;
                        break;
                    }
                    else if (index > 0 && Character.isWhitespace(line.charAt(index - 1)))
                        earliestIndex = Math.min(earliestIndex, index);
                }
                commentStart = earliestIndex;
            }

            if (commentStart != 0)
            {
                // Full line comment?
                for (String prefix : commentPrefixes)
                {
                    if (StringUtil.strip(line).startsWith(prefix))
                    {
                        commentStart = 0;
                        break;
                    }
                }
            }

            // Get the trimmed non-comment substring, if applicable
            String value;
            if (commentStart != -1)
                value = line.substring(0, commentStart);
            else
                value = line;
            value = StringUtil.strip(value);

            if (value.isEmpty())
            {
                if (emptyLinesInValues)
                {
                    // For ongoing option values, add an empty line, but only if there was no comment on this line
                    if (commentStart == -1 && currSection != null && currOptionName != null
                            && currSection.containsKey(currOptionName))
                    {
                        currSection.get(currOptionName).add("");
                    }
                }
                else
                {
                    // Empty line marks the end of a value
                    indentLevel = Integer.MAX_VALUE;
                }
            }
            else
            {
                // Find index of first non-whitespace character in the raw line (not value)
                Matcher nonWhitespaceMatcher = nonWhitespacePattern.matcher(line);
                int firstNonWhitespace = -1;
                if (nonWhitespaceMatcher.find())
                    firstNonWhitespace = nonWhitespaceMatcher.start();
                // This is the indent level, otherwise it is zero
                int currIndentLevel = Math.max(firstNonWhitespace, 0);

                // Continuation line
                if (currSection != null && currOptionName != null && currIndentLevel > indentLevel)
                {
                    currSection.get(currOptionName).add(value);
                }
                // Section/option header
                else
                {
                    indentLevel = currIndentLevel;

                    Matcher sectionMatcher = sectionPattern.matcher(value);
                    // Section header
                    if (sectionMatcher.matches())
                    {
                        currSectionName = sectionMatcher.group("header");
                        if (unjoinedSections.containsKey(currSectionName))
                        {
                            if (!allowDuplicates)
                            {
                                parsingErrors.add(new DuplicateSectionError(lineNo, currSectionName));
                                currSectionName = null;
                                currSection = null;
                            }
                            else
                            {
                                currSection = unjoinedSections.get(currSectionName);
                            }
                        }
                        else
                        {
                            currSection = new LinkedHashMap<>();
                            unjoinedSections.put(currSectionName, currSection);
                        }
                        // So sections can't start with a continuation line
                        currOptionName = null;
                    }
                    // No section header in file
                    else if (currSection == null)
                    {
                        parsingErrors.add(new MissingSectionHeaderError(lineNo, line));
                    }
                    // Option header
                    else
                    {
                        Matcher optionMatcher = optionPattern.matcher(value);
                        if (optionMatcher.matches())
                        {
                            currOptionName = optionMatcher.group("option");
                            String optionValue = optionMatcher.group("value");
                            if (currOptionName == null || currOptionName.length() == 0)
                            {
                                parsingErrors.add(new InvalidLine(lineNo, line));
                            }
                            currOptionName = StringUtil.rstrip(currOptionName).toLowerCase();
                            if (!allowDuplicates && unjoinedSections.get(currSectionName).containsKey(currOptionName))
                            {
                                parsingErrors.add(new DuplicateOptionError(lineNo, currSectionName, currOptionName));
                            }
                            else
                            {
                                LinkedList<String> valueList = new LinkedList<>();
                                if (optionValue != null)
                                {
                                    optionValue = StringUtil.rstrip(optionValue);
                                    valueList.add(optionValue);
                                }
                                currSection.put(currOptionName, valueList);
                            }
                        }
                        else
                        {
                            parsingErrors.add(new InvalidLine(lineNo, line));
                        }
                    }
                }
            }
        }

        if (parsingErrors.size() > 0)
            throw new IniParserException(parsingErrors);

        // Join multi line values
        for (Entry<String, Map<String, List<String>>> unjoinedSectionEntry : unjoinedSections.entrySet())
        {
            String unjoinedSectionName = unjoinedSectionEntry.getKey();
            Map<String, List<String>> unjoinedSectionOptions = unjoinedSectionEntry.getValue();

            Map<String, String> sectionOptions = new LinkedHashMap<>();

            for (Entry<String, List<String>> unjoinedOptionValueEntry : unjoinedSectionOptions.entrySet())
            {
                String unjoinedOptionName = unjoinedOptionValueEntry.getKey();
                List<String> unjoinedOptionValue = unjoinedOptionValueEntry.getValue();

                String optionValue;

                if (unjoinedOptionValue.size() > 0)
                {
                    // Remove trailing whitespace lines
                    ListIterator<String> iter = unjoinedOptionValue.listIterator(unjoinedOptionValue.size());
                    while (iter.hasPrevious())
                        if (StringUtil.strip(iter.previous()).isEmpty())
                            iter.remove();
                        else
                            break;

                    // Join lines with newline character
                    StringBuilder optionValueBuilder = new StringBuilder();
                    String prefix = "";
                    for (String valueLine : unjoinedOptionValue)
                    {
                        optionValueBuilder.append(prefix);
                        prefix = "\n";
                        optionValueBuilder.append(valueLine);
                    }
                    optionValue = optionValueBuilder.toString();
                }
                else
                {
                    optionValue = null;
                }

                sectionOptions.put(unjoinedOptionName, optionValue);
            }

            sections.put(unjoinedSectionName, sectionOptions);
        }
        return this;
    }

    /**
     * Parse an INI file with the default {@link Charset}
     *
     * @param iniPath
     *            The {@link Path} pointing the the INI file to read
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while reading from reader
     * @throws IniParserException
     *             When the INI text read is invalid in some way.
     * @see StandardCharsets#UTF_8
     */
    public Ini read(Path iniPath) throws IOException, IniParserException
    {
        read(iniPath, StandardCharsets.UTF_8);
        return this;
    }

    /**
     * Parse an INI file with a specified {@link Charset}
     *
     * @param iniPath
     *            The {@link Path} pointing the the INI file to read
     * @param charset
     *            The {@link Charset} to use when reading the file
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while reading from reader
     * @throws IniParserException
     *             When the INI text read is invalid in some way.
     * @see StandardCharsets
     */
    public Ini read(Path iniPath, Charset charset) throws IOException, IniParserException
    {
        try (BufferedReader reader = Files.newBufferedReader(iniPath, charset))
        {
            read(reader);
        }
        return this;
    }

    /**
     * Set if duplicate sections and options will be accepted, or throw a {@link IniParserException} at
     * {@link #read(BufferedReader)} time.
     *
     * @param allowDuplicates
     *            duplicates are accepted iff true
     * @return this Ini
     */
    public Ini setAllowDuplicates(boolean allowDuplicates)
    {
        this.allowDuplicates = allowDuplicates;
        return this;
    }

    /**
     * Set if option keys with no values will be accepted, or throw a {@link IniParserException} at
     * {@link #read(BufferedReader)} time.
     *
     * @param allowNoValue
     *            no value options are accepted iff true
     * @return this Ini
     */
    public Ini setAllowNoValue(boolean allowNoValue)
    {
        this.allowNoValue = allowNoValue;

        compileOptionPattern();
        return this;
    }

    /**
     * Set which {@link String}s should start full comment lines.
     *
     * @param commentPrefixes
     *            the full line comment prefixes
     * @return this Ini
     */
    public Ini setCommentPrefixes(List<String> commentPrefixes)
    {
        this.commentPrefixes = commentPrefixes;
        return this;
    }

    /**
     * Set which {@link String}s should divide option keys from values
     *
     * @param delimiters
     *            the key/value delimiters
     * @return this Ini
     */
    public Ini setDelimiters(List<String> delimiters)
    {
        this.delimiters = delimiters;

        compileOptionPattern();
        return this;
    }

    /**
     * Set if empty lines should be considered to be a part of the latest option's value or not. Can cause
     * {@link IniParserException} to be thrown in some cases when false
     *
     * @param emptyLinesInValues
     *            empty lines are added to option values iff true
     * @return this Ini
     */
    public Ini setEmptyLinesInValues(boolean emptyLinesInValues)
    {
        this.emptyLinesInValues = emptyLinesInValues;
        return this;
    }

    /**
     * Set which {@link String}s should divide data from comments on non-blank lines
     *
     * @param inlineCommentPrefixes
     *            the partial line comment prefixes
     * @return this Ini
     */
    public Ini setInlineCommentPrefixes(List<String> inlineCommentPrefixes)
    {
        this.inlineCommentPrefixes = inlineCommentPrefixes;
        return this;
    }

    /**
     * Set if spaces should be placed around option key/value delimiters when writing
     *
     * @param spaceAroundDelimiters
     *            place spaces around key/value delimiters iff true
     * @return this Ini
     */
    public Ini setSpaceAroundDelimiters(boolean spaceAroundDelimiters)
    {
        this.spaceAroundDelimiters = spaceAroundDelimiters;
        return this;
    }

    /**
     * Write INI formatted text
     *
     * @param writer
     *            the {@link BufferedWriter} to write the INI text to
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while writing to the writer
     */
    public Ini write(BufferedWriter writer) throws IOException
    {
        // Create option/value delimiter string using first in configured delimiters
        StringBuilder sb = new StringBuilder();
        if (spaceAroundDelimiters)
            sb.append(" ");
        sb.append(delimiters.get(0));
        if (spaceAroundDelimiters)
            sb.append(" ");
        String delimiter = sb.toString();

        // Write out each section
        for (Entry<String, Map<String, String>> sectionEntry : sections.entrySet())
        {
            String sectionName = sectionEntry.getKey();
            Map<String, String> sectionOptions = sectionEntry.getValue();

            // Section Header (ex: [mysection])
            writer.append("[");
            writer.append(sectionName);
            writer.append("]");
            writer.newLine();

            // Write out each option/value pair
            for (Entry<String, String> optionEntry : sectionOptions.entrySet())
            {
                String option = optionEntry.getKey();
                String value = optionEntry.getValue();

                // Option Header (ex: key = value)
                writer.append(option);
                if (value == null && allowNoValue)
                {
                    // Append nothing after the key
                }
                else
                {
                    writer.append(delimiter);
                    if (value != null)
                    {
                        writer.append(value.replace("\n", System.lineSeparator() + "\t"));
                    }
                    else
                    {
                        writer.append(value);
                    }
                }
                writer.newLine();
            }

            writer.newLine();
        }
        return this;
    }

    /**
     * Write an INI file with the default {@link Charset}
     *
     * @param iniPath
     *            The {@link Path} pointing the the INI file to write
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while writing to the writer
     * @see StandardCharsets#UTF_8
     */
    public Ini write(Path iniPath) throws IOException
    {
        write(iniPath, StandardCharsets.UTF_8);
        return this;
    }

    /**
     * Write an INI file with a specified {@link Charset}
     *
     * @param iniPath
     *            The {@link Path} pointing the the INI file to write
     * @param charset
     *            The {@link Charset} to use when writing the file
     * @return this Ini
     * @throws IOException
     *             When errors are encountered while writing to the writer
     * @see StandardCharsets
     */
    public Ini write(Path iniPath, Charset charset) throws IOException
    {
        try (BufferedWriter writer = Files.newBufferedWriter(iniPath, charset))
        {
            write(writer);
        }
        return this;
    }
}
TOP

Related Classes of ca.szc.configparser.Ini

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.