javax0.jamal.tools.Scanner 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.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Macros that use parameters can implement this interface to get the parameters parsed in an easier way.
*
* The introduction of this interface was motivated by the fact that the macro definitions had to specify
* parameters to the scanning redundantly. After calling the {@code Scan.using()} method they had to call
* {@code from(this)} to pass the current instance of the macro to the scanner.
*
* In addition to that, the call also had to list all the parameter variables so that the scanner could parse
* the values and store them into the different objects. It was checked during run time and threw an exception
* when an object not passed to the parser was used, but many times this error was triggered during the development.
*
* When using this interface, the macro parameter handling is simplified. The macro is supposed to "implement" this
* interface, or one of the sub interfaces. These also provide a declarative way to specify the delimiters of the
* parameters. The scanner is created by calling the {@code newScanner()} method of the macro. The scanner object
* returned has methods to specify the parameters.
*
* When a parameter is returned, the scanner object also stores it in a list. When the {@code done()} method is called
* the scanner object passes the list of parameters to the scanner, and the scanner parses the input and stores the
* values in the parameter objects. This way, there is no need to specify the parameter objects twice, once in the
* macro definition and once in the scanner call.
*
* The delimiter specification works so that the scanner object will use {@code ()} as the delimiters. However, when the
* class implements the {@link FirstLine} interface then the parameter parsing is delimited by a new line character.
* When the class implements the {@link WholeInput} interface then the parameter parsing goes to the end of the input.
* Changing the delimiter is simply changing the interface name the macro implements.
*/
public interface Scanner {
/**
* The object that holds the parsing parameters, like the processor and the input and does the parsing.
* The name of the class is not user-friendly, but it is not supposed to be used by the user. A new instance
* is created by the {@link #newScanner(Input, Processor)} method. Since this method is defined in the surrounding
* interface implemented by the macro there is no need to pass the {@code this} reference to the scanner.
*/
class ScannerObject {
private final Processor processor;
private final Input in;
private final Identified macro;
private Function setDelimiters;
private final ArrayList> params = new ArrayList<>();
private final Map> parmap = new HashMap<>();
private Params.ExtraParams extraParams = null;
public ScannerObject(Processor processor, Input in, Identified macro) {
this.processor = processor;
this.in = in;
this.macro = macro;
}
public Map> getParMap() {
return parmap;
}
/**
* Define an arbitrary type parameter. Use this method if none of the other methods is suitable.
*
* @param keys the name and the aliases of the parameter
* @param the parameter type
* @return the parameter object
*/
public Params.Param param(String... keys) {
final var param = Params.holder(keys);
add(param);
return param;
}
/**
* Define a list of string parameter.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public ListParameter list(String... keys) {
final var param = Params.holder(keys).asBoolean();
add(param);
return new ListParameter(param.asList(String.class));
}
/**
* Define a boolean parameter with the names of an enum class.
*
* @param klass is the enumeration class. The names of the enum constants are used as the aliases of the parameter.
* @return the parameter object
*/
public EnumerationParameter enumeration(Class klass) throws BadSyntax {
if (klass.getEnumConstants().length == 0) {
throw new IllegalArgumentException("The enumeration class " + klass.getName() + " has no constants.");
}
final var keys = new String[klass.getEnumConstants().length + 1];
keys[0] = null; // the first element is the name, the enum names are all aliases
final var i = new Object() {
int i = 1;
};
Arrays.stream(klass.getEnumConstants())
.map(e -> (Enum>) e)
.map(Enum::name).forEach(s -> keys[i.i++] = s);
final var param = Params.holder(keys).asBoolean();
add(param);
return new EnumerationParameter(param, klass);
}
/**
* Define a boolean parameter.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public BooleanParameter bool(String... keys) {
final var param = Params.holder(keys).asBoolean();
add(param);
return new BooleanParameter(param);
}
/**
* Define a string parameter.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public StringParameter str(String... keys) {
final var param = Params.holder(keys).asString();
add(param);
return new StringParameter(param);
}
/**
* Define a file parameter. The file name is resolved relative to the directory of the input.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public FileParameter file(String... keys) {
final var param = Params.holder(keys).asString();
add(param);
return new FileParameter(param.as(s -> FileTools.absolute(in.getReference(), s)));
}
/**
* Define a regular expression pattern parameter.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public PatternParameter pattern(String... keys) {
final var param = Params.holder(keys).asPattern();
add(param);
return new PatternParameter(param);
}
/**
* Define an integer parameter.
*
* @param keys the name and the aliases of the parameter
* @return the parameter object
*/
public IntegerParameter number(String... keys) {
final var param = Params.holder(keys).asInt();
add(param);
return new IntegerParameter(param);
}
private void add(Params.Param param) {
params.add(param);
final var keys = param.keys();
if (keys != null) {
if (keys.length > 0 && keys[0] != null) {
parmap.put(keys[0], param);
} else if (keys.length > 1 && keys[1] != null) {
parmap.put(keys[1], param);
} else {
throw new IllegalArgumentException("Parameter has no name.");
}
}
}
/**
* Signal that the parsing accepts just any parameter and does not check the parameter names.
* These extra parameters are stored in a map and can be queried from the returned object.
*
* @return the extra parameter object
*/
public Params.ExtraParams extra() {
extraParams = new Params.ExtraParams();
return extraParams;
}
/**
* Finish the parameter parsing.
* This method should be called after all the parameters are defined.
*
* @throws BadSyntax if the input does not match the parameter definitions
*/
public void done() throws BadSyntax {
final var p = setDelimiters.apply(Params.using(processor).from(macro));
final Params z;
if (extraParams == null) {
z = p.keys(params.toArray(new Params.Param>[0]));
} else {
z = p.keys(extraParams, params.toArray(new Params.Param>[0]));
}
z.parse(in);
checkMandatoryPresence();
}
/**
* Check that all mandatory parameters are present.
*
* @throws BadSyntax if a mandatory parameter is missing
*/
private void checkMandatoryPresence() throws BadSyntax {
final var missers = params.stream()
.filter(t -> !t.isOptional())
.filter(t -> {
try {
t.get();
return false;
} catch (BadSyntax e) {
return !t.isPresent();
}
})
.map(t -> Arrays.stream(t.keys())
.filter(k -> k != null)
.map(k -> "'" + k + "'")
.collect(Collectors.joining(" or "))
)
.collect(Collectors.joining(","));
if (!missers.isEmpty()) {
throw new BadSyntax("The mandatory parameters " + missers + " are missing for the macro '" + macro.getId() + "'");
}
}
public ScannerObject delimiterSetter(Function setDelimiters) {
this.setDelimiters = setDelimiters;
return this;
}
}
private static ScannerObject nso(Processor processor, Input in, Scanner it) {
if (!(it instanceof Identified)) {
throw new IllegalArgumentException("The Scanner interface can only be used by Macros.");
}
return new ScannerObject(processor, in, (Identified) it);
}
/**
* The scanner interface that uses the {@code ()} characters as delimiters.
*/
default ScannerObject newScanner(Input in, Processor processor) {
return nso(processor, in, this).delimiterSetter(p -> p.between("()"));
}
interface Core extends Scanner {
default ScannerObject newScanner(Input in, Processor processor) {
return nso(processor, in, this).delimiterSetter(p -> p.between("[]"));
}
}
/**
* The scanner interface that uses the first line of the input.
*/
interface FirstLine extends Scanner {
default ScannerObject newScanner(Input in, Processor processor) {
return nso(processor, in, this).delimiterSetter(Function.identity());
}
}
/**
* The scanner interface that uses the whole input.
*/
interface WholeInput extends Scanner {
default ScannerObject newScanner(Input in, Processor processor) {
return nso(processor, in, this).delimiterSetter(Params::tillEnd);
}
}
}