// Copyright (c) 2003-present, Jodd Team (http://jodd.org) // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package jodd.props; import jodd.util.CharUtil; import jodd.util.StringPool; import jodd.util.StringUtil; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * {@link Props} parser. */ public class PropsParser implements Cloneable { protected static final String PROFILE_LEFT = "<"; protected static final String PROFILE_RIGHT = ">"; protected final PropsData propsData; /** * Value that will be inserted when escaping the new line. */ protected String escapeNewLineValue = StringPool.EMPTY; /** * Trims left the value. */ protected boolean valueTrimLeft = true; /** * Trims right the value. */ protected boolean valueTrimRight = true; /** * Defines if starting whitespaces when value is split in the new line * should be ignored or not. */ protected boolean ignorePrefixWhitespacesOnNewLine = true; /** * Defines if multi-line values may be written using triple-quotes * as in python. */ protected boolean multilineValues = true; /** * Don't include empty properties. */ protected boolean skipEmptyProps = true; public PropsParser() { this.propsData = new PropsData(); } public PropsParser(final PropsData propsData) { this.propsData = propsData; } public PropsData getPropsData() { return propsData; } @Override public PropsParser clone() { final PropsParser pp = new PropsParser(this.propsData.clone()); pp.escapeNewLineValue = escapeNewLineValue; pp.valueTrimLeft = valueTrimLeft; pp.valueTrimRight = valueTrimRight; pp.ignorePrefixWhitespacesOnNewLine = ignorePrefixWhitespacesOnNewLine; pp.skipEmptyProps = skipEmptyProps; pp.multilineValues = multilineValues; return pp; } /** * Parsing states. */ protected enum ParseState { TEXT, ESCAPE, ESCAPE_NEWLINE, COMMENT, VALUE } /** * Different assignment operators. */ protected enum Operator { ASSIGN, QUICK_APPEND, COPY } /** * Loads properties. */ public void parse(final String in) { ParseState state = ParseState.TEXT; ParseState stateOnEscape = null; boolean insideSection = false; String currentSection = null; String key = null; Operator operator = Operator.ASSIGN; final StringBuilder sb = new StringBuilder(); final int len = in.length(); int ndx = 0; while (ndx < len) { final char c = in.charAt(ndx); ndx++; if (state == ParseState.COMMENT) { // comment, skip to the end of the line if (c == '\n') { state = ParseState.TEXT; } } else if (state == ParseState.ESCAPE) { state = stateOnEscape;//ParseState.VALUE; switch (c) { case '\r': case '\n': // if the EOL is \n or \r\n, escapes both chars state = ParseState.ESCAPE_NEWLINE; break; // encode UTF character case 'u': int value = 0; for (int i = 0; i < 4; i++) { final char hexChar = in.charAt(ndx++); if (CharUtil.isDigit(hexChar)) { value = (value << 4) + hexChar - '0'; } else if (hexChar >= 'a' && hexChar <= 'f') { value = (value << 4) + 10 + hexChar - 'a'; } else if (hexChar >= 'A' && hexChar <= 'F') { value = (value << 4) + 10 + hexChar - 'A'; } else { throw new IllegalArgumentException("Malformed \\uXXXX encoding."); } } sb.append((char) value); break; case 't': sb.append('\t'); break; case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 'f': sb.append('\f'); break; default: sb.append(c); } } else if (state == ParseState.TEXT) { switch (c) { case '\\': // escape char, take the next char as is stateOnEscape = state; state = ParseState.ESCAPE; break; // start section case '[': sb.setLength(0); insideSection = true; break; // end section case ']': if (insideSection) { currentSection = sb.toString().trim(); sb.setLength(0); insideSection = false; if (currentSection.length() == 0) { currentSection = null; } } else { sb.append(c); } break; case '#': case ';': state = ParseState.COMMENT; break; // copy operator case '<': if (ndx == len || in.charAt(ndx) != '=') { sb.append(c); break; } operator = Operator.COPY; //ndx++; continue; // assignment operator case '+': if (ndx == len || in.charAt(ndx) != '=') { sb.append(c); break; } operator = Operator.QUICK_APPEND; //ndx++; continue; case '=': case ':': if (key == null) { key = sb.toString().trim(); sb.setLength(0); } else { sb.append(c); } state = ParseState.VALUE; break; case '\r': case '\n': add(currentSection, key, sb, true, operator); sb.setLength(0); key = null; operator = Operator.ASSIGN; break; case ' ': case '\t': // ignore whitespaces break; default: sb.append(c); } } else { switch (c) { case '\\': // escape char, take the next char as is stateOnEscape = state; state = ParseState.ESCAPE; break; case '\r': case '\n': if ((state == ParseState.ESCAPE_NEWLINE) && (c == '\n')) { sb.append(escapeNewLineValue); if (!ignorePrefixWhitespacesOnNewLine) { state = ParseState.VALUE; } } else { add(currentSection, key, sb, true, operator); sb.setLength(0); key = null; operator = Operator.ASSIGN; // end of value, continue to text state = ParseState.TEXT; } break; case ' ': case '\t': if ((state == ParseState.ESCAPE_NEWLINE)) { break; } default: sb.append(c); state = ParseState.VALUE; if (multilineValues) { if (sb.length() == 3) { // check for ''' beginning if (sb.toString().equals("'''")) { sb.setLength(0); int endIndex = in.indexOf("'''", ndx); if (endIndex == -1) { endIndex = in.length(); } sb.append(in, ndx, endIndex); // append add(currentSection, key, sb, false, operator); sb.setLength(0); key = null; operator = Operator.ASSIGN; // end of value, continue to text state = ParseState.TEXT; ndx = endIndex + 3; } } } } } } if (key != null) { add(currentSection, key, sb, true, operator); } } // ---------------------------------------------------------------- add /** * Adds accumulated value to key and current section. */ protected void add( final String section, final String key, final StringBuilder value, final boolean trim, final Operator operator) { // ignore lines without : or = if (key == null) { return; } String fullKey = key; if (section != null) { if (fullKey.length() != 0) { fullKey = section + '.' + fullKey; } else { fullKey = section; } } String v = value.toString(); if (trim) { if (valueTrimLeft && valueTrimRight) { v = v.trim(); } else if (valueTrimLeft) { v = StringUtil.trimLeft(v); } else { v = StringUtil.trimRight(v); } } if (v.length() == 0 && skipEmptyProps) { return; } extractProfilesAndAdd(fullKey, v, operator); } /** * Extracts profiles from the key name and adds key-value to them. */ protected void extractProfilesAndAdd(final String key, final String value, final Operator operator) { String fullKey = key; int ndx = fullKey.indexOf(PROFILE_LEFT); if (ndx == -1) { justAdd(fullKey, value, null, operator); return; } // extract profiles ArrayList<String> keyProfiles = new ArrayList<>(); while (true) { ndx = fullKey.indexOf(PROFILE_LEFT); if (ndx == -1) { break; } final int len = fullKey.length(); int ndx2 = fullKey.indexOf(PROFILE_RIGHT, ndx + 1); if (ndx2 == -1) { ndx2 = len; } // remember profile final String profile = fullKey.substring(ndx + 1, ndx2); keyProfiles.add(profile); // extract profile from key ndx2++; final String right = (ndx2 == len) ? StringPool.EMPTY : fullKey.substring(ndx2); fullKey = fullKey.substring(0, ndx) + right; } if (fullKey.startsWith(StringPool.DOT)) { // check for special case when only profile is defined in section fullKey = fullKey.substring(1); } // add value to extracted profiles justAdd(fullKey, value, keyProfiles, operator); } /** * Core key-value addition. */ protected void justAdd(final String key, final String value, final ArrayList<String> keyProfiles, final Operator operator) { if (operator == Operator.COPY) { HashMap<String,Object> target = new HashMap<>(); String[] profiles = null; if (keyProfiles != null) { profiles = keyProfiles.toArray(new String[keyProfiles.size()]); } String[] sources = StringUtil.splitc(value, ','); for (String source : sources) { source = source.trim(); // try to extract profile for parsing String[] lookupProfiles = profiles; String lookupProfilesString = null; int leftIndex = source.indexOf('<'); if (leftIndex != -1) { int rightIndex = source.indexOf('>'); lookupProfilesString = source.substring(leftIndex + 1, rightIndex); source = source.substring(0, leftIndex).concat(source.substring(rightIndex + 1)); lookupProfiles = StringUtil.splitc(lookupProfilesString, ','); StringUtil.trimAll(lookupProfiles); } String[] wildcards = new String[] {source + ".*"}; propsData.extract(target, lookupProfiles, wildcards, null); for (Map.Entry<String, Object> entry : target.entrySet()) { String entryKey = entry.getKey(); String suffix = entryKey.substring(source.length()); String newKey = key + suffix; String newValue = "${" + entryKey; if (lookupProfilesString != null) { newValue += "<" + lookupProfilesString + ">"; } newValue += "}"; if (profiles == null) { propsData.putBaseProperty(newKey, newValue, false); } else { for (final String p : profiles) { propsData.putProfileProperty(newKey, newValue, p, false); } } } } return; } boolean append = operator == Operator.QUICK_APPEND; if (keyProfiles == null) { propsData.putBaseProperty(key, value, append); return; } for (final String p : keyProfiles) { propsData.putProfileProperty(key, value, p, append); } } }