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

javax0.jamal.tools.Params Maven / Gradle / Ivy

package javax0.jamal.tools;

import javax0.jamal.api.BadSyntax;
import javax0.jamal.api.Identified;
import javax0.jamal.api.Input;
import javax0.jamal.api.Processor;
import javax0.jamal.tools.param.StringFetcher;
import javax0.levenshtein.Levenshtein;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static javax0.jamal.tools.InputHandler.fetchId;
import static javax0.jamal.tools.InputHandler.firstCharIs;
import static javax0.jamal.tools.InputHandler.skip;
import static javax0.jamal.tools.InputHandler.startsWith;

/**
 * Parse the start of the input for macro parameters.
 * 

* Multi-line built-in macros, like {@code replaceLines} or {@code trimLines} get some parameters from the first line of * the input as well as from user defined macros. For built-in macros that need parameters this class helps with parsing * services. The use of the class is the following: *

{@code
 * Params.using(processor).from(this).keys(Set.of(n1,h2,h3,...,hN)).parse(input)
 * }
*

* This call parses the start of the input also consuming it. The parsing starts at the end of the first line. The * processor is used to fetch values from user defined macros in case the parameter is not defined on the line. The * parsing also checks that only {@code k1}, {@code k2}, {@code k3}, ... , {@code kn} keywords are used as parameters. *

* For example the macro trim line can be used as the following: * *

{@code
 * [@trimLine margin=5
 *   content to be trimmed
 * ]
 * }
*

* The implementation has to use the created {@code params} object to query the values. There are four different ways to * access the values, as documented in the JavaDoc of the public methods. *

* A parameter can present more than one time on the parsed line. In that case it is possible to query more values as * list. If multiple values are present then calling the single value return will throw an exception. *

* If a key is present on the input then the user defined macro of the same name is not used as value source. */ public class Params { public static class ExtraParams { private final Map> params = new HashMap<>(); } public interface Param { String[] keys(); void copy(Param p); void inject(Processor processor, String macroName); void set(String value); Param orElse(String i); Param orElseNull(); Param orElseInt(int i); Param as(Function converter); Param asPattern(); Param asType(Class type); Param as(Class klass, Function converter); Param asInt(); Param asBoolean(); Param asString(); Param> asList(); Param> asList(Class k); T get() throws BadSyntax; boolean is() throws BadSyntax; boolean isPresent() throws BadSyntax; /** * @return the name, which was actually used for the parameter. It is the same string as the name of the * parameter or one of the aliases. *

* If the parameter is multi-values then the name used the last time will be returned. */ String name(); /** * Set the name of the parameter, which was used. This is the original name of the parameter or one of the * aliases. This value can be queried later calling {@link #name()}. * * @param id the string that was used as the parameter name/id */ void setName(String id); } private final Processor processor; private final Map> holders = new HashMap<>(); private ExtraParams extraParams = null; private String macroName = null; // The character that signals the end of the parameters // by default it is `\n` which means that the parameters are occupying the first line // new-line, when processing parameters can also be escaped // many times this character is ')', for core macros ']' private Character terminal = '\n'; // The character that starts the parameters // by default there is no such character, parameters just start on the input // in some cases this is '(', for core macros it is '[' private Character start = null; private static class BadKey extends BadSyntax { final String key; BadKey(String key, String message) { super(message); this.key = key; } } private Params(Processor processor) { this.processor = processor; } /** * Call this static method as the first in the chain to get an uninitialized params builder structure. * * @param processor the processor which is used to access user defined macros when querying data * @return a new uninitialized object needing parsing */ public static Params using(Processor processor) { return new Params(processor); } /** * This method identifies the macro that uses the services of the class. Only the name of the macro is used for * error reporting purposes. The format of the use of this method is usually {@code from(this)}. * * @param macro the macro that is the caller object of this method * @return {@code this} for chaining */ public Params from(Identified macro) { macroName = macro.getId(); return this; } public Params between(String seps) { Objects.requireNonNull(seps); if (seps.length() != 2) { throw new IllegalArgumentException("The argument to method 'between()' has to be a 2-character string. It was '" + seps + "'"); } this.start = seps.charAt(0); this.terminal = seps.charAt(1); return this; } public Params startWith(char start) { this.start = start; return this; } public Params endWith(char terminal) { this.terminal = terminal; return this; } public Params tillEnd() { this.terminal = null; return this; } /** * Specify the parameter holders and also an extra parameter holders in case there is a parameter, which is not used * by this macro. These extra parameters may be passed to another marco that this macro may invoke and then that * macro, instead of the {@link #parse(Input)} method will invoke the {@link #parse(ExtraParams)} methods. * * @param extraParams the holder for the extra parameters. It has to be a variable, as it will be passed on to the * next macro in the chain. It can be created as {@code new Params.ExtraParams()} * @param holders the parameter holders * @return {@code this} */ public Params keys(ExtraParams extraParams, Param... holders) { this.extraParams = extraParams; return keys(holders); } /** * Specify the parameter holders. When the parsing works it stores the values parsed into these parameter holders. * When there is a parameter for which there is no holder an error occurs. * * @param holders the parameter holders * @return {@code this} */ public Params keys(Param... holders) { for (final var holder : holders) { for (final var key : holder.keys()) { if (key != null && this.holders.containsKey(key)) { throw new IllegalArgumentException( "The key '" + key + "' is used multiple times in macro '" + macroName + "'"); } if (key != null) { this.holders.put(key, holder); } } } return this; } /** * Specify the keys that can be used to specify the value. There can be aliases. The first value is the one that can * also be used as a macro name, the rests are alias. If the first value is null then no macro can define the value * for this parameter. * * @param key the array of key and aliases * @param the type of the parameter, can be {@code Integer}, {@code String} or {@code Boolean} * @return a new holder */ public static Param holder(String... key) { Objects.requireNonNull(key); if (key.length == 0 || (key.length == 1 && key[0] == null)) { throw new IllegalArgumentException("Parameter holder has to have at least one name."); } for (int i = 1; i < key.length; i++) { if (key[i] == null) { // key[0] may be null, that is OK, no macro can define the parameter throw new IllegalArgumentException("Parameter alias names must not be null."); } } return new javax0.jamal.tools.param.Param<>(key); } /** * Parse the input and collect the parameters in a map. The characters parsed are consumed from the input including * the last new-line or other specified terminating character. *

* The parameters have the format *

{@code
     * key=value
     * key="value"
     * key="""value"""
     * key
     * }
*

* When the string starts with {@code """} it is a multi-line string. In this case the parsing does not stop at the * new line, which is inside the string. *

* New-line characters can als be escaped using a {@code \} character right before the new line outside of strings. * This can be used to increase the readability of the code in case there are many parameters for the macro. *

* String parsing implements all Java string features, including all escape sequences, octal number and unicode * escape sequences. * * @param input the input that starts with the parameters * @throws BadSyntax in many cases. *

    *
  • if a string is not terminated till the end of the input
  • *
  • if a {@code "} separated string is not terminated till the end of the first line
  • *
  • if there is a parameter, which is not listed in the keys
  • *
*/ public void parse(Input input) throws BadSyntax { try { if (extraParams == null) { parse(input, (id, param) -> { holders.get(id).set(param); holders.get(id).setName(id); }, holders::containsKey); } else { parse(input, (id, param) -> { getHolder(id).set(param); getHolder(id).setName(id); }, (id) -> true); } } catch (BadKey e) { final var suggestions = suggest(e.key, holders.keySet()); throw throwExceptionWithSuggestions(suggestions, e); } } private Params.Param getHolder(final String id) { return holders.computeIfAbsent(id, (xId -> extraParams.params.computeIfAbsent(xId, javax0.jamal.tools.param.Param::new))); } public void parse(ExtraParams extraParams) throws BadSyntax { parse(); for (final var e : extraParams.params.entrySet()) { final var id = e.getKey(); if (!holders.containsKey(id)) { if (this.extraParams != null) { getHolder(id).copy(e.getValue()); } else { final var suggestions = suggest(id, holders.keySet()); throw throwExceptionWithSuggestions(suggestions, new BadKey(e.getKey(), "The key '" + id + "' is not used by the macro '" + macroName + "'.")); } } holders.get(e.getKey()).copy(e.getValue()); } } /** * This is a general purpose version of the parameter handling. This method parses the parameters and fills them * into a LinkedHashMap. It is guaranteed that the keys are in the same order as they are in the input. *

* This version of the parsing was added to support parameter parsing of XML tag attributes, which is the same * syntax as the parameters. In this version the parsed values are returned as a map and the holders of the * {@code Params} are not used. * * @param input the input that contains the parameters * @return the linked hash map with the parameters * @throws BadSyntax in the same cases as {@link #parse(Input)} */ public LinkedHashMap fetchParameters(Input input) throws BadSyntax { final var parameters = new LinkedHashMap(); try { parse(input, parameters::put, id -> !parameters.containsKey(id)); } catch (BadKey e) { final var suggestions = suggest(e.key, parameters.keySet()); throw throwExceptionWithSuggestions(suggestions, e); } return parameters; } /** * Throws a BadSyntax exception fetching the erroneous key from the {@link BadKey} exception and composing a * suggestion set. *

* If the suggestions set is empty, the exception is thrown without suggestions. * * @param suggestions the set of suggestions * @param e the BadKey exception * @return does not return, but declared to return a BadSyntax exception, so that the caller can "throw" it, and * it helps the data flow analysis of the compiler. * @throws BadSyntax is the composed exception */ private BadSyntax throwExceptionWithSuggestions(final Set suggestions, final BadKey e) throws BadSyntax { if (suggestions.isEmpty()) { throw e; } else { throw new BadSyntax("The key '" + e.key + "' is not used by the macro '" + macroName + "'. Did you mean " + suggestions .stream() .map(s -> "'" + s + "'") .collect(Collectors.joining(", ")) + "?"); } } /** * Get the suggestions that are similar to the given key. * * @param spelling the misspelled key * @param keys the set of the valid keys * @return the set of suggestions */ private Set suggest(String spelling, Set keys) { final Set suggestions = new HashSet<>(); int minDistance = 3; for (String key : keys) { final int distance = Levenshtein.distance(key, spelling); if (distance < minDistance) { minDistance = distance; suggestions.clear(); } if (distance <= minDistance) { suggestions.add(key); } } return suggestions; } public void parse(Input input, BiConsumer store, Predicate valid) throws BadSyntax { parse(); skipStartingSpacesAndEscapedTerminal(input); if (start != null) { if (firstCharIs(input, start)) { skip(input, 1); } else { return; } } while ((terminal == null || !firstCharIs(input, terminal)) && input.length() > 0) { if (terminal != null && firstCharIs(input, terminal)) { break; } skipSpacesAndEscapedTerminal(input); final var id = fetchId(input); if (!valid.test(id)) { throw new BadKey(id, "The key '" + id + "' is not used by the macro '" + macroName + "'."); } final String param; skipSpacesAndEscapedTerminal(input); if (firstCharIs(input, '=')) { skip(input, 1); skipSpacesAndEscapedTerminal(input); param = StringFetcher.getString(input, terminal); } else { param = "true"; } store.accept(id, param); skipSpacesAndEscapedTerminal(input); } skip(input, 1); } public void parse() { for (final var holder : holders.values()) { holder.inject(processor, macroName); } } /** * Skip all the characters for which the {@code skipper} predicate returns true. * If the next character after all the skipped characters are {@code \} and {@code \n} (escaped new line) then * step over these and start the skipping again. *

* Skipping a character means deleting it from the front of theinput. * * @param input to input to delete the characters from * @param skipper the predicate deciding if a chavater is to be skipped or not */ private static void skipper(Input input, Predicate skipper) { while (true) { while (skipper.test(input)) { input.delete(1); } if (startsWith(input, "\\\n") != -1) { skip(input, 2); } else { return; } } } /** * Skip all characters, also new line characters, except when the terminal character is newline. In that case * the terminal character (which is a new line) stops the skipping. This method is used to eat the extra characters * between parameters. * * @param input the input that contains the next parameters with spaces optionally in front of them */ private void skipSpacesAndEscapedTerminal(Input input) { skipper(input, i -> i.length() > 0 && Character.isWhitespace(i.charAt(0)) && !Objects.equals(i.charAt(0), terminal)); } /** * Skip all the white space characters except new line. This method is invoked before the parameters. * An \ escaped newline character does not stop the skipping. * * @param input that contains the parameters with spaces optionally in front of them */ private void skipStartingSpacesAndEscapedTerminal(Input input) { skipper(input, i -> i.length() > 0 && Character.isWhitespace(i.charAt(0)) && !Objects.equals(i.charAt(0), '\n')); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy