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

org.tbee.tecl.TECL Maven / Gradle / Ivy

Go to download

A language parser specifically aimed at configuration files, trying to solve the issues found in XML, JSON, YAML, etc.

There is a newer version: 1.7.1
Show newest version
package org.tbee.tecl;

import java.io.IOException;

/*-
 * #%L
 * TECL
 * %%
 * Copyright (C) 2020 Tom Eugelink
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.text.StringTokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

For a more detail explanation refer to the README

* *

Basic usage: *

{@code
 *     TECL tecl = TECL.parser()
 *         .addParameter("env", "production") // Optional
 *         .schema("..TESD file.." ) // Optional
 *         .parse("..TECL file..");
 *     String title = tecl.str("title");
 *     int timeout = tecl.integer("/servers/settings[3]/timeout", 1000);  
 * }
*

* *

Quick usage: *

{@code
 *     TECL tecl = TECL.parser().findAndParse();
 *     String title = tecl.str("title");
 *     int timeout = tecl.integer("/servers/settings[3]/timeout", 1000);  
 * }
*

*/ public class TECL { private final static String ENV_PREFIX = "env@"; private final static String SYS_PREFIX = "sys@"; final Logger logger = LoggerFactory.getLogger(TECL.class); static Map, BiFunction> buildinConvertFunctions = new HashMap<>(); static { buildinConvertFunctions.put(String.class, (s, d) -> s); buildinConvertFunctions.put(Integer.class, (s, d) -> s.isBlank() ? d : Integer.valueOf(s)); buildinConvertFunctions.put(BigInteger.class, (s, d) -> s.isBlank() ? d : new BigInteger(s)); buildinConvertFunctions.put(BigDecimal.class, (s, d) -> s.isBlank() ? d : new BigDecimal(s)); buildinConvertFunctions.put(Boolean.class, (s, d) -> s.isBlank() ? d : Boolean.valueOf(s)); buildinConvertFunctions.put(Double.class, (s, d) -> s.isBlank() ? d : Double.valueOf(s)); buildinConvertFunctions.put(LocalDate.class, (s, d) -> s.isBlank() ? d : LocalDate.parse(s)); buildinConvertFunctions.put(LocalDateTime.class, (s, d) -> s.isBlank() ? d : LocalDateTime.parse(s)); } // ===================================== // parser /** * */ static public TECLParser parser() { return new TECLParser(); } /** * Will try to find a config.tecl and then parse that. * First match will be used: * 1. See if system property config.tecl is defined (-Dconfig.tecl) * 2. See if environment variable config_tecl is defined * 3. See if the file ./config.tecl exists * 4. See if we can open the resource /config.tecl * * Assuming the config.tecl is encoded in UTF-8. * * @return TECL or null if not found, may throw RuntimeException containing a IOException * * @see TECLParser */ static public TECL findAndParse() { try { return parser().findAndParse(); } catch (IOException e) { throw new RuntimeException(e); } } // ===================================== // Constructor public TECL(String id) { this.id = id; } private final String id; TECL(String id, TECL parent) { this(id); setParent(parent, 0); } public void setParent(TECL parent, int idxInParent) { this.parent = parent; this.idxInParent = idxInParent; } private TECL parent; private Integer idxInParent; public String getId() { return id; } /** * Path from root to here */ public String getPath() { String path = (parent == null ? "" : parent.getPath()) + id + (idxInParent == null ? "" : "[" + idxInParent + "]") + "/" ; return path; } public String createFullPathToKey(int idx, String key) { String path = getPath() + key + "[" + idx + "]" ; return path; } /** * Get the parent TECL * @return */ public TECL getParent() { return parent; } /** * Get the root TECL * @return */ public TECL getRoot() { TECL tecl = this; while (tecl.parent != null) { tecl = tecl.parent; } return tecl; } /** * Is the TECL empty; no properties, no groups * @return */ public boolean isEmpty() { return properties.isEmpty() && groups.isEmpty(); } // ===================================== // get /** * Get a value using a directory-style path, like /group1/group2[4]/value * * @param * @param path the path the access * @param clazz the return type (also used to get appropriate convert function) * @return a list of found values */ public R get(String path, Class clazz) { return get(path, null, clazz); } /** * Get a value using a directory-style path, like /group1/group2[4]/value * * @param * @param path the path the access * @param def * @param clazz the return type (also used to get appropriate convert function) * @return a list of found values */ public R get(String path, R def, Class clazz) { List list = list(path, clazz); if (list.isEmpty()) { return def; } return list.get(0); } /** * Get a value using a directory-style path, like /group1/group2[4]/value * * @param * @param path the path the access * @param def * @param convertFunction * @return a list of found values */ public R getUsingFunction(String path, R def, BiFunction convertFunction) { List list = listUsingFunction(path, Arrays.asList(def), convertFunction); return list.isEmpty() ? def : list.get(0); } /** * Lookup function: * - First search for the occurence of indexOfValue in indexOfPath. * - Using that index get the value for path. * * @param * @param indexOfPath * @param indexOfValue * @param path * @param def * @param clazz * @return */ public List list(String indexOfPath, String indexOfValue, String path, List def, Class clazz) { int idx = strs(indexOfPath).indexOf(indexOfValue); if (idx < 0) { return def; } return listUsingFunction(path + "[" + idx + "]", def, convertFunction(clazz)); } /** * Get a value using a directory-style path, like /group1/group2[4]/value * * @param * @param path the path the access * @param clazz the return type (also used to get appropriate convert function) * @return a list of found values */ public List list(String path, Class clazz) { return list(path, Collections.emptyList(), clazz); } /** * Get a value using a directory-style path, like /group1/group2[4]/value * * This is the main way to access values in TECL, all convenience methods use this method * * @param * @param path the path the access * @param def the value to return if nothing is found * @param clazz the return type (also used to get appropriate convert function) * @return a list of found values */ public List list(String path, List def, Class clazz) { return listUsingFunction(path, def, convertFunction(clazz)); } /** * Get values using a directory-style path, like /group1/group2[4]/value * * This is the main way to access values in TECL, all convenience methods use this method * * @param * @param path the path the access * @param def the value to return if nothing is found * @param convertFunction the conversion function to convert properties to their end type, if you access groups this should be null * @return a list of found values */ @SuppressWarnings("unchecked") public List listUsingFunction(String path, List def, BiFunction convertFunction) { String context = this.getPath() + " -> " + path + ": "; // Specials if (path.startsWith(ENV_PREFIX)) { return getEnv(path, convertFunction, context); } if (path.startsWith(SYS_PREFIX)) { return getSys(path, convertFunction, context); } // Travel the TECL tree using the path EndNode endNode = travelHierarchy(path, context); final TECL tecl = endNode.tecl; final String node = endNode.node; final List idxs = endNode.idxs; context = tecl.getPath() + " -> " + path + ": "; // This is the last node, it may be a property, group, list or reference // First get all relevant info List> valueAttibutesPairs = tecl.properties.get(node); List properties = (valueAttibutesPairs == null ? null : valueAttibutesPairs.stream() .map(vap -> vap == null ? null : vap.value) .collect(Collectors.toList())); if (logger.isDebugEnabled()) logger.debug(context + "Properties = " + properties); List groups = tecl.groups.get(node); if (logger.isDebugEnabled()) logger.debug(context + "Groups = " + groups); List list = tecl.groups.get("|" + node + "|"); if (logger.isDebugEnabled()) logger.debug(context + "Lists = " + list); // Construct the results List results = null; Integer idx = (idxs.isEmpty() ? null : idxs.get(0)); // If there is no convertFunction, then the result are groups if (convertFunction == null) { if (logger.isDebugEnabled()) logger.debug(context + "There no convert function, so the last token must be groups."); results = resolveFinalGroup(properties, groups, idx, context); } else { if (logger.isDebugEnabled()) logger.debug(context + "There is a convert function, so the last token must be properties."); results = resolveFinalProperty(node, properties, list, idx, convertFunction, def, context); } if (logger.isDebugEnabled()) logger.debug(context + "Results: " + results); // secondary index (for lists) Integer idx1 = (idxs.size() <= 1 ? null : idxs.get(1)); results = optionallyApplyIdx(context, results, idx1); // done return results.isEmpty() ? def : results; } /* */ private List getSys(String path, BiFunction convertFunction, String context) { String sys = path.substring(SYS_PREFIX.length()); R result = convertFunction.apply(System.getProperty(sys), null); if (logger.isDebugEnabled()) logger.debug(context + "sys path, result = " + result); return Arrays.asList(result); } /* */ private List getEnv(String path, BiFunction convertFunction, String context) { String env = path.substring(ENV_PREFIX.length()); R result = convertFunction.apply(System.getenv(env), null); if (logger.isDebugEnabled()) logger.debug(context + "env path, result = " + result); return Arrays.asList(result); } /* */ private EndNode travelHierarchy(String path, String context) { // Determine the starting point TECL tecl = determineStartingPoint(path, context); context = tecl.getPath() + " -> " + path + ": "; // First split into its parts List nodes = new ArrayList<>(new StringTokenizer(path, "/").getTokenList()); if (logger.isDebugEnabled()) logger.debug(context + "path tokenized: " + nodes); // step over the nodes in the path String node = null; List idxs = new ArrayList(); while (!nodes.isEmpty()) { // Get current node node = nodes.remove(0); if (logger.isDebugEnabled()) logger.debug(context + "node = " + node); context = tecl.getPath() + node + ": "; // extract the indexes from the node (if any, may be two) node = extractIdxs(node, idxs); if (logger.isDebugEnabled()) logger.debug(context + "node = " + node + ", idxs = " + idxs); // Is this the last token? // If so, break out, because the travel part is done (the end node is handled differently) boolean lastToken = nodes.isEmpty(); if (lastToken) { break; } // Not the last token, get the properties for this node List properties = tecl.properties.get(node).stream().map(vap -> vap.value).collect(Collectors.toList());; int idx = (idxs.isEmpty() ? 0 : idxs.get(0)); // This either is a group or a reference resolving to a group // If it is a reference if (isReference(properties, idx)) { // it must be a list of groups at this point List tecls = resolveReference(properties, idx, notExistingGroup(idx), null, context); tecl = tecls.get(0); if (logger.isDebugEnabled()) logger.debug(context + "Resolved reference: TECL= " + tecl.getPath()); continue; } // If it is a goto parent if ("..".equals(node)) { tecl = tecl.getParent(); if (logger.isDebugEnabled()) logger.debug(context + "Assumed group, TECL= " + tecl.getPath()); continue; } // It must a the name of a group then tecl = tecl.grp(idx, node); if (logger.isDebugEnabled()) logger.debug(context + "Assumed group, TECL= " + tecl.getPath()); } // return the node we ended on EndNode endNode = new EndNode(); endNode.tecl = tecl; endNode.node = node; endNode.idxs = idxs; return endNode; } class EndNode { TECL tecl; String node; List idxs; } /* */ private TECL determineStartingPoint(String path, String context) { TECL tecl = null; if (path.startsWith("/")) { tecl = this.getRoot(); if (logger.isDebugEnabled()) logger.debug(context + "start at root, tecl = " + tecl); } else { tecl = this; if (logger.isDebugEnabled()) logger.debug(context + "start at current, tecl = " + tecl); } return tecl; } /* */ private List resolveFinalGroup(List properties, List groups, Integer idx, String context) { // If we have a reference overlapping the groups if (groups.isEmpty() && isReference(properties, 0)) { // Resolve the reference if (logger.isDebugEnabled()) logger.debug(context + "We have no groups, but we do a single property which is a reference: " + properties); List results = resolveReference(properties, idx, null, null, context); return results; } // The result are the groups groups = optionallyApplyIdx(context, groups, idx); List results = (List) groups; return results; } /* */ private List resolveFinalProperty(String node, List properties, List list, Integer idx, BiFunction convertFunction, List def, String context) { // If there is a reference if (isReference(properties, 0)) { if (logger.isDebugEnabled()) logger.debug(context + "We have a single property which is a reference, going to resolve that: " + properties); List results = resolveReference(properties, 0, null, convertFunction, context); results = optionallyApplyIdx(context, results, idx); return results; } // If we have a list overlapping the properties, replace the properties with those in the list if (idx != null && list.size() > idx && list.get(idx) != null) { properties = list.get(idx).properties.get(node).stream().map(vap -> vap.value).collect(Collectors.toList()); if (logger.isDebugEnabled()) logger.debug(context + "There is an overlapping list, replaced properties with its contents. Properties = " + properties); } // Apply the index properties = optionallyApplyIdx(context, properties, idx); // Convert to end value List results = new ArrayList(); for (String property : properties) { // But each property can be a reference again if (isReference(property)) { // Resolve reference if (logger.isDebugEnabled()) logger.debug(context + "Property is a reference: " + property); List varResult = resolveReference(property, null, convertFunction, context); results.addAll(varResult); } else { // Convert property to end type property = sanatizeString(property); R result = convertFunction.apply(property, def == null || def.isEmpty() ? null : def.get(0)); if (logger.isDebugEnabled()) logger.debug(context + "Property converted: " + property + " -> " + result); results.add(result); } }; return results; } /* * */ private String extractIdxs(String node, List idxs) { idxs.clear(); while (node.contains("[")) { int startIdx = node.indexOf("["); int endIdx = node.indexOf("]"); String idxString = node.substring(startIdx + 1,endIdx); Integer idx = idxString.trim().isEmpty() ? null : Integer.parseInt(idxString); idxs.add(idx); String remainingIdx = node.substring(endIdx + 1); node = node.substring(0, startIdx) + remainingIdx; } return node; } /* */ private List optionallyApplyIdx(String context, List list, Integer idx) { if (idx != null && list.size() > idx) { R result = list.get(idx); list = new ArrayList(); list.add(result); if (logger.isDebugEnabled()) logger.debug(context + "Limit to idx = " + idx + ", result: " + list); } return list; } /* */ private boolean isReference(List properties, int idx) { return properties != null // && properties.size() > idx // && properties.get(idx) != null // && properties.get(idx).startsWith("$"); } /* */ private boolean isReference(String property) { return property != null && property.startsWith("$"); } /* */ private T resolveReference(List properties, Integer idx, List def, BiFunction convertFunction, String context) { if (idx == null) { idx = 0; } String var = properties.get(idx); return resolveReference(var, def, convertFunction, context); } /* */ private T resolveReference(String var, List def, BiFunction convertFunction, String context) { if (logger.isDebugEnabled()) logger.debug(context + "Found reference: " + var); var = var.substring(1); // strip "$" T t = (T)listUsingFunction(var, def, convertFunction); if (logger.isDebugEnabled()) logger.debug(context + "Resolved reference: " + var + " -> " + t); return t; } // ===================================== // properties private class ValueAttibutesPair { T value; TECL attributes; ValueAttibutesPair(T value, TECL attributes) { this.value = value; this.attributes = attributes; this.attributes.convertFunctions = TECL.this.getRoot().convertFunctions; // use the convertFunctions from the official TECL tree } ValueAttibutesPair(T value, List attributes) { this(value, convertToTECL(attributes)); } ValueAttibutesPair(T value) { this(value, EMPTY_ATTRIBUTES); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o == null || !(o instanceof ValueAttibutesPair)) { return false; } return value.equals(((ValueAttibutesPair)o).value); } @Override public String toString() { return "" + value + (attributes == EMPTY_ATTRIBUTES ? "" : attributes.toString()); } } static private TECL convertToTECL(List attributes) { if (attributes == null || attributes.isEmpty()) { return EMPTY_ATTRIBUTES; } TECL tecl = new TECL(""); attributes.forEach(a -> tecl.setProperty(a.key, a.value, null)); // Do we need to support multiple attributes (x=0 x=1 x=2 using something like list("x").size as index (1st parameter)? return tecl; } final static private TECL EMPTY_ATTRIBUTES = new TECL(""); final IndexedValues> properties = new IndexedValues<>(); /** * Set a property value. This is identical to setting an indexed property with index 0. * @param key * @param value */ public void setProperty(String key, String value, List attributes) { properties.set(0, key, new ValueAttibutesPair(value, attributes), false); } /** * Set an indexed property value. You cannot override a value. * @param idx * @param key * @param value * @param attributes */ public void setProperty(int idx, String key, String value, List attributes) { properties.set(idx, key, new ValueAttibutesPair(value, attributes), false); } /** * Set an indexed property value. * @param idx * @param key * @param value * @param allowOverwrite * @param attributes */ public void setProperty(int idx, String key, String value, boolean allowOverwrite, List attributes) { properties.set(idx, key, new ValueAttibutesPair(value, attributes), allowOverwrite); } /** * Clear property. This means all values in case of an index property. * @param key */ public void clearProperty(String key) { properties.clear(key); } /** * Clear single indexed property. * @param key */ public void clearProperty(int idx, String key) { properties.clear(idx, key); } /** * Return the index of the specified value for this specified property * @param key * @param value * @return */ public int indexOf(String key, String value) { return properties.indexOf(key, new ValueAttibutesPair(value)); } /** * Count the number of values for the specified property * @param key * @return */ public int count(String key) { return properties.count(key); } /** * Get the raw uninterpreted value of a property. * This is a last resort when, for example, there is an overlap using references. * But these situations preferably are prevented. * * @param idx * @param key * @param def * @return */ public String raw(int idx, String key, String def) { ValueAttibutesPair valueAttibutesPair = properties.get(idx, key, null); return valueAttibutesPair == null ? null : valueAttibutesPair.value; } /** * Get the raw uninterpreted value of a property. * This is a last resort when, for example, there is an overlap using references. * But these situations preferably are prevented. * * @param idx * @param key * @return */ public TECL attr(int idx, String key) { ValueAttibutesPair valueAttibutesPair = properties.get(idx, key, null); return valueAttibutesPair == null ? new TECL("", this) : valueAttibutesPair.attributes; } public TECL attr(String key) { return attr(0, key); } /** * Add a custom convert function to the tecl */ public void addConvertFunction(Class clazz, BiFunction convertFunction) { convertFunctions.put(clazz, convertFunction); } @SuppressWarnings("unchecked") public BiFunction convertFunction(Class clazz) { return (BiFunction)getRoot().convertFunctions.get(clazz); } public void populateConvertFunctions() { convertFunctions = new HashMap<>(buildinConvertFunctions); } Map, BiFunction> convertFunctions = null; /** Convenience method to return a string */ public String str(String key) { return str(0, key, null); } public String str(String key, String def) { return str(0, key, def); } public String str(int idx, String key) { return str(idx, key, null); } public String str(int idx, String key, String def) { return list(key + "[" + idx + "]", asList(def), String.class).get(0); } public List strs(String key) { return list(key, Collections.emptyList(), String.class); } public String str(String indexOfKey, String indexOfValue, String key, String def) { return list(indexOfKey, indexOfValue, key, asList(def), String.class).get(0); } /** Convenience method to return an Integer */ public Integer integer(String key) { return integer(0, key, null); } public Integer integer(String key, Integer def) { return integer(0, key, def); } public Integer integer(int idx, String key) { return integer(idx, key, null); } public Integer integer(int idx, String key, Integer def) { return list(key + "[" + idx + "]", asList(def), Integer.class).get(0); } public List integers(String key) { return list(key, Collections.emptyList(), Integer.class); } public Integer integer(String indexOfKey, String indexOfValue, String key, Integer def) { return list(indexOfKey, indexOfValue, key, asList(def), Integer.class).get(0); } /** Convenience method to return an Double */ public Double dbl(String key) { return dbl(0, key, null); } public Double dbl(String key, Double def) { return dbl(0, key, def); } public Double dbl(int idx, String key) { return dbl(idx, key, null); } public Double dbl(int idx, String key, Double def) { return list(key + "[" + idx + "]", asList(def), Double.class).get(0); } public List dbls(String key) { return list(key, Collections.emptyList(), Double.class); } public Double dbl(String indexOfKey, String indexOfValue, String key, Double def) { return list(indexOfKey, indexOfValue, key, asList(def), Double.class).get(0); } /** Convenience method to return an Boolean */ public Boolean bool(String key) { return bool(0, key, null); } public Boolean bool(String key, Boolean def) { return bool(0, key, def); } public Boolean bool(int idx, String key) { return bool(idx, key, null); } public Boolean bool(int idx, String key, Boolean def) { return list(key + "[" + idx + "]", asList(def), Boolean.class).get(0); } public List bools(String key) { return list(key, Collections.emptyList(), Boolean.class); } public Boolean bool(String indexOfKey, String indexOfValue, String key, Boolean def) { return list(indexOfKey, indexOfValue, key, asList(def), Boolean.class).get(0); } /** Convenience method to return a BigInteger */ public BigInteger bi(String key) { return bi(0, key, null); } public BigInteger bi(String key, BigInteger def) { return bi(0, key, def); } public BigInteger bi(int idx, String key) { return bi(idx, key, null); } public BigInteger bi(int idx, String key, BigInteger def) { return list(key + "[" + idx + "]", asList(def), BigInteger.class).get(0); } public List bis(String key) { return list(key, Collections.emptyList(), BigInteger.class); } public BigInteger bi(String indexOfKey, String indexOfValue, String key, BigInteger def) { return list(indexOfKey, indexOfValue, key, asList(def), BigInteger.class).get(0); } /** Convenience method to return a BigDecimal */ public BigDecimal bd(String key) { return bd(0, key, null); } public BigDecimal bd(String key, BigDecimal def) { return bd(0, key, def); } public BigDecimal bd(int idx, String key) { return bd(idx, key, null); } public BigDecimal bd(int idx, String key, BigDecimal def) { return list(key + "[" + idx + "]", asList(def), BigDecimal.class).get(0); } public List bds(String key) { return list(key, Collections.emptyList(), BigDecimal.class); } public BigDecimal bd(String indexOfKey, String indexOfValue, String key, BigDecimal def) { return list(indexOfKey, indexOfValue, key, asList(def), BigDecimal.class).get(0); } /** Convenience method to return a LocalDate */ public LocalDate localDate(String key) { return localDate(0, key, null); } public LocalDate localDate(String key, LocalDate def) { return localDate(0, key, def); } public LocalDate localDate(int idx, String key) { return localDate(idx, key, null); } public LocalDate localDate(int idx, String key, LocalDate def) { return list(key + "[" + idx + "]", asList(def), LocalDate.class).get(0); } public List localDates(String key) { return list(key, Collections.emptyList(), LocalDate.class); } public LocalDate localDate(String indexOfKey, String indexOfValue, String key, LocalDate def) { return list(indexOfKey, indexOfValue, key, asList(def), LocalDate.class).get(0); } /** Convenience method to return a LocalDateTime */ public LocalDateTime localDateTime(String key) { return localDateTime(0, key, null); } public LocalDateTime localDateTime(String key, LocalDateTime def) { return localDateTime(0, key, def); } public LocalDateTime localDateTime(int idx, String key) { return localDateTime(idx, key, null); } public LocalDateTime localDateTime(int idx, String key, LocalDateTime def) { return list(key + "[" + idx + "]", asList(def), LocalDateTime.class).get(0); } public List localDateTimes(String key) { return list(key, Collections.emptyList(), LocalDateTime.class); } public LocalDateTime localDateTime(String indexOfKey, String indexOfValue, String key, LocalDateTime def) { return list(indexOfKey, indexOfValue, key, asList(def), LocalDateTime.class).get(0); } // ===================================== // decrypt // Storage private String decryptKeyBase64 = null; void setDecryptKeyBase64(String decryptKeyBase64) { this.decryptKeyBase64 = decryptKeyBase64; } /** Methods for decrypting */ public String decrypt(String key) { return decrypt(0, key, null); } public String decrypt(String key, String def) { return decrypt(0, key, def); } public String decrypt(int idx, String key) { return decrypt(idx, key, null); } public String decrypt(int idx, String key, String def) { String str = str(idx, key, def); return decryptOnly(str); } public String decrypt(String indexOfKey, String indexOfValue, String key, String def) { String str = str(indexOfKey, indexOfValue, key, def); return decryptOnly(str); } /** * Provide a value to decrypt directly * * @param encryptedBase64 * @return */ public String decryptOnly(String encryptedBase64) { String decryptKeyBase64 = getRoot().decryptKeyBase64; if (decryptKeyBase64 == null) { throw new IllegalStateException("No decrypt key set. \n1) Generate a key pair using the EncrpytionHelper class (has a main, just start it). \n2) Encrypt the value with the public key using the EncryptionHelper and store in the TECL file. \n3) provide the private key to the parser."); } String decoded = EncryptionHelper.me.decode(encryptedBase64, decryptKeyBase64); return decoded; } // ===================================== // groups final IndexedValues groups = new IndexedValues<>(); /** * Add a group. * @param id * @return */ public TECL addGroup(String id) { TECL tecl = new TECL(id); int idx = groups.add(id, tecl); tecl.setParent(this, idx); return tecl; } TECL setGroup(int idx, String id) { TECL tecl = new TECL(id); groups.set(idx, id, tecl, false); tecl.setParent(this, idx); return tecl; } /** * Count the number of groups with the same id * @param id * @return */ public int countGrp(String id) { return groups.count(id); } /** * Get the group with the specified key. This is the same as getting thee first (index 0) group. * @param id * @return */ public TECL grp(String id) { return grp(0, id); } /** * Get the indexed group. * If the group is not found, an empty group is returned. * @param idx * @param key * @return */ public TECL grp(int idx, String key) { return listUsingFunction(key + "[" + idx + "]", notExistingGroup(idx), null).get(0); } private List notExistingGroup(int idx) { TECL group = new TECL("", this); group.exists = false; return asList(group); } private boolean exists = true; /** * Get all groups for a key. * @param key * @return */ public List grps(String key) { return listUsingFunction(key, Collections.emptyList(), null); } // ===================================== // ARGS /** * * @param args */ public void addCommandLineArguments(String[] args) { String key = null; for (String arg : args) { // do we have a key? if (arg.startsWith("--")) { key = arg.substring(2); continue; } if (arg.startsWith("-")) { key = arg.substring(1); continue; } // else it is a value, but for that we need a key if (key != null) { TECL group = getRoot(); String propertyId = key; // process intermediate groups if (key.startsWith("/")) { // split up the path String[] groupIds = key.split("\\/"); // the first one is empty because of the starting /, the last one is a property, so skip those for (int i = 1; i < groupIds.length - 1; i++) { // get the child group for this id String groupId = groupIds[i]; TECL parentGroup = group; group = group.grp(groupId); // If the child group does not exist, add it if (!group.exists) { group = parentGroup.addGroup(groupId); } } // the last node is the property id propertyId = groupIds[groupIds.length - 1]; } // set value group.setProperty(group.count(propertyId), propertyId, arg, null); key = null; } } } // ===================================== // SUPPORT /* * Implements an indexed store */ class IndexedValues { private final Map> keyTovaluesMap = new LinkedHashMap<>(); /* * Completely clear a single key; remove all values. */ void clear(String key) { List values = keyTovaluesMap.get(key); if (values != null) { if (logger.isDebugEnabled()) logger.debug(getPath() + ": clear property " + key); values.clear(); } } /* * Clear a single value for a single key */ void clear(int idx, String key) { List values = keyTovaluesMap.get(key); if (values != null) { values.clear(); } // Check if the value can be cleared if (values.size() <= idx) { return; } if (logger.isDebugEnabled()) logger.debug(getPath() + ": clear property " + key + "[" + idx + "]"); values.set(idx, null); } /* * Set a single value for a single key */ void set(int idx, String key, T value, boolean allowOverwrite) { // First get the list of values List values = keyTovaluesMap.get(key); if (values == null) { values = new ArrayList(); keyTovaluesMap.put(key, values); } // Check if the value can be put in while (values.size() <= idx) { values.add(null); } // Store the value T oldValue = values.get(idx); if (oldValue != null && !allowOverwrite) { throw new IllegalStateException(createFullPathToKey(idx, key) + " value is overwritten! " + oldValue + " -> " + value); } if (logger.isDebugEnabled()) logger.debug(getPath() + ": set property " + key + "[" + idx + "] = " + value); values.set(idx, value); } /* * Append a single value to a key */ int add(String key, T value) { int idx = count(key); set(idx, key, value, false); return idx; } /* * Count the number of values of a key */ int count(String key) { List values = keyTovaluesMap.get(key); if (values == null) { return 0; } return values.size(); } /* * Find the index of a value within a key. * This can be used to determine the row in a table, in order to support value-based-indexes. * For example * * | id | value | * | id1 | val1 | * | id2 | val2 | * | id3 | val3 | * * int idx = indexof("id", "id2"); * String val = get(idx, "value"); // This will hold "val2"; */ int indexOf(String key, T value) { List values = keyTovaluesMap.get(key); if (values == null) { return -1; } return values.indexOf(value); } /* * Get all values for a key */ List get(String key) { List values = keyTovaluesMap.get(key); if (values == null) { return Collections.emptyList(); } values = new ArrayList(values); // End users are not allowed to modify the list return values; } /* * Get a single value for a key */ T get(int idx, String key, T def) { List values = keyTovaluesMap.get(key); if (values == null) { return def; } T value = values.get(idx); if (value == null) { return def; } return value; } /* * Get all keys */ List getKeys() { return new ArrayList(keyTovaluesMap.keySet()); } /* * empty */ boolean isEmpty() { return keyTovaluesMap.isEmpty(); } /* * */ @Override public String toString() { return keyTovaluesMap.toString(); } } public static class ParseException extends RuntimeException { public ParseException(Exception e) { super(e); } } /** * @param s * @return */ private String sanatizeString(String s) { if (logger.isDebugEnabled()) logger.debug("-----"); if (logger.isDebugEnabled()) logger.debug("sanatize: >" + s + "<"); // check to see if it is quoted String trimmed = s.trim(); if ( s.length() >= 6 && trimmed.startsWith("\"\"\"") && trimmed.endsWith("\"\"\"") ) { s = sanitizeMultiline(s); } else if ( s.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"") ) { s = sanatizeQuotedString(s); } else { s = sanatizeUnquotedString(s); } if (logger.isDebugEnabled()) logger.debug("sanatize: done: >" + s + "<"); return s; } /** * A normal string is "...", a multiline string is """...""" * * @param s * @return */ String sanitizeMultiline(String s) { // Strip multiline markers s = s.substring(3, s.length() - 3); // There is possible whitespace plus a newline after the CDATA-start, // and a newline plus white spaces before the CDATA-end s = s.replaceAll("^\\s*\n", "") // preceding whitespace + first newline .replaceAll("\n\\s*$", ""); // last newline + trailing whitespace // split in lines List lines = s.lines().collect(Collectors.toList()); // determine the minimal number of whitespaces prefixing any of the lines int numberOfWhitespaceMin = Integer.MAX_VALUE; for (String line : lines) { int numberOfWhitespace = 0; while (Character.isWhitespace(line.charAt(0))) { line = line.substring(1); numberOfWhitespace++; } if (numberOfWhitespace < numberOfWhitespaceMin) { numberOfWhitespaceMin = numberOfWhitespace; } } // strip that number of whitespace from each line int numberOfWhitespaceMinFinal = numberOfWhitespaceMin; s = lines.stream() .map((line) -> line.substring(numberOfWhitespaceMinFinal)) .collect(Collectors.joining("\n")); // done return s; } /* * */ private String sanatizeQuotedString(String s) { if (logger.isDebugEnabled()) logger.debug("sanatize: treat as quoted string: >" + s + "<"); // strip quoted s = s.substring(1, s.length() - 1); if (logger.isDebugEnabled()) logger.debug("sanatize: trimmed quotes: >" + s + "<"); // unescape s = StringEscapeUtils.unescapeJava(s); // done return s; } /* * */ private String sanatizeUnquotedString(String s) { if (logger.isDebugEnabled()) logger.debug("sanatize: treat as unquoted string: >" + s + "<"); // done return s.trim(); } /* * */ private List asList(R value) { List values = new ArrayList(); values.add(value); return values; } @Override public String toString() { return getPath(); } public static class Attribute { final String key; final String value; public Attribute(String key, String value) { this.key = key; this.value = value; } public String toString() { return key + "=" + value; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy