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

com.sshtools.jini.INIReader Maven / Gradle / Ivy

The newest version!
/**
 * Copyright © 2023 JAdaptive Limited ([email protected])
 *
 * 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 com.sshtools.jini;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.TreeMap;

import com.sshtools.jini.INI.AbstractIO;
import com.sshtools.jini.INI.AbstractIOBuilder;
import com.sshtools.jini.INI.EscapeMode;
import com.sshtools.jini.INI.DefaultINI;
import com.sshtools.jini.INI.LinkedCaseInsensitiveMap;
import com.sshtools.jini.INI.MissingVariableMode;
import com.sshtools.jini.INI.Section;
import com.sshtools.jini.INI.SectionImpl;
import com.sshtools.jini.Interpolation.Interpolator;

/**
 * An {@INIReader} can read text (from files, streams or strings) in the INI
 * format to produce an {@link INI} object instance.
 * 

* This class should not be directly created, instead use * {@link INIReader.Builder} and configure it accordingly before calling * {@link INIReader.Builder#build()}. * */ public final class INIReader extends AbstractIO { /** * Used to configure how to behave when duplicate value or section keys are * encountered. * * @see Builder#withDuplicateKeysAction(DuplicateAction) * @see Builder#withDuplicateSectionAction(DuplicateAction) */ public enum DuplicateAction { /** * When a duplicate value key or section key is encountered, a * {@link ParseException} will be thrown. With this mode no keys can have * multiple values. */ ABORT, /** * When a duplicate value key or section key in encountered, the duplicates * value will be entirely ignore. With this mode no keys can have multiple * values. */ IGNORE, /** * When a duplicate value key or section key is encountered, the previous value * is entirely replaced. With this mode no keys can have multiple values. */ REPLACE, /** * When a duplicate value key is encountered, the previous values are merged * with the new values. */ MERGE, /** * When a duplicate value key is encountered, the new values are appended to the * existing values */ APPEND } /** * Used to configure how to behave when parsing or writing multiple value or * section keys. * * */ public enum MultiValueMode { /** * Multiple values are expressed as value keys repeating in the content. */ REPEATED_KEY, /** * Multiple values are not allowed at all, a value will always be treated * as a single value. */ OFF, /** * Multiple values are expressed as a single key, with it's string value being * made up of (comma by default) separated values. */ SEPARATED } /** * Creates {@link INIReader} instances. Builders may be re-used, once {@link #build()} * is used, any changes to the builder will not affect the created instance. */ public static class Builder extends AbstractIOBuilder { private boolean globalSection = true; private boolean caseSensitiveKeys = false; private boolean caseSensitiveSections = false; private boolean preserveOrder = true; private boolean nestedSections = true; private boolean parseExceptions = true; private boolean comments = true; private boolean inlineComments = true; private DuplicateAction duplicateKeysAction = DuplicateAction.REPLACE; private DuplicateAction duplicateSectionAction = DuplicateAction.REPLACE; private char[] quoteCharacters = new char[] { '"', '\'' }; private Optional interpolator = Optional.empty(); private Optional variablePattern = Optional.empty(); private MissingVariableMode missingVariableMode = MissingVariableMode.ERROR; /** * Configure how to react when a variable is encountered that does not exist. * * @param missingVariableMode missing variable mode * @return this for chaining */ public final Builder withMissingVariableMode(MissingVariableMode missingVariableMode) { this.missingVariableMode = missingVariableMode; return this; } /** * Configure the regular expression pattern to use to extract interpolatable variables. * * @param variablePattern variable pattern * @return this for chaining */ public final Builder withVariablePattern(String variablePattern) { this.variablePattern = Optional.of(variablePattern); return this; } /** * Configure an "Interpolator", that processes string values before they * are returned, replacing string variable patterns. See {@link Interpolation} * for some default interpolators. * * @param interpolator interpolator * @return this for chaining */ public final Builder withInterpolator(Interpolator interpolator) { this.interpolator = Optional.of(interpolator); return this; } /** * Configure either the reader to not expect any type of string quotes. * * @return this for chaining */ public final Builder withoutStringQuoting() { return withQuoteCharacters(); } /** * Configure the reader to expect quoted strings using the provided quote * characters. * * @return this for chaining */ public final Builder withQuoteCharacters(char... quoteCharacters) { this.quoteCharacters = quoteCharacters; return this; } /** * Do not throw {@link ParseException} when invalid syntax is found when parsing * an INI document. In general the line will just be ignored and treated as if * it were a comment. * * @return this for chaining */ public final Builder withoutParseExceptions() { return withParseExceptions(false); } /** * Configure whether to throw {@link ParseException} when invalid syntax is * found when parsing an INI document. In general the line will just be ignored * and treated as if it were a comment. * * @param parseExceptions throw parse exceptions on invalid syntax * @return this for chaining */ public final Builder withParseExceptions(boolean parseExceptions) { this.parseExceptions = parseExceptions; return this; } /** * Do not allow inline comments. Comments will be treated as part of the value. * * @return this for chaining */ public final Builder withoutInlineComments() { return withInlineComments(false); } /** * Configure whether to allow inline comments. When false, comments * will be treated as part of the value. * * @param inlineComments allow inline comments * @return this for chaining */ public final Builder withInlineComments(boolean inlineComments) { this.inlineComments = inlineComments; return this; } /** * Configure to not allow comments. Anything that starts with * {@link #withCommentCharacter(char)} will be treated as a valid key. * * @return this for chaining */ public final Builder withoutComments() { return withComments(false); } /** * Configure whether to allow comments. When false, anything that * starts with {@link #withCommentCharacter(char)} will be treated as a valid * key. When true anything that starts with * {@link #withCommentCharacter(char)} will be ignored. * * @param comments allow comments * @return this for chaining */ public final Builder withComments(boolean comments) { this.comments = comments; return this; } /** * Configure to not allow any global properties, i.e. all values must be in a * section. * * @return this for chaining */ public final Builder withoutGlobalProperties() { return withGlobalSection(false); } /** * Configure whether to allow any global properties, i.e. all values must be in * a section. * * @param globalSection allow properties global in section * @return this for chaining */ public final Builder withGlobalSection(boolean globalSection) { this.globalSection = globalSection; return this; } /** * Value keys are by default case insensitive. Use this method to make value * keys case sensitive. * * @return this for chaining */ public final Builder withCaseSensitiveKeys() { return withCaseSensitiveKeys(true); } /** * Configure whether value keys are case sensitive. Value keys are by default * case insensitive. * * @param caseSensitiveKeys case sensitive keys * @return this for chaining */ public final Builder withCaseSensitiveKeys(boolean caseSensitiveKeys) { this.caseSensitiveKeys = caseSensitiveKeys; return this; } /** * Sections keys are by default case insensitive. Use this method to make * section keys case sensitive. * * @return this for chaining */ public final Builder withCaseSensitiveSections() { return withCaseSensitiveSections(true); } /** * Configure whether section keys are case sensitive. Sections keys are by * default case insensitive. * * @param caseSensitiveSections case sensitive sections * @return this for chaining */ public final Builder withCaseSensitiveSections(boolean caseSensitiveSections) { this.caseSensitiveSections = caseSensitiveSections; return this; } /** * Configure to not allow nested sections. Any section key encountered that has * {@link #withSectionPathSeparator(char)} (by default .), will be * treated as literally that key. * * @return this for chaining */ public final Builder withoutNestedSections() { return withNestedSections(false); } /** * Configure whether to allow nested sections. When false, any * section key encountered that has {@link #withSectionPathSeparator(char)} (by * default .), will be treated as literally that key. When * true the key will be split up based on the separator and that is * used as the path to section. * * @paran nestedSections nested sections * @return this for chaining */ public final Builder withNestedSections(boolean nestedSections) { this.nestedSections = nestedSections; return this; } /** * Configure to not preserve the order of sections and values as they are * inserted. Upon writing, keys and sections will be in an indeterminate order. * * @return this for chaining */ public final Builder withoutPreserveOrder() { return withPreserveOrder(false); } /** * Configure whether to preserve the order of sections and values as they are * inserted. When false, upon writing keys and sections will be in * an indeterminate order. When true, upon writing keys and * sections will be ordered according to the same rules as * {@link String#compareTo(String)}, i.e. alphabetical. * * @param preserveOrder preserve order * @return this for chaining */ public final Builder withPreserveOrder(boolean preserveOrder) { this.preserveOrder = preserveOrder; return this; } /** * Configure how to behave when duplicate value keys are encountered. * * @param duplicateKeysAction action * @return this for chaining */ public final Builder withDuplicateKeysAction(DuplicateAction duplicateKeysAction) { this.duplicateKeysAction = duplicateKeysAction; return this; } /** * Configure how to behave when duplicate section keys are encountered. * * @param duplicateKeysAction action * @return this for chaining */ public final Builder withDuplicateSectionAction(DuplicateAction duplicateSectionAction) { this.duplicateSectionAction = duplicateSectionAction; return this; } /** * Build a new {@link INIReader}. * * @return reader */ public final INIReader build() { return new INIReader(this); } } private final boolean globalSection; private final boolean caseSensitiveKeys; private final boolean caseSensitiveSections; private final boolean nestedSections; private final boolean preserveOrder; private final boolean comments; private final DuplicateAction duplicateKeysAction; private final DuplicateAction duplicateSectionAction; private final boolean inlineComments; private final boolean parseExceptions; private final char[] quoteCharacters; private final Optional interpolator; private final Optional variablePattern; private final MissingVariableMode missingVariableMode; private INIReader(Builder builder) { super(builder); this.interpolator = builder.interpolator; this.comments = builder.comments; this.globalSection = builder.globalSection; this.caseSensitiveKeys = builder.caseSensitiveKeys; this.caseSensitiveSections = builder.caseSensitiveSections; this.nestedSections = builder.nestedSections; this.preserveOrder = builder.preserveOrder; this.duplicateKeysAction = builder.duplicateKeysAction; this.duplicateSectionAction = builder.duplicateSectionAction; this.inlineComments = builder.inlineComments; this.parseExceptions = builder.parseExceptions; this.quoteCharacters = builder.quoteCharacters; this.variablePattern = builder.variablePattern; this.missingVariableMode = builder.missingVariableMode; } /** * Create an {@link INI} document instance by reading a file with content in INI * format. * * @param file file containing INI * @return document * @throws IOException on I/O error * @throws ParseException on parsing error */ public INI read(Path file) throws IOException, ParseException { try (var rdr = Files.newBufferedReader(file)) { return read(rdr); } } /** * Create an {@link INI} document instance from a string in INI format. * * @param content string of INI content * @return document * @throws IOException on I/O error * @throws ParseException on parsing error */ public INI read(String content) throws IOException, ParseException { try (var rdr = new StringReader(content)) { return read(rdr); } } /** * Create an {@link INI} document instance by reading a stream with content in * INI format. The current default character set encoding will be used. * * @param input stream of INI content * @return document * @throws IOException on I/O error * @throws ParseException on parsing error */ public INI read(InputStream input) throws IOException, ParseException { return read(new InputStreamReader(input)); } /** * Create an {@link INI} document instance by reading a stream with content in * INI format. * * @param input stream of INI content * @param charset character set * @return document * @throws IOException on I/O error * @throws ParseException on parsing error */ public INI read(InputStream input, String charset) throws IOException, ParseException { return read(new InputStreamReader(input, charset)); } /** * Create an {@link INI} document instance by reading a stream with content in * INI format. * * @param reader reader providing stream of INI content * @return document * @throws IOException on I/O error * @throws ParseException on parsing error */ public INI read(Reader reader) throws IOException, ParseException { String line; var lineReader = new BufferedReader(reader); var lineBuffer = new StringBuilder(); var continuation = false; var offset = 0; var rootSections = createSectionMap(preserveOrder, caseSensitiveSections); var globalProperties = createPropertyMap(preserveOrder, caseSensitiveKeys); SectionImpl section = null; var lineNo = 0; var lastAppendedLine = -1; var ini = new DefaultINI(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveSections, globalProperties, rootSections, interpolator, variablePattern, missingVariableMode); while ((line = lineReader.readLine()) != null) { offset += line.length(); if (continuation) { lineBuffer.append(' '); line = line.stripLeading(); continuation = false; } if (lineContinuations && isLineContinuation(line)) { line = line.substring(0, line.length() - 1); lineBuffer.append(line); continuation = true; continue; } lineBuffer.append(line); if (lineBuffer.length() == 0) continue; var fullLine = lineBuffer.toString(); lineBuffer.setLength(0); var lineWithoutLeading = fullLine.stripLeading(); if (lineWithoutLeading.length() == 0 || ( comments && lineWithoutLeading.charAt(0) == commentCharacter)) { continue; } var lineChars = lineWithoutLeading.toCharArray(); var escape = false; var buf = new StringBuilder(); String key = null; char quoted = '\0'; var column = 0; while(column < lineChars.length) { for (; column < lineChars.length; column++) { var ch = lineChars[column]; if (escape) { switch (ch) { case '\\': case '\'': case '"': case '#': case ':': buf.append(ch); break; case '0': buf.append((char) 0); break; case 'a': buf.append((char) 7); break; case 'b': buf.append((char) 8); break; case 't': buf.append((char) 11); break; case 'n': buf.append((char) 10); break; case 'r': buf.append((char) 13); break; // TODO unicode escape default: if ((comments && ch == commentCharacter) || ch == valueSeparator) buf.append(ch); else { buf.append('\\'); buf.append(ch); } break; } escape = false; } else { if (ch == '\\' && (escapeMode == EscapeMode.ALWAYS || (escapeMode == EscapeMode.QUOTED && quoted != '\0'))) { escape = true; } else { if (quoted != '\0') { if (quoted == ch) { quoted = '\0'; continue; } else buf.append(ch); } else { if (isQuote(ch)) { quoted = ch; } else if (ch == commentCharacter && comments && inlineComments) { column = line.length(); break; } else if (key == null) { if (ch == valueSeparator) { key = unescapeJavaString(buf.toString()); buf.setLength(0); } // else if(ch == ' ' || ch == '\t') { // continue; // } else buf.append(ch); } else { if(ch == multiValueSeparator && multiValueMode == MultiValueMode.SEPARATED) { column++; break; } buf.append(ch); } } } } } if (key == null) { key = buf.toString(); buf.setLength(0); } if (valueSeparatorWhitespace) key = key.stripTrailing(); if (key.startsWith("[")) { var eidx = key.indexOf(']', 1); if (eidx == -1) { if (parseExceptions) { throw new ParseException("Incorrect syntax for section name, no closing ']'.", offset); } } else { if (eidx != key.length() - 1) { if (parseExceptions) { throw new ParseException( "Incorrect syntax for section name, trailing content after closing ']'.", offset); } else continue; } key = key.substring(1, eidx); String[] sectionPath; if (nestedSections) { sectionPath = tokenize(key, sectionPathSeparator); } else { sectionPath = new String[] { key }; } var parent = rootSections; Data lastSection = null; for (int sectionIdx = 0; sectionIdx < sectionPath.length; sectionIdx++) { var sectionKey = sectionPath[sectionIdx]; var last = sectionIdx == sectionPath.length - 1; var sectionsForKey = parent.get(sectionKey); var parentSection = lastSection == null ? ini : lastSection; if (last) { var newSection = new SectionImpl(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveSections, parentSection, sectionKey, interpolator, variablePattern, missingVariableMode); if (sectionsForKey == null) { /* Doesn't exist, just add */ sectionsForKey = new Section[] { newSection }; parent.put(sectionKey, sectionsForKey); } else { switch (duplicateSectionAction) { case ABORT: throw new ParseException( MessageFormat.format("Duplicate section key {0}.", sectionKey), offset); case REPLACE: sectionsForKey = new Section[] { newSection }; parent.put(sectionKey, sectionsForKey); break; case IGNORE: continue; case APPEND: var newSections = new Section[sectionsForKey.length + 1]; System.arraycopy(sectionsForKey, 0, newSections, 0, sectionsForKey.length); newSections[sectionsForKey.length] = newSection; parent.put(sectionKey, newSections); sectionsForKey = newSections; break; case MERGE: newSection.merge(sectionsForKey[0].values()); parent.put(sectionKey, new Section[] { newSection }); sectionsForKey = new Section[] { newSection }; break; } } } else { if (sectionsForKey == null) { /* Doesn't exist, just add */ sectionsForKey = new Section[] { new SectionImpl(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveSections, parentSection, sectionKey, interpolator, variablePattern, missingVariableMode) }; parent.put(sectionKey, sectionsForKey); } } parent = sectionsForKey[0].sections(); lastSection = section = (SectionImpl)sectionsForKey[0]; } } } else { var val = buf.toString(); buf.setLength(0); if (val.isEmpty() && !emptyValues) { if (parseExceptions) throw new ParseException("Empty values are not allowed.", offset); } else { if (trimmedValue) { val = val.trim(); } else if (valueSeparatorWhitespace) { val = val.stripLeading(); } Map sectionProperties; if (section == null) { if (globalSection) { sectionProperties = globalProperties; } else { if (parseExceptions) throw new ParseException( "Global properties are not allowed, all properties must be in a [Section].", offset); else continue; } } else { sectionProperties = section.values; } var oldValues = sectionProperties.get(key); if (oldValues != null && multiValueMode == MultiValueMode.SEPARATED && duplicateKeysAction != DuplicateAction.ABORT && duplicateKeysAction != DuplicateAction.IGNORE && duplicateKeysAction != DuplicateAction.REPLACE) { appendValue(key, sectionProperties, val, oldValues); lastAppendedLine = lineNo; } else if (oldValues == null || multiValueMode == MultiValueMode.OFF || ( duplicateKeysAction == DuplicateAction.REPLACE && oldValues != null && lastAppendedLine != lineNo ) ) { /* Doesn't exist, just add */ if(val.equals("")) sectionProperties.put(key, new String[0]); else sectionProperties.put(key, new String[] { val }); lastAppendedLine = lineNo; } else { switch (duplicateKeysAction) { case ABORT: throw new ParseException(MessageFormat.format("Duplicate property key {0}.", key), offset); case IGNORE: lastAppendedLine = -1; continue; default: appendValue(key, sectionProperties, val, oldValues); lastAppendedLine = lineNo; break; } } } } } lineNo++; } return ini; } protected void appendValue(String key, Map sectionProperties, String value, String[] valuesForKey) { var newValues = new String[valuesForKey.length + 1]; System.arraycopy(valuesForKey, 0, newValues, 0, valuesForKey.length); newValues[valuesForKey.length] = value; sectionProperties.put(key, newValues); } private boolean isQuote(char ch) { for (var c : quoteCharacters) if (c == ch) return true; return false; } /** * Unescapes a string that contains standard Java escape sequences. *

    *
  • \b \f \n \r \t \" \' : * BS, FF, NL, CR, TAB, double and single quote.
  • *
  • \X \XX \XXX : Octal character * specification (0 - 377, 0x00 - 0xFF).
  • *
  • \uXXXX : Hexadecimal based Unicode character.
  • *
* * @param st * A string optionally containing standard java escape sequences. * @return The translated string. */ public String unescapeJavaString(String st) { StringBuilder sb = new StringBuilder(st.length()); for (int i = 0; i < st.length(); i++) { char ch = st.charAt(i); if (ch == '\\') { char nextChar = (i == st.length() - 1) ? '\\' : st .charAt(i + 1); // Octal escape? if (nextChar >= '0' && nextChar <= '7') { String code = "" + nextChar; i++; if ((i < st.length() - 1) && st.charAt(i + 1) >= '0' && st.charAt(i + 1) <= '7') { code += st.charAt(i + 1); i++; if ((i < st.length() - 1) && st.charAt(i + 1) >= '0' && st.charAt(i + 1) <= '7') { code += st.charAt(i + 1); i++; } } sb.append((char) Integer.parseInt(code, 8)); continue; } switch (nextChar) { case '\\': ch = '\\'; break; case 'b': ch = '\b'; break; case 'f': ch = '\f'; break; case 'n': ch = '\n'; break; case 'r': ch = '\r'; break; case 't': ch = '\t'; break; case '\"': ch = '\"'; break; case '\'': ch = '\''; break; // Hex Unicode: u???? case 'u': if (i >= st.length() - 5) { ch = 'u'; break; } int code = Integer.parseInt( "" + st.charAt(i + 2) + st.charAt(i + 3) + st.charAt(i + 4) + st.charAt(i + 5), 16); sb.append(Character.toChars(code)); i += 5; continue; } i++; } sb.append(ch); } return sb.toString(); } boolean isLineContinuation(String line) { var contCount = 0; for (int i = line.length() - 1; i >= 0; i--) { var ch = line.charAt(i); if (ch != '\\') break; contCount++; } return contCount % 2 == 1; } static String[] tokenize(String val, char sep) { var tkns = new StringTokenizer(val, String.valueOf(sep)); var arr = new String[tkns.countTokens()]; for (var i = 0; tkns.hasMoreTokens(); i++) { arr[i] = tkns.nextToken(); } return arr; } static String[] trim(String[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = arr[i].trim(); return arr; } static Map createSectionMap(boolean preserveOrder, boolean caseSensitiveSections) { if (preserveOrder) { if (caseSensitiveSections) { return new LinkedHashMap<>(); } else { return new LinkedCaseInsensitiveMap<>(); } } else { if (caseSensitiveSections) { return new HashMap<>(); } else { return new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } } } static Map createPropertyMap(boolean preserveOrder, boolean caseSensitiveKeys) { if (preserveOrder) { if (caseSensitiveKeys) { return new LinkedHashMap<>(); } else { return new LinkedCaseInsensitiveMap<>(); } } else { if (caseSensitiveKeys) { return new HashMap<>(); } else { return new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy