javax0.jamal.java.JavaSourceMacro Maven / Gradle / Ivy
Show all versions of jamal-java Show documentation
package javax0.jamal.java;
import com.javax0.sourcebuddy.Compiler;
import com.javax0.sourcebuddy.Fluent;
import javax0.jamal.api.BadSyntax;
import javax0.jamal.api.Identified;
import javax0.jamal.api.Input;
import javax0.jamal.api.Macro;
import javax0.jamal.api.Processor;
import javax0.jamal.tools.Scanner;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Macros that allow Java code included in the source code to define built-in macros using Java.
*/
public class JavaSourceMacro {
/**
* The namespace of the macros in this class. JBIM stands for Java Built-In Macros.
*/
// snipline MACRO_NS filter="(.*?)"
private static final String MACRO_NS = "jbim:";
// snipline DEFAULT_ID filter="(.*?)"
private static final String DEFAULT_ID = MACRO_NS + "source";
/**
* An instance of this class will be stored as a macro. It is used when a source class,
* when a macro source, or module info file is added to the source set.
*/
private static class JavaMacroSet implements Identified {
private String moduleInfo;
void setModuleInfo(String moduleInfo) {
if (this.moduleInfo != null)
throw new IllegalStateException(String.format("Module info is already set to %s", this.moduleInfo));
this.moduleInfo = Objects.requireNonNull(moduleInfo);
}
final private Set sources = new HashSet<>();
void addSource(String javaSource) {
sources.add(javaSource);
}
final private Set macroSources = new HashSet<>();
void addMacroSource(String macroSource) {
macroSources.add(macroSource);
}
private final String id;
private JavaMacroSet(final String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
}
private static JavaMacroSet getJavaMacroSet(final Processor processor, final String name) {
return processor.getRegister().getUserDefined(name).filter(JavaMacroSet.class::isInstance).map(JavaMacroSet.class::cast)
.orElseGet(() -> {
final var holder = new JavaMacroSet(name);
processor.getRegister().define(holder);
return holder;
});
}
/**
* Get the ID parop from the input. If there is no ID parop, then the default ID is returned.
* @param in the input
* @param processor the processor used to process the input
* @param macro the macro that is used to create the scanner
* @return the ID
* @throws BadSyntax if there is a syntax error in the input parsing the parop
*/
private static String getId(final Input in, final Processor processor, Scanner.FirstLine macro) throws BadSyntax {
final var scanner = macro.newScanner(in, processor);
final var id = scanner.str(null, "id").defaultValue(DEFAULT_ID);
scanner.done();
return id.get();
}
/**
* Add a Java source file to the source set. The source file will be part of the code, loaded by the classloader,
* but will not be registered as macro, even if it implements the {@link Macro} interface.
*/
public static class JavaClass implements Macro, Scanner.FirstLine {
@Override
public String evaluate(final Input in, final Processor processor) throws BadSyntax {
final String name = JavaSourceMacro.getId(in, processor, this);
getJavaMacroSet(processor, name).addSource(in.toString());
return "";
}
@Override
public String getId() {
return MACRO_NS + "class";
}
}
/**
* Add a Java source file to the source set. The source file will be part of the code, loaded by the classloader,
* and will be registered as macro. It has to implement the {@link Macro} interface.
*/
public static class JavaMacroClass implements Macro, Scanner.FirstLine {
@Override
public String evaluate(final Input in, final Processor processor) throws BadSyntax {
final String name = JavaSourceMacro.getId(in, processor, this);
getJavaMacroSet(processor, name).addMacroSource(in.toString());
return "";
}
@Override
public String getId() {
return MACRO_NS + "macro";
}
}
/**
* Add the module info file source to the compilation set. If no module info is added then a default module info
* will be created. It will contain 'requires' for the Jamal module {@code jamal.api} and exports the packages
* that contain a macro.
*
* If the specified module info is zero length then no module info file will be created.
*/
@Macro.Name({MACRO_NS + "moduleInfo", MACRO_NS + "moduleinfo"})
public static class JavaModuleInfo implements Macro, Scanner.FirstLine {
@Override
public String evaluate(final Input in, final Processor processor) throws BadSyntax {
final String name = JavaSourceMacro.getId(in, processor, this);
getJavaMacroSet(processor, name).setModuleInfo(in.toString());
return "";
}
}
/**
* Compile the Java source files that were added to the source set. The compilation is done in memory, the
* compiled classes are not written to the file system. After the compilation the classes are loaded by the
* class loader and the macro classes are registered.
*/
public static class JavaCompile implements Macro, Scanner.FirstLine {
@Override
public String evaluate(final Input in, final Processor processor) throws BadSyntax {
final String name = JavaSourceMacro.getId(in, processor, this);
final var set = getJavaMacroSet(processor, name);
try {
final var compiler = Compiler.java();
final var nameSet = new HashSet();
final var pckgSet = new HashSet();
for (final var src : set.macroSources) {
compiler.from(src);
final var className = Compiler.getBinaryNameFromSource(src);
nameSet.add(className);
int i = className.lastIndexOf('.');
if (i > 0) {
pckgSet.add(className.substring(0, i));
}
}
if (set.moduleInfo != null) {
if (set.moduleInfo.trim().isEmpty()) {
addDefaultModuleInfo(compiler, pckgSet);
} else {
compiler.from("module-info", set.moduleInfo);
}
} else {
doNotAddModuleInfo();
}
for (final var src : set.sources) {
compiler.from(src);
}
final var loader = compiler.annotatedClasses().compile()
.load(Compiler.LoaderOption.REVERSE);//even if there is a class with the same name in the classpath
final var register = processor.getRegister();
for (final var nm : nameSet) {
final var macro = loader.newInstance(nm, Macro.class);
register.define(macro);
}
// delete the user defined macro replacing it with an empty set
register.define(new JavaMacroSet(name));
return "";
} catch (Exception e) {
throw new BadSyntax("There was an exception compiling the Java sources", e);
}
}
@Override
public String getId() {
return MACRO_NS + "load";
}
}
/**
* Do not add the specified or default module info to the compiler. This method is invoked when there was no
* module info specified.
*/
private static void doNotAddModuleInfo() {
}
private static void addDefaultModuleInfo(final Fluent.AddSource compiler, final HashSet pckgSet) {
// snippet addDefaultModuleInfo
compiler.from("module-info", "module A" + System.currentTimeMillis() + " {\n" +
" requires jamal.api;" +
pckgSet.stream().map(s -> " exports " + s + ";\n").collect(Collectors.joining("\n")) +
"}");
// end snippet
}
}