javax0.jamal.api.Processor Maven / Gradle / Ivy
package javax0.jamal.api;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
import java.util.Properties;
/**
* The processor object that can be used to process an input to generate the Jamal output.
*
* Processor instances should not be used by multiple threads. They are not thread safe by design.
*
* A processor is AutoClosable, and it has to be closed.
*
* Creating a processor instance may be expensive consuming significant amount of CPU cycles. Create one in your code
* when you are going to need it.
*/
public interface Processor extends AutoCloseable {
/**
* Get the debugger that is currently configured for the processor.
*
* @return the current debugger
*/
Optional getDebugger();
Optional getDebuggerStub();
/**
* Process the input and result the string after processing all built-in and user defined macros.
*
* @param in the input the processor has to work on.
* @return the string after the processing
* @throws BadSyntax when the input contains something that cannot be processed.
*/
String process(final Input in) throws BadSyntax;
/**
* A convenience method that executes the Jamal process for a String. It may be handy when the processor is
* used to process some input that is not a file.
*
* @param in the input string
* @return the result of the processing
* @throws BadSyntax in case the input contains something that cannot be processed.
*/
default String process(final String in) throws BadSyntax {
throw new IllegalArgumentException("The method process(String) is not supported by this processor");
}
/**
* Get the macro register of this processor. See {@link MacroRegister}
*
* @return the register
*/
MacroRegister getRegister();
/**
* Get the JShell engine that the processor has.
*
* Note that the JShell engine may not be initialized. It initializes automatically the first time when the engine's
* {@link JShellEngine#evaluate(String)} is invoked.
*
* @return the JShell engine
*/
JShellEngine getJShellEngine();
/**
* Create a new user defined macro. The implementation of this method calls the constructor of the matching
* implementation of the user defined macro. The existence of this method loosens the coupling of the user of the
* API and the actual implementation. The code using the implementation may need only to initiate the engine that
* implements this interface, but does not need to access directly the UserDefinedMacro or other interface
* implementations.
*
* NOTE: The invocation of this method creates a new object, but it DOES NOT register the created user defined macro
* in the macro registry. The sole purpose of this method is to decouple the API usage and the implementation.
*
* @param id the identifier (name) of the macro
* @param input the content of the macro
* @param params the parameter names of the macro
* @return the new user defined macro
* @throws BadSyntax in case the parameter names contain each other
*/
UserDefinedMacro newUserDefinedMacro(String id, String input, String... params) throws BadSyntax;
/**
* The same as {@link #newUserDefinedMacro(String, String, String[])} but it can also define when the macro is
* verbatim. The default implementation ignores the verbatim flag. See the note of {@link
* #newUserDefinedMacro(String, String, String[]) newUserDefinedMacro()}
*
* @param id see {@link #newUserDefinedMacro(String, String, String[])}
* @param input see {@link #newUserDefinedMacro(String, String, String[])}
* @param verbatim {@code true} if the result of the macro should not be evaluated
* @param tailParameter {@code true} if the macro should accept a tail parameter
* @param params see {@link #newUserDefinedMacro(String, String, String[])}
* @return see {@link #newUserDefinedMacro(String, String, String[])}
* @throws BadSyntax see {@link #newUserDefinedMacro(String, String, String[])}
*/
default UserDefinedMacro newUserDefinedMacro(String id, String input, boolean verbatim, boolean tailParameter, String... params) throws BadSyntax {
return newUserDefinedMacro(id, input, params);
}
/**
* The same as {@link #newUserDefinedMacro(String, String, String[])} but it can also define when the macro is
* verbatim. The default implementation ignores the verbatim flag. See the note of {@link
* #newUserDefinedMacro(String, String, String[]) newUserDefinedMacro()}
*
* @param id see {@link #newUserDefinedMacro(String, String, String[])}
* @param input see {@link #newUserDefinedMacro(String, String, String[])}
* @param verbatim {@code true} if the result of the macro should not be evaluated
* @param params see {@link #newUserDefinedMacro(String, String, String[])}
* @return see {@link #newUserDefinedMacro(String, String, String[])}
* @throws BadSyntax see {@link #newUserDefinedMacro(String, String, String[])}
*/
default UserDefinedMacro newUserDefinedMacro(String id, String input, boolean verbatim, String... params) throws BadSyntax {
return newUserDefinedMacro(id, input, params);
}
/**
* Create a new user defined script. Read the important comments for {@link #newUserDefinedMacro(String, String,
* String[])}
*
* @param id see {@link #newUserDefinedMacro(String, String, String[])}
* @param scriptType see {@link #newUserDefinedMacro(String, String, String[])}
* @param input see {@link #newUserDefinedMacro(String, String, String[])}
* @param params see {@link #newUserDefinedMacro(String, String, String[])}
* @return see {@link #newUserDefinedMacro(String, String, String[])}
*/
ScriptMacro newScriptMacro(String id, String scriptType, String input, String[] params) throws BadSyntax;
/**
* Register an AutoCloseable closer that has to be closed when the execution is finished.
*
* Some user defined (Java implemented) or built-in macro may create resources that perform some actions
* asynchronous. The typical example is when a macro that creates some external resource starts a separate thread to
* execute the task. This task has to be joined at the end of the processing. The general model is that there is a
* resource that has to be closed. The {@code closer} may be the resource itself or some object that will close the
* resource.
*
* Closing as an operation may be treated fairly liberal. Almost anything can be "closing". The macro
* {@code xmlFormat}, for example, "closes" the operation replacing the final output of Jamal with the XML formatted
* version.
*
* The {@code closer} may also implement the interfaces {@link javax0.jamal.api.Closer.ProcessorAware} or {@link
* javax0.jamal.api.Closer.OutputAware} or both.
* In that case the
* {@link javax0.jamal.api.Closer.ProcessorAware#set(Processor) set(Processor)}
* and/or
* {@link javax0.jamal.api.Closer.OutputAware#set(Input)} set(Input)}
* methods will be called before calling {@link AutoCloseable#close() close()} passing the {@link Processor}
* or the {@link Input} instance holding the final processed output as argument.
*
* Since the call to {@link AutoCloseable#close() close()} comes before {@link Processor#process(Input)} returns the
* output may be altered by the implemented {@link AutoCloseable#close() close()} method. That way a built-in macro
* may implement post-processing logic that works on the whole output.
*
* The sample test {@code javax0.jamal.engine.TestProcessor#testPostProcessor()} in the file {@code
* src/test/java/javax0/jamal/engine/TestProcessor.java} shows an example that converts the whole result to
* uppercase.
*
* The closer objects {@link AutoCloseable#close() close()} method may invoke the injected processors
* {@link Processor#process(Input) process(Input)} method. In this case, however, the processor is already in a
* state closing resources and processing the whole input again will not recursively invoke the closers. After
* the input is processed the invocation of the closers registered in the first round continues. Any closer
* registered during the call to {@link Processor#process(Input) process(Input)} from a closer will be ignored.
*
* Calling this method the macro can register an {@link AutoCloseable} object. The method {@link
* AutoCloseable#close() close()} will be invoked when the method {@link Processor#process(Input)} finishes its top
* level execution. When the method is called in recursive calls from a macro or from any other place the deferred
* resources will not be closed upon return, only when the top level call is to be returned.
*
* The processor implementation guarantees that the processor will invoke the closers in the order registered.
* The processor will never register an already registered closer. In other words, every closer is invoked only
* once. In the order of executions the first registering is relevant. A closer {@code c2} is treated as already
* registered if there is a registered closer {@code c1} so that {@code c1.equals(c2)}.
*
* Note that this method, or any other method of the processor MUST NOT be invoked from other than the main thread
* of the Jamal processing. Even if a macro spawns a new thread the new thread must not do anything with the
* processor.
*
* @param closer the autocloseable object to be closed at the end of the processing.
* @return the registered closer. It may not be the same closer as the argument {@code closer}. If the closer
* was already registered, then the first registered closer will be returned. More formally, if there was a {@code
* closer2} already registered such that {@code closer2.equals(closer)} then {@code closer2} will be returned.
*/
AutoCloseable deferredClose(AutoCloseable closer);
/**
* Get the context object that the embedding application was setting. The context object is a general object and the
* processor does not do anything with it, except that it provides this method for all macros to get access to the
* object.
*
* The implementation of the processor must return the same object during its lifetime.
*
* @return the context object the embedding application set or {@code null} in case the context object was not set.
*/
Context getContext();
/**
* A very simple functional interface that the embedding applications can implement, provide to accommodate log
* messages from the Jama processing.
*/
@FunctionalInterface
interface Logger {
/**
* A logger interface that the embedding application may provide for the processor.
*
* @param level the message level, standard JKD level
* @param pos position, may be null, and the implementation MUST NOT fail if it is null
* @param format the message or the message format to be use in String.format()
* @param params the parameters for the format
*/
void log(final System.Logger.Level level, final Position pos, final String format, final String... params);
}
/**
* @return the logger implementation that was set by the embedding application. There is no method to set the logger
* object, just as there is no metjod to set the context. Both of these objects are application specific and as the
* embedding applications are using a specific implementation of this interface they will use the one that provides
* the possibility to set the logger (context, {@link #getContext}).
*
* The default implementation returns a null logger that just does not log.
*/
default Logger logger() {
return (level, pos, format, params) -> {
};
}
/**
* IOHookResult is the type of the object returned by an IO Hook object {@link FileReader#read(String)} or
* {@link FileWriter#write(String, String)} method.
*/
interface IOHookResult {
enum Type {
IGNORE, // the reader does not care
REDIRECT, // reader identified the final file name, get() returns the name
DONE // reader was reading the file, get() returns the content
}
/**
* Get the type of the result.
*
* @return the result type.
*/
Type type();
/**
* Get the result of the reader.
*
* @return the name of the file to read/write in case of REDIRECT or
* the content of the file in case the result is DONE and the hook was reading.
* In any other cases the method will throw {@link IllegalStateException}.
*/
String get();
default byte[] getBinary() {
throw new RuntimeException("GetBinary for this hook is nNot implemented");
}
/**
* A singleton instance to be returned by FileReader implementations when the file reading is ignored by the
* hook.
*/
IOHookResult IGNORE = new IOHookResult() {
@Override
public Type type() {
return Type.IGNORE;
}
@Override
public String get() {
throw new IllegalStateException("IO hook result was IGNORE, nothing to \"get()\".");
}
@Override
public byte[] getBinary() {
throw new IllegalStateException("IO hook result was IGNORE, nothing to \"getBinary()\".");
}
};
}
class IOHookResultImpl implements IOHookResult {
private final Type type;
private final byte[] content;
public IOHookResultImpl(final Type type, final String content) {
this.type = type;
this.content = content == null ? null : content.getBytes(StandardCharsets.UTF_8);
}
public IOHookResultImpl(final Type type, final byte[] content) {
this.type = type;
this.content = content;
}
@Override
public Type type() {
return type;
}
@Override
public String get() {
return new String(content, StandardCharsets.UTF_8);
}
@Override
public byte[] getBinary() {
return content;
}
}
class IOHookResultDone extends IOHookResultImpl {
public IOHookResultDone(final String content) {
super(Type.DONE, content);
}
public IOHookResultDone() {
super(Type.DONE, (byte[]) null);
}
}
/**
* Use {@code IOHookResultRedirect("fileName)} to redirect the file reading or writing to a different file.
*/
class IOHookResultRedirect extends IOHookResultImpl {
public IOHookResultRedirect(final String content) {
super(Type.REDIRECT, content);
}
}
/**
* A file writer can be set to work with a processor to intercept any file writing operations the macros may make.
* If the writer is set into the processor via the {@link #setFileWriter(FileWriter)} it will be invoked whenever
* a macro wants to write a file. It can be used to implement a special file system or file mapping.
*/
@FunctionalInterface
interface FileWriter {
/**
* Tries to write the file, decides on redirect or do nothing.
*
* @param fileName the original name of the file
* @return the structure containing the result, which is nothing, or final name
*/
IOHookResult write(final String fileName, final String content);
}
void setFileWriter(FileWriter fileWriter);
Optional getFileWriter();
@FunctionalInterface
interface FileReader {
/**
* Tries to read the file, decides on redirect or do nothing.
*
* @param fileName the original name of the file
* @return the structure containing the result, which is nothing, the final name of the file or the content of
* the file
*/
IOHookResult read(final String fileName);
/**
* The processor calls this method in case the result of the reading was {@link IOHookResult.Type#REDIRECT} or
* {@link IOHookResult.Type#IGNORE} after reading the file. When the result is {@link IOHookResult.Type#DONE}
* the method is not invoked because in that case the reader already had access to the content, it does not
* need to get it again.
*
* Note that the processor may invoke this method for the same file multiple times. This happens when the file
* is redirected. For example
*
*
* - Jamal includes the file {@code f1}
* - The read hook redirects it to {@code f2}
* - The processor invokes the read hook again for the file name {@code f2}.
* - The read hook returns {@link IOHookResult.Type#IGNORE}
* - The processor reads the content of the file {@code f3}
* - The processor calls the read hook {@link #set(String, String) set("f3", "...")} with the content
* - The processor calls the read hook {@link #set(String, String) set("f2", "...")} with the content
*
*
* @param fileName the name of the file, which was passed to the {@link #read(String)} method (not the altered
* name returned).
* @param content the content of the file, which was read by the processor.
*/
default void set(final String fileName, final String content) {
}
default void set(final String fileName, final byte[] content) {
}
}
/**
* Set a {@link FileReader} hook to work with a processor to intercept any file reading operations the macros may make.
*
* @param fileReader the file reader
*/
void setFileReader(FileReader fileReader);
/**
* Get the file reader hook.
*
* @return the file reader hook
*/
Optional getFileReader();
/**
* @param id the identifier of the user defined macro
* @return {@code true} if the user defined macro is defined at the current contex and {@code false} otherwise.
*/
default boolean isDefined(String id) {
return getRegister().getUserDefined(id).isPresent();
}
/**
* Define a new user defined macro on the global level. Technically anything can be defined that implements the
* {@link Identified} interface. Usually {@link UserDefinedMacro} is registered using this method.
*
* @param macro the macro to be registered
*/
default void defineGlobal(Identified macro) {
getRegister().global(macro);
}
/**
* Define a new user defined macro on the current scope. Technically anything can be defined that implements the
* {@link Identified} interface. Usually {@link UserDefinedMacro} is registered using this method.
*
* @param macro the macro to be registered
*/
default void define(Identified macro) {
getRegister().define(macro);
}
/**
* This is a convenience method with the default implementation calling to the {@link
* MacroRegister#separators(String, String)} method.
*
* @param openMacro see {@link MacroRegister#separators(String, String)}
* @param closeMacro see {@link MacroRegister#separators(String, String)}
* @throws BadSyntax see {@link MacroRegister#separators(String, String)}
*/
default void separators(String openMacro, String closeMacro) throws BadSyntax {
getRegister().separators(openMacro, closeMacro);
}
Deque EMPTY_DEQUEUE = new ArrayDeque<>();
/**
* @return the current number of errors that were detected in the source file, but were not aborting the evaluation.
*/
default Deque errors() {
return EMPTY_DEQUEUE;
}
/**
* Throw the last exception that was deferred.
*/
default void throwUp() throws BadSyntax {
}
/**
* Convert a Jamal version string to a {@link Runtime.Version}.
*
* The method removes the trailing zero versions, because those are not allowed by the parsing of {@link
* Runtime.Version}.
*
* @param version the version string, probably from the macro argument {@code require}
* @return the parsed version, which can be compared to the current version
*/
static Runtime.Version jamalVersion(final String version) {
return Runtime.Version.parse(version.replaceAll("(?:\\.0+){0,2}\\.0+(-|$)", "$1"));
}
/**
* Load the version property from the properties file and store it into the properties variable {@code version}. The
* properties will contain one property named {@code "version"}.
*
* The implementation loads all the {@code version.properties} files from the classpath and selects the one that
* contains the string {@code "jamal-api"} in the path. This is needed because there are some implementations, like
* the IntelliJ embedding where there is a {@code version.properties} file in the classpath, but it is not the one we want,
* and also it happens sooner in the classpath, so it is loaded first.
*
* @param version the properties that will hold the version property
*/
static void jamalVersion(Properties version) {
try {
final var it = Processor.class.getClassLoader().getResources("version.properties").asIterator();
while (it.hasNext()) {
final var url = it.next();
if (url.getPath().contains("jamal-api")) {
version.load(url.openStream());
}
}
} catch (IOException e) {
throw new IllegalArgumentException("Version information of Jamal cannot be identified.");
}
}
/**
* @return the current Jamal version in the form of a string
*/
static String jamalVersionString() {
final var version = new Properties();
jamalVersion(version);
return version.getProperty("version");
}
/**
* @return the current Jamal version in the form of a {@link Runtime.Version}
*/
static Runtime.Version jamalVersion() {
return jamalVersion(jamalVersionString());
}
}