add j2html plugin

This commit is contained in:
Jörg Prante 2024-05-14 20:01:32 +02:00
parent 324d8498ec
commit 9784dc72d7
45 changed files with 132590 additions and 19 deletions

View file

@ -0,0 +1,34 @@
plugins {
id 'java-gradle-plugin'
alias(libs.plugins.publish)
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'com.gradle.plugin-publish'
apply from: rootProject.file('gradle/compile/groovy.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
dependencies {
api gradleApi()
implementation libs.jsoup
implementation libs.javapoet
testImplementation gradleTestKit()
}
if (project.hasProperty('gradle.publish.key')) {
gradlePlugin {
website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-j2html'
vcsUrl = 'https://xbib.org/joerg/gradle-plugins'
plugins {
j2htmlPlugin {
id = 'org.xbib.gradle.plugin.j2html'
implementationClass = 'org.xbib.gradle.plugin.j2html.J2HtmlPlugin'
version = project.version
description = 'Gradle J2HTML plugin'
displayName = 'Gradle J2HTML plugin'
tags.set(['j2html'])
}
}
}
}

View file

@ -0,0 +1,33 @@
package com.j2html.codegen;
import com.j2html.codegen.generators.SpecializedTagClassCodeGenerator;
import com.j2html.codegen.generators.AttributeInterfaceCodeGenerator;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class App
{
public static void main( String[] args )
{
final Path relPath = Paths.get("../library/src/main/java/j2html/");
final Path absPath = relPath.toAbsolutePath();
System.out.println("writing in "+absPath);
//decide if the files should be
//deleted or generated
final boolean delete = false;
try {
AttributeInterfaceCodeGenerator.generate(absPath, delete);
SpecializedTagClassCodeGenerator.generate(absPath, delete);
//TagCreatorCodeGenerator.print();
} catch (IOException e) {
e.printStackTrace();
}
//don't forget to auto-reformat the generated code.
}
}

View file

@ -0,0 +1,50 @@
package com.j2html.codegen;
import com.j2html.codegen.model.AttrD;
import static com.j2html.codegen.generators.TagCreatorCodeGenerator.containerTags;
import static com.j2html.codegen.generators.TagCreatorCodeGenerator.emptyTags;
import static com.j2html.codegen.model.AttributesList.attributesDescriptive;
import static com.j2html.codegen.model.AttributesList.getCustomAttributesForHtmlTag;
import static java.lang.System.*;
public class Export {
public static void main(String[] args){
for (final String tag : emptyTags()) {
out.print("EMPTY-ELEMENT[");
out.print(tag);
out.print("]");
out.println();
}
for (final String tag : containerTags()) {
out.print("ELEMENT[");
out.print(tag);
out.print("]");
out.println();
}
out.println();
for(AttrD attr : attributesDescriptive()){
if(attr.hasArgument){
out.print("STRING");
}else{
out.print("BOOLEAN");
}
out.print("[");
out.print(attr.attr);
out.print("]");
out.println();
for(String tag : attr.tags){
out.print("ATTRIBUTE[");
out.print(tag);
out.print(":");
out.print(attr.attr);
out.print("]");
out.println();
}
out.println();
}
}
}

View file

@ -0,0 +1,199 @@
package com.j2html.codegen;
import com.j2html.codegen.Model.Node;
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static com.j2html.codegen.Model.Metadata.ON_OFF;
import static com.j2html.codegen.Model.Metadata.SELF_CLOSING;
public class Generator {
public static final ClassName INSTANCE = ClassName.get("j2html.tags", "IInstance");
public static final ClassName TAG = ClassName.get("j2html.tags", "Tag");
public static final ClassName EMPTY_TAG = ClassName.get("j2html.tags", "EmptyTag");
public static final ClassName CONTAINER_TAG = ClassName.get("j2html.tags", "ContainerTag");
public static void main(String... args) throws IOException {
Path path = Paths.get("j2html-codegen", "src", "test", "resources", "html.model");
String definitions = new String(Files.readAllBytes(path));
Model model = new Model();
Parser.parse(definitions, model);
Path dir = Paths.get("/j2html/generated-source");
Files.createDirectories(dir);
generate(dir, "j2html.tags.attributes", "j2html.tags.specialized", model);
}
public static void generate(Path root, String attributePkg, String elementPkg, Model model) throws IOException {
Map<String, JavaFile> attributes = generateAttributePackage(attributePkg, model);
for (JavaFile file : attributes.values()) {
file.writeTo(root);
}
Map<String, JavaFile> elements = generateElementPackage(elementPkg, model, attributes);
for (JavaFile file : elements.values()) {
file.writeTo(root);
}
}
private static Map<String, JavaFile> generateElementPackage(String pkg, Model model, Map<String, JavaFile> attributes) {
Map<String, JavaFile> files = new HashMap<>();
// Convert all elements into classes.
for (Node element : model.elements()) {
ClassName className = ClassName.get(pkg, capitalize(element.name) + "Tag");
TypeSpec.Builder type = defineElementClass(element, className);
// Assign attributes to this element.
for (Node attribute : element.children) {
JavaFile file = attributes.get(attribute.name);
type.addSuperinterface(
ParameterizedTypeName.get(
ClassName.get(file.packageName, file.typeSpec.name),
className
)
);
}
files.put(
element.name,
JavaFile.builder(pkg, type.build())
.skipJavaLangImports(true)
.build()
);
}
return files;
}
private static Map<String, JavaFile> generateAttributePackage(String pkg, Model model) {
Map<String, JavaFile> files = new HashMap<>();
// Convert all attributes into classes.
for (Node attribute : model.attributes()) {
TypeSpec.Builder type = defineAttributeClass(pkg, attribute);
if (attribute.type.equals(Node.Type.STRING)) {
defineStringAttributeMethods(attribute, type);
} else if (attribute.type.equals(Node.Type.BOOLEAN) && !attribute.is(ON_OFF)) {
defineBooleanAttributeMethods(attribute, type);
} else if (attribute.type.equals(Node.Type.BOOLEAN) && attribute.is(ON_OFF)) {
defineOnOffAttributeMethods(attribute, type);
}
files.put(
attribute.name,
JavaFile.builder(pkg, type.build())
.skipJavaLangImports(true)
.build()
);
}
return files;
}
private static TypeSpec.Builder defineElementClass(Node element, ClassName className) {
MethodSpec constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("super(\"" + element.name + "\")")
.build();
TypeSpec.Builder type = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.superclass(
ParameterizedTypeName.get(element.is(SELF_CLOSING) ? EMPTY_TAG : CONTAINER_TAG, className)
)
.addMethod(constructor);
return type;
}
private static TypeSpec.Builder defineAttributeClass(String pkg, Node attribute) {
ClassName name = ClassName.get(pkg, "I" + capitalize(attribute.name));
return TypeSpec.interfaceBuilder(name)
.addSuperinterface(ParameterizedTypeName.get(INSTANCE, TypeVariableName.get("T")))
.addTypeVariable(TypeVariableName.get("T", ParameterizedTypeName.get(TAG, TypeVariableName.get("T"))))
.addModifiers(Modifier.PUBLIC);
}
private static void defineBooleanAttributeMethods(Node attribute, TypeSpec.Builder type) {
MethodSpec with = MethodSpec.methodBuilder(methodName("is", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addStatement("return self().attr(\"" + attribute.name + "\")")
.returns(TypeVariableName.get("T"))
.build();
MethodSpec withCond = MethodSpec.methodBuilder(methodName("withCond", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(TypeName.BOOLEAN, "enable", Modifier.FINAL)
.addStatement("return enable ? self().attr(\"" + attribute.name + "\") : self()")
.returns(TypeVariableName.get("T"))
.build();
type.addMethod(with);
type.addMethod(withCond);
}
private static void defineOnOffAttributeMethods(Node attribute, TypeSpec.Builder type) {
MethodSpec with = MethodSpec.methodBuilder(methodName("is", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addStatement("return self().attr(\"" + attribute.name + "\", \"on\")")
.returns(TypeVariableName.get("T"))
.build();
MethodSpec withCond = MethodSpec.methodBuilder(methodName("withCond", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(TypeName.BOOLEAN, "enable", Modifier.FINAL)
.addStatement("return enable ? self().attr(\"" + attribute.name + "\", \"on\") : self()")
.returns(TypeVariableName.get("T"))
.build();
type.addMethod(with);
type.addMethod(withCond);
}
private static void defineStringAttributeMethods(Node attribute, TypeSpec.Builder type) {
MethodSpec with = MethodSpec.methodBuilder(methodName("with", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(String.class, parameter(attribute), Modifier.FINAL)
.addStatement("return self().attr(\"" + attribute.name + "\", " + parameter(attribute) + ")")
.returns(TypeVariableName.get("T"))
.build();
MethodSpec withCond = MethodSpec.methodBuilder(methodName("withCond", attribute.name))
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(TypeName.BOOLEAN, "enable", Modifier.FINAL)
.addParameter(String.class, parameter(attribute), Modifier.FINAL)
.addStatement("return enable ? self().attr(\"" + attribute.name + "\", " + parameter(attribute) + ") : self()")
.returns(TypeVariableName.get("T"))
.build();
type.addMethod(with);
type.addMethod(withCond);
}
private static String parameter(Node attribute) {
return attribute.name + "_";
}
private static String methodName(String... words) {
String[] camelCase = new String[words.length];
camelCase[0] = words[0];
for (int i = 1; i < words.length; i++) {
camelCase[i] = capitalize(words[i]);
}
return String.join("", camelCase);
}
private static String capitalize(String word) {
return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();
}
}

View file

@ -0,0 +1,16 @@
package com.j2html.codegen;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public final class GeneratorUtil {
public static final void deleteAllFilesInDir(final Path dir) throws IOException {
for(final File file : dir.toFile().listFiles()){
System.out.println("deleting " + file.toPath());
Files.delete(file.toPath());
}
}
}

View file

@ -0,0 +1,171 @@
package com.j2html.codegen;
import java.util.*;
import static com.j2html.codegen.Model.Metadata.ON_OFF;
import static com.j2html.codegen.Model.Metadata.SELF_CLOSING;
import static com.j2html.codegen.Model.Node.Type.*;
public class Model implements Parser.Listener {
private Map<String, Node> elements;
private Map<String, Node> attributes;
public Model() {
elements = new LinkedHashMap<>();
attributes = new LinkedHashMap<>();
}
public Collection<Node> elements(){
return elements.values();
}
public Collection<Node> attributes(){
return attributes.values();
}
public Node addElement(String name) {
return add(ELEMENT, name, elements);
}
public Node addBooleanAttribute(String name) {
return add(BOOLEAN, name, attributes);
}
public Node addStringAttribute(String name) {
return add(STRING, name, attributes);
}
public Node element(String name) {
if (!elements.containsKey(name)) {
throw new NodeDoesNotExist(name);
}
return elements.get(name);
}
public Node attribute(String name) {
if (!attributes.containsKey(name)) {
throw new NodeDoesNotExist(name);
}
return attributes.get(name);
}
private Node add(Node.Type type, String name, Map<String, Node> nodes) {
if (nodes.containsKey(name)) {
throw new NodeAlreadyExists(name);
}
Node node = new Node(type, name);
nodes.put(name, node);
return node;
}
@Override
public void lineCommented(int line, String txt) {
// Ignore.
}
@Override
public void elementDefined(int line, String name) {
attempt(() -> addElement(name), line);
}
@Override
public void emptyElementDefined(int line, String name) {
attempt(() -> addElement(name).annotate(SELF_CLOSING), line);
}
@Override
public void booleanDefined(int line, String name) {
attempt(() -> addBooleanAttribute(name), line);
}
@Override
public void onOffDefined(int line, String name) {
attempt(() -> addBooleanAttribute(name).annotate(ON_OFF), line);
}
@Override
public void stringDefined(int line, String name) {
attempt(() -> addStringAttribute(name), line);
}
@Override
public void attributeDefined(int line, String element, String name) {
attempt(() -> element(element).addChild(attribute(name)), line);
}
@Override
public void invalidLine(int line, String txt) {
throw new RuntimeException("Invalid line [" + line + "]: " + txt);
}
@FunctionalInterface
private interface Unsafe {
void call() throws RuntimeException;
}
private void attempt(Unsafe operation, int line) {
try {
operation.call();
} catch (RuntimeException e) {
throw new InvalidModel(e, line);
}
}
public static class Node {
enum Type {
ELEMENT,
BOOLEAN,
STRING
}
public final Type type;
public final String name;
public final List<Metadata> metadata;
public final List<Node> children;
private Node(Type type, String name) {
this.type = type;
this.name = name;
this.metadata = new ArrayList<>();
this.children = new ArrayList<>();
}
public void annotate(Metadata meta) {
metadata.add(meta);
}
public void addChild(Node node) {
children.add(node);
}
public boolean is(Metadata annotation){
return metadata.contains(annotation);
}
}
public enum Metadata {
SELF_CLOSING,
ON_OFF,
OBSOLETE
}
public static class InvalidModel extends RuntimeException {
public InvalidModel(Exception cause, int line) {
super(cause.getMessage() + ". At line " + line, cause);
}
}
public static class NodeAlreadyExists extends RuntimeException {
public NodeAlreadyExists(String name) {
super("Node already exists: " + name);
}
}
public static class NodeDoesNotExist extends RuntimeException {
public NodeDoesNotExist(String name) {
super("Node does not exist: " + name);
}
}
}

View file

@ -0,0 +1,91 @@
package com.j2html.codegen;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Parser {
private static final Pattern EMPTY_LINE_PATTERN = Pattern.compile("\\s*");
private static final Pattern COMMENT_PATTERN = Pattern.compile("#.*");
private static final Pattern NODE_PATTERN = Pattern.compile("(?<type>ELEMENT|EMPTY-ELEMENT|BOOLEAN|ONOFF|STRING)\\[(?<name>\\S+)\\]");
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("ATTRIBUTE\\[(?<element>\\S+):(?<name>\\S+)\\]");
public interface Listener {
void lineCommented(int line, String txt);
void elementDefined(int line, String name);
void emptyElementDefined(int line, String name);
void booleanDefined(int line, String name);
void onOffDefined(int line, String name);
void stringDefined(int line, String name);
void attributeDefined(int line, String element, String name);
void invalidLine(int line, String txt);
}
public static void parse(String txt, Listener listener) {
String[] lines = txt.split("[\r\n]+");
for (int i = 0; i < lines.length; i++) {
int number = i + 1;
String line = lines[i];
if (match(EMPTY_LINE_PATTERN, line)) continue;
if (match(COMMENT_PATTERN, line, matcher -> {
listener.lineCommented(number, line);
})) continue;
if (match(NODE_PATTERN, line, matcher -> {
String type = matcher.group("type");
String name = matcher.group("name");
switch (type) {
case "ELEMENT":
listener.elementDefined(number, name);
break;
case "EMPTY-ELEMENT":
listener.emptyElementDefined(number, name);
break;
case "BOOLEAN":
listener.booleanDefined(number, name);
break;
case "ONOFF":
listener.onOffDefined(number, name);
break;
case "STRING":
listener.stringDefined(number, name);
break;
}
})) continue;
if (match(ATTRIBUTE_PATTERN, line, matcher -> {
listener.attributeDefined(
number,
matcher.group("element"),
matcher.group("name")
);
})) continue;
listener.invalidLine(number, line);
}
}
private static boolean match(Pattern pattern, String txt) {
return pattern.matcher(txt).matches();
}
private static boolean match(Pattern pattern, String txt, Consumer<Matcher> onMatch) {
Matcher matcher = pattern.matcher(txt);
if (matcher.matches()) {
onMatch.accept(matcher);
return true;
}
return false;
}
}

View file

@ -0,0 +1,185 @@
package com.j2html.codegen.generators;
import com.j2html.codegen.GeneratorUtil;
import com.j2html.codegen.model.AttrD;
import com.j2html.codegen.model.AttributesList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public final class AttributeInterfaceCodeGenerator {
private static final String relPath = "tags/attributes/";
public static void generate(final Path absPath, final boolean delete) throws IOException {
//delete all files in the directory for fresh generation
final Path dir = Paths.get(absPath.toString(),relPath);
GeneratorUtil.deleteAllFilesInDir(dir);
for (final AttrD attr : AttributesList.attributesDescriptive()) {
final Path path = makePath(attr.attr, absPath);
final String interfaceName = interfaceNameFromAttribute(attr.attr)+"<T extends Tag<T>>";
/*
IFormAction<T extends Tag<T>> extends IInstance<T>
default T withFormAction(String formAction){
return self().attr("formaction", formAction);
}
*/
final String interfaceStr = getInterfaceTemplate(
interfaceName,
Optional.of("IInstance<T>"),
Arrays.asList("j2html.tags.Tag","j2html.tags.IInstance"),
interfaceNameFromAttribute(attr.attr).substring(1),
attr
);
if (!delete) {
System.out.println("writing to "+path);
Files.write(path, interfaceStr.getBytes());
}
}
}
private static String getPackage(){
return "package j2html.tags.attributes;\n";
}
private static String makeReturnTypeAndMethodName(final String name){
return "default "+ "T "+name;
}
private static String getInterfaceTemplate(
final String interfaceName,
final Optional<String> optExtends,
final List<String> imports,
final String interfaceNameSimple,
final AttrD attrD
){
final StringBuilder sb = new StringBuilder();
sb.append(getPackage());
sb.append("\n");
for(String importName : imports){
sb.append("import ").append(importName).append(";\n");
}
sb.append("\n");
sb.append("public interface ")
.append(interfaceName);
optExtends.ifPresent(ext -> sb.append(" extends ").append(ext).append(" "));
sb.append(" {\n");
//interface contents
/*
IFormAction<T extends Tag> extends IInstance<T>
default T withFormAction(String formAction){
return self().attr("formaction", formAction);
}
*/
//IMPORTANT: '_' added as suffix to mitigate problems
//where attributes are java keywords. Just to make it consistent and avoid special cases.
final String attrName = interfaceNameSimple.toLowerCase();
final String paramName = attrName+"_";
//depending on if the attribute has an argument or not,
//generate methods according to the convention in Tag.java
// arg -> with$ATTR(arg), withCond$ATTR(condition, arg)
// no arg -> is$ATTR(), withCond$ATTR(condition)
//append the 'with$ATTR' method
writeAttributeMethod(interfaceNameSimple, attrD, sb, attrName, paramName);
writeAttributeMethodCond(interfaceNameSimple, attrD, sb, attrName, paramName);
sb.append("}\n");
return sb.toString();
}
private static void addAttributeNoArg(final StringBuilder sb, final String attrName){
//generate the code to add an attribute without an argument
//there are some special attributes
//which do take an argument, but where the argument
//is boolean (meaning on/off, yes/no and the like)
sb.append("self().attr(\"");
if (attrName.equals("autocomplete")){
sb.append(attrName).append("\",\"on\"");
} else {
sb.append(attrName).append("\"");
}
sb.append(");\n");
}
private static void writeAttributeMethodCond(String interfaceNameSimple, AttrD attrD, StringBuilder sb, String attrName, String paramName) {
sb.append(makeReturnTypeAndMethodName("withCond"+interfaceNameSimple));
if(attrD.hasArgument){
//add a variant where you can specify the argument
sb.append("(final boolean enable, final String ").append(paramName).append(") {");
sb.append("if (enable){\n");
sb.append("self().attr(\"").append(attrName).append("\", ").append(paramName).append(");\n");
sb.append("}\n");
sb.append("return self();\n");
}else{
//add a variant where you can toggle the attribute
sb.append("(final boolean enable) {");
sb.append("if (enable){\n");
addAttributeNoArg(sb, attrName);
sb.append("}\n");
sb.append("return self();\n");
}
sb.append("}\n");
}
private static void writeAttributeMethod(String interfaceNameSimple, AttrD attrD, StringBuilder sb, String attrName, String paramName) {
sb.append(makeReturnTypeAndMethodName(
((attrD.hasArgument)?"with":"is")+interfaceNameSimple)
);
if(attrD.hasArgument){
//add a variant where you can specify the argument
sb.append("(final String ").append(paramName).append(") {")
.append("return self().attr(\"").append(attrName).append("\", ").append(paramName).append(");\n");
}else{
//add a variant where you can toggle the attribute
sb.append("() {");
addAttributeNoArg(sb, attrName);
sb.append("return self();\n");
}
sb.append("}\n");
}
public static String interfaceNameFromAttribute(String attribute){
String res = attribute.substring(0,1).toUpperCase()+attribute.substring(1);
return "I" + res;
}
private static Path makePath(String tagLowerCase, final Path absPath){
final String filename = interfaceNameFromAttribute(tagLowerCase)+".java";
return Paths.get(absPath.toString(),relPath,filename);
}
}

View file

@ -0,0 +1,166 @@
package com.j2html.codegen.generators;
import com.j2html.codegen.GeneratorUtil;
import com.j2html.codegen.model.AttributesList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.j2html.codegen.generators.TagCreatorCodeGenerator.containerTags;
import static com.j2html.codegen.generators.TagCreatorCodeGenerator.emptyTags;
public final class SpecializedTagClassCodeGenerator {
private static final String relPath = "tags/specialized";
public static void generate(final Path absPath, final boolean delete) throws IOException {
//delete all files in the directory for fresh generation
final Path dir = Paths.get(absPath.toString(),relPath);
GeneratorUtil.deleteAllFilesInDir(dir);
//the delete argument serves to give the possibility
//to delete the classes that were written before
System.out.println("// EmptyTags, generated in " + SpecializedTagClassCodeGenerator.class);
for (final String tag : emptyTags()) {
final String className = classNameFromTag(tag);
final Path path = makePath(absPath,tag);
final List<String> interfaceNames = getInterfaceNamesForTag(tag);
final String classString =
getClassTemplate(
className,
Optional.of("EmptyTag<"+className+">"),
Arrays.asList(
"j2html.tags.EmptyTag",
"j2html.tags.attributes.*"
),
tag,
interfaceNames
);
/*
public InputTag() {
super("input");
}
*/
if(!delete){
System.out.println("writing to "+path);
Files.write(path, classString.getBytes());
}
}
System.out.println("// ContainerTags, generated in " + SpecializedTagClassCodeGenerator.class);
for (final String tag : containerTags()) {
final Path path = makePath(absPath, tag);
final String className = classNameFromTag(tag);
final List<String> interfaceNames = getInterfaceNamesForTag(tag);
final String classString =
getClassTemplate(
className,
Optional.of("ContainerTag<"+className+">"),
Arrays.asList(
"j2html.tags.ContainerTag",
"j2html.tags.attributes.*"
),
tag,
interfaceNames
);
if(delete){
if(Files.exists(path)) {
System.out.println("deleting " + path);
Files.delete(path);
}
}else {
System.out.println("writing to "+path);
Files.write(path, classString.getBytes());
}
}
}
public static String classNameFromTag(String tageNameLowerCase){
String res = tageNameLowerCase.substring(0,1).toUpperCase()+tageNameLowerCase.substring(1);
return res + "Tag";
}
private static Path makePath(final Path absPath, String tagLowerCase){
final String filename = classNameFromTag(tagLowerCase)+".java";
return Paths.get(absPath.toString(),relPath,filename);
}
private static String getPackage(){
return "package j2html.tags.specialized;\n";
}
private static String getClassTemplate(
final String className,
final Optional<String> optExtends,
final List<String> imports,
final String tag,
final List<String> interfaces
){
final StringBuilder sb = new StringBuilder();
sb.append(getPackage());
sb.append("\n");
for(String importName : imports){
sb.append("import ").append(importName).append(";\n");
}
sb.append("\n");
sb.append("public final class ")
.append(className)
.append(" ");
optExtends.ifPresent(ext -> sb.append("extends ").append(ext).append(" "));
//add the 'implements' clause
if(!interfaces.isEmpty()) {
sb.append("\n");
sb.append("implements ");
final List<String> genericInterfaceNames
= interfaces.stream().map(iName -> iName+"<"+className+">")
.collect(Collectors.toList());
sb.append(
String.join(",", genericInterfaceNames)
);
}
sb.append(" {\n");
//class contents
sb.append("public ")
.append(className)
.append("() {")
.append("super(\"").append(tag).append("\");")
.append("}\n");
sb.append("}\n");
return sb.toString();
}
private static List<String> getInterfaceNamesForTag(final String tagNameLowercase){
return AttributesList.getCustomAttributesForHtmlTag(tagNameLowercase)
.stream()
.map(
AttributeInterfaceCodeGenerator::interfaceNameFromAttribute
).collect(Collectors.toList());
}
}

View file

@ -0,0 +1,189 @@
package com.j2html.codegen.generators;
import java.util.Arrays;
import java.util.List;
public final class TagCreatorCodeGenerator {
public static void print() {
System.out.println("// EmptyTags, generated in " + TagCreatorCodeGenerator.class);
for (String tag : emptyTags()) {
final String className = SpecializedTagClassCodeGenerator.classNameFromTag(tag);
final String publicstaticTypeMethod = "public static "+className+" "+tag+" ";
final String castReturn = " return ("+className+") ";
final String construct = " new "+className+"()";
String emptyA1 = publicstaticTypeMethod + "()";
String emptyA2 = "{ return "+construct+"; }";
// Attr shorthands
String emptyB1 = publicstaticTypeMethod + "(Attr.ShortForm shortAttr)";
String emptyB2 = "{ "+castReturn+" Attr.addTo("+construct+", shortAttr); }";
// Print
System.out.println(String.format("%-80s%1s", emptyA1, emptyA2));
System.out.println(String.format("%-80s%1s", emptyB1, emptyB2));
System.out.println();
}
System.out.println("// ContainerTags, generated in " + TagCreatorCodeGenerator.class);
for (String tag : containerTags()) {
final String className = SpecializedTagClassCodeGenerator.classNameFromTag(tag);
final String publicstaticTypeMethod = "public static "+className+" "+tag+" ";
final String castReturn = " return ("+className+") ";
final String construct = " new "+className+"()";
String containerA1 = publicstaticTypeMethod+ "()";
String containerA2 = "{ "+castReturn + construct + "; }";
String containerB1 = publicstaticTypeMethod + "(String text)";
String containerB2 = "{ "+castReturn + construct + ".withText(text); }";
String containerC1 = publicstaticTypeMethod + "(DomContent... dc)";
String containerC2 = "{ "+castReturn + construct+".with(dc); }";
// Attr shorthands
String containerD1 = publicstaticTypeMethod + "(Attr.ShortForm shortAttr)";
String containerD2 = "{ "+castReturn+" Attr.addTo("+construct+", shortAttr); }";
String containerE1 = publicstaticTypeMethod + "(Attr.ShortForm shortAttr, String text)";
String containerE2 = "{ "+castReturn+" Attr.addTo("+construct+".withText(text), shortAttr); }";
String containerF1 = publicstaticTypeMethod + "(Attr.ShortForm shortAttr, DomContent... dc)";
String containerF2 = "{ "+castReturn+" Attr.addTo("+construct+".with(dc), shortAttr); }";
// Print
System.out.println(String.format("%-80s%1s", containerA1, containerA2));
System.out.println(String.format("%-80s%1s", containerB1, containerB2));
System.out.println(String.format("%-80s%1s", containerC1, containerC2));
System.out.println(String.format("%-80s%1s", containerD1, containerD2));
System.out.println(String.format("%-80s%1s", containerE1, containerE2));
System.out.println(String.format("%-80s%1s", containerF1, containerF2));
System.out.println();
}
}
// This is a method that contains all ContainerTags, there is nothing below it
public static List<String> emptyTags() {
return Arrays.asList(
"area",
"base",
"br",
"col",
//"!DOCTYPE html",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
);
}
public static List<String> containerTags() {
return Arrays.asList(
"a",
"abbr",
"address",
"article",
"aside",
"audio",
"b",
"bdi",
"bdo",
"blockquote",
"body",
"button",
"canvas",
"caption",
"cite",
"code",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"html",
"i",
"iframe",
"ins",
"kbd",
"label",
"legend",
"li",
"main",
"map",
"mark",
"menu",
"menuitem",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"slot",
"small",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"template",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"u",
"ul",
"var",
"video"
);
}
}

View file

@ -0,0 +1,94 @@
package com.j2html.codegen.generators;
import com.j2html.codegen.wattsi.AttributeDefinition;
import com.j2html.codegen.wattsi.ElementDefinition;
import com.j2html.codegen.wattsi.WattsiSource;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class WattsiGenerator {
public static void main(String... args) throws IOException {
Path source = Paths.get(args[0]);
Document doc = Jsoup.parse(source.toFile(), "UTF-8", "https://html.spec.whatwg.org/");
WattsiSource wattsi = new WattsiSource(doc);
List<ElementDefinition> elements = wattsi.elementDefinitions();
List<AttributeDefinition> attributes = wattsi.attributeDefinitions();
// for (ElementDefinition element : elements) {
// System.out.println((element.isObsolete() ? "!" : "") + element.name());
// for (AttributeDefinition attribute : attributes) {
// if (attribute.appliesTo(element)) {
// System.out.println(" " + (attribute.isObsolete() ? "!" : "") + attribute.name());
// }
// }
// System.out.println();
// }
for (ElementDefinition element : elements) {
ClassName className = ClassName.get(
"com.j2html",
capitalize(element.name()) + "Tag"
);
TypeSpec.Builder type = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
if (element.isObsolete()) {
type.addAnnotation(Deprecated.class);
}
for (AttributeDefinition attribute : attributes) {
if (attribute.appliesTo(element)) {
String name = methodName("with", attribute.name().split("-"));
MethodSpec.Builder setter = MethodSpec.methodBuilder(name)
.addModifiers(Modifier.PUBLIC)
.returns(className)
.addStatement("return this");
if(attribute.isObsolete()){
setter.addAnnotation(Deprecated.class);
}
type.addMethod(setter.build());
}
}
System.out.println(type.build().toString());
}
// System.out.println(doc.select("dfn"));
}
private static String methodName(String prefix, String... words){
String[] tmp = new String[words.length + 1];
tmp[0] = prefix;
for(int i = 0; i < words.length; i++){
tmp[i+1] = words[i];
}
return methodName(tmp);
}
private static String methodName(String... words){
String[] camelCase = new String[words.length];
camelCase[0] = words[0];
for(int i = 1; i < words.length; i++){
camelCase[i] = capitalize(words[i]);
}
return String.join("", camelCase);
}
private static String capitalize(String word){
return word.substring(0,1).toUpperCase() + word.substring(1).toLowerCase();
}
}

View file

@ -0,0 +1,24 @@
package com.j2html.codegen.model;
public final class AttrD {
//attribute descriptor
public final String attr;
public final boolean hasArgument;
//the html tags that this attribute can be used on
public final String[] tags;
public AttrD(final String attr, boolean hasArgument){
this.attr = attr;
this.hasArgument = hasArgument;
this.tags = new String[]{};
}
public AttrD(final String attr, boolean hasArgument, final String... tags) {
this.attr = attr;
this.hasArgument = hasArgument;
this.tags = tags;
}
}

View file

@ -0,0 +1,182 @@
package com.j2html.codegen.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class AttributesList {
//https://www.w3schools.com/tags/ref_attributes.asp
public static List<String> getCustomAttributesForHtmlTag(final String tagLowercase){
final List<String> attrs = new ArrayList<>();
for(AttrD attrD : attributesDescriptive()){
if(
Arrays.asList(attrD.tags).contains(tagLowercase)
){
attrs.add(attrD.attr);
}
}
return attrs;
}
public static List<AttrD> attributesDescriptive() {
return Arrays.asList(
new AttrD("accept", true, "input"),
//new AttrD("accept-charset","form"), //contains dashes, TODO
//new AttrD("accesskey"), //global attribute
new AttrD("action", true, "form"),
//"align", not supported in HTML5
new AttrD("alt", true, "area","img","input"),
new AttrD("async", false, "script"),
new AttrD("autocomplete", false, "form","input"),
new AttrD("autofocus", false, "button","input","select","textarea"),
new AttrD("autoplay", false, "audio","video"),
//"bgcolor", not supported in HTMTL5
//"border", not supported in HTML5
new AttrD("charset", true, "meta","script"),
new AttrD("checked", false, "input"),
new AttrD("cite", true, "blockquote","del","ins","q"),
//"class" already implemented in Tag.java // global attribute
new AttrD("cols", true, "textarea"),
new AttrD("colspan", true, "td","th"),
new AttrD("content", true, "meta"),
//"contenteditable" global attribute, should be in Tag.java
new AttrD("controls", false, "audio","video"),
new AttrD("coords", true, "area"),
new AttrD("data", true, "object"),
new AttrD("datetime", true, "del","ins","time"),
new AttrD("default", false, "track"),
new AttrD("defer", false, "script"),
//new AttrD("dir"), //global attribute
new AttrD("dirname", true, "input","textarea"),
new AttrD("disabled",false, "button","fieldset","input","optgroup","option","select","textarea"),
new AttrD("download",false, "a","area"),
//new AttrD("draggable") global attribute, should be in Tag.java
new AttrD("enctype", true, "form"),
new AttrD("for", true, "label","output"),
new AttrD("form", true, "button","fieldset","input","label","meter","object","output","select","textarea"),
new AttrD("formaction", true, "button","input"),
new AttrD("headers", true, "td","th"),
new AttrD("height", true, "canvas","embed","iframe","img","input","object","video"),
//new AttrD("hidden"), global attribute
new AttrD("high", true, "meter"),
new AttrD("href", true, "a","area","base","link"),
new AttrD("hreflang", true, "a","area","link"),
//"http-equiv", //TODO: '-' is problematic in code generation
//"id" global attribute, should be in Tag.java
new AttrD("ismap", false, "img"),
new AttrD("kind", true, "track"),
new AttrD("label", true, "track","option","optgroup"),
//"lang" global attribute, should be in Tag.java
new AttrD("list", true, "input"),
new AttrD("loop", false, "audio","video"),
new AttrD("low", true, "meter"),
new AttrD("max", true, "input","meter","progress"),
new AttrD("maxlength", true, "input","textarea"),
new AttrD("media", true, "a","area","link","source","style"),
new AttrD("method", true, "form"),
new AttrD("min", true, "input","meter"),
new AttrD("multiple", false, "input","select"),
new AttrD("muted", false, "video","audio"),
new AttrD("name", true, "button","fieldset","form","iframe","input","map","meta","object","output","param","select","slot","textarea"),
new AttrD("novalidate", false, "form"),
new AttrD("onabort", true, "audio","embed","img","object","video"),
new AttrD("onafterprint", true, "body"),
new AttrD("onbeforeprint", true, "body"),
new AttrD("onbeforeunload", true, "body"),
//new AttrD("onblur"), global attribute
new AttrD("oncanplay", true, "audio","embed","object","video"),
new AttrD("oncanplaythrough", true, "audio","video"),
/* a bunch of event attributes that are on all visible elements (so should be in Tag.java)
"onchange",
"onclick",
"oncontextmenu",
"oncopy",
*/
new AttrD("oncuechange", true, "track"),
/*
"oncut",
...
"ondrop",
*/
new AttrD("ondurationchange", true, "audio","video"),
new AttrD("onemptied", true, "audio","video"),
new AttrD("onended", true, "audio","video"),
new AttrD("onerror", true, "audio","body","embed","img","object","script","style","video"),
//new AttrD("onfocus"),// global attribute
new AttrD("onhashchange", true, "body"),
// ... a bunch of event attributes visible on all elements
new AttrD("onload", true, "body","iframe","img","input","link","script","style"),
new AttrD("onloadeddata", true, "audio","video"),
new AttrD("onloadedmetadata", true, "audio","video"),
new AttrD("onloadstart", true, "audio","video"),
// ... a bunch of event attributes visible on all elements
new AttrD("onoffline", true, "body"),
new AttrD("ononline", true, "body"),
new AttrD("onpagehide", true, "body"),
new AttrD("onpageshow", true, "body"),
//new AttrD("onpaste"),// global attribute
new AttrD("onpause", true, "audio","video"),
new AttrD("onplay", true, "audio","video"),
new AttrD("onplaying", true, "audio","video"),
new AttrD("onpopstate", true, "body"),
new AttrD("onprogress", true, "audio","video"),
new AttrD("onratechange", true, "audio","video"),
new AttrD("onreset", true, "form"),
new AttrD("onresize", true, "body"),
//new AttrD("onscroll"), //global attribute
new AttrD("onsearch", true, "input"),
new AttrD("onseeked", true, "audio","video"),
new AttrD("onseeking", true, "audio","video"),
//new AttrD("onselect"), //global attribute
new AttrD("onstalled", true, "audio","video"),
new AttrD("onstorage", true, "body"),
new AttrD("onsubmit", true, "form"),
new AttrD("onsuspend", true, "audio","video"),
new AttrD("ontimeupdate", true, "audio","video"),
new AttrD("ontoggle", true, "details"),
new AttrD("onunload", true, "body"),
new AttrD("onvolumechanged", true, "audio","video"),
new AttrD("onwaiting", true, "audio","video"),
//new AttrD("onwheel"), //global attribute
new AttrD("open", false, "details"),
new AttrD("optimum", true, "meter"),
new AttrD("pattern", true, "input"),
new AttrD("placeholder", true, "input","textarea"),
new AttrD("poster", true, "video"),
new AttrD("preload", true, "audio","video"),
new AttrD("readonly", false, "input","textarea"),
new AttrD("rel", true, "a","area","form","link"),
new AttrD("required", false, "input","select","textarea"),
new AttrD("reversed", false, "ol"),
new AttrD("rows", true, "textarea"),
new AttrD("rowspan", true, "td","th"),
new AttrD("sandbox", false, "iframe"),
new AttrD("scope", true, "th"),
new AttrD("selected", false, "option"),
new AttrD("shape", true, "area"),
new AttrD("size", true, "input","select"),
new AttrD("sizes", true, "img","link","source"),
new AttrD("span", true, "col","colgroup"),
//new AttrD("spellcheck"), //global attribute
new AttrD("src", true, "audio","embed","iframe","img","input","script","source","track","video"),
new AttrD("srcdoc", true, "iframe"),
new AttrD("srclang", true, "track"),
new AttrD("srcset", true, "img","source"),
new AttrD("start", true, "ol"),
new AttrD("step", true, "input"),
//new AttrD("style"), //global attribute
//new AttrD("tabindex"), //global attribute
new AttrD("target", true, "a","area","base","form"),
//new AttrD("title"), //global attribute
//new AttrD("translate"),// global attribute
new AttrD("type", true, "a","button","embed","input","link","menu","object","script","source","style"),
new AttrD("usemap", true, "img","object"),
new AttrD("value", true, "button","data","input","li","option","meter","progress","param"),
new AttrD("width", true, "canvas","embed","iframe","img","input","object","video"),
new AttrD("wrap", true, "textarea")
);
}
}

View file

@ -0,0 +1,10 @@
package com.j2html.codegen.wattsi;
public interface AttributeDefinition {
String name();
boolean appliesTo(ElementDefinition element);
boolean isObsolete();
}

View file

@ -0,0 +1,11 @@
package com.j2html.codegen.wattsi;
public interface ElementDefinition {
String name();
boolean isSelfClosing();
boolean isObsolete();
}

View file

@ -0,0 +1,209 @@
package com.j2html.codegen.wattsi;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
public class WattsiSource {
private final Document doc;
private final Set<Reference> obsolete = new HashSet<>();
public WattsiSource(Document doc) {
this.doc = doc;
// Find where obsolete elements are defined or referenced.
Elements obsoleteElements = doc.select("p:contains(Elements in the following list are entirely obsolete) + dl");
// Convert definitions into references to record obsolete elements.
obsoleteElements.select("dt > dfn[element]")
.stream()
.map(WattsiElement::new)
.map(WattsiElement::reference)
.forEach(obsolete::add);
// Extract references to record obsolete elements.
obsoleteElements.select("dt > code")
.stream()
.map(Element::childNodes)
.map(Reference::from)
.forEach(obsolete::add);
// Find where obsolete attributes are defined or referenced.
Elements obsoleteAttributes = doc.select("p:contains(The following attributes are obsolete) + dl");
// Convert definitions into references to record obsolete attributes.
obsoleteAttributes.select("dt > dfn[element-attr]").stream()
.map(WattsiAttribute::new)
.map(WattsiAttribute::reference)
.forEach(obsolete::add);
// System.out.println(obsoleteAttributes.select("dt"));
// obsoleteAttributes.select("dt > code").stream()
// .map(Element::childNodes)
// .map(Reference::from)
// .forEach(System.err::println);
// System.out.println(
// doc.select("dfn[obsolete]")
// );
}
public List<ElementDefinition> elementDefinitions() {
return doc.select("dfn[element]").stream()
.map(WattsiElement::new)
.collect(toList());
}
public List<AttributeDefinition> attributeDefinitions() {
return doc.select("dfn[element-attr]").stream()
.map(WattsiAttribute::new)
.collect(toList());
}
public class WattsiElement implements ElementDefinition {
private final Element dfn;
WattsiElement(Element dfn) {
if (!"dfn".equals(dfn.tagName())) {
throw new IllegalArgumentException("Element cannot be defined from: " + dfn);
}
if (!dfn.hasAttr("element")) {
throw new IllegalArgumentException("Does not define an element: " + dfn);
}
if (dfn.childrenSize() != 1) {
throw new IllegalArgumentException("Element cannot have multiple definitions: " + dfn);
}
this.dfn = dfn;
}
private Reference reference() {
return Reference.from(dfn.childNodes());
}
@Override
public String name() {
if (dfn.hasAttr("data-x")) {
return dfn.attr("data-x");
}
return Reference.from(dfn.childNodes()).key;
}
@Override
public boolean isSelfClosing() {
return false;
}
@Override
public boolean isObsolete() {
return obsolete.contains(reference());
}
}
public class WattsiAttribute implements AttributeDefinition {
private final Element dfn;
WattsiAttribute(Element dfn) {
if (!"dfn".equals(dfn.tagName())) {
throw new IllegalArgumentException("Attribute cannot be defined from: " + dfn);
}
if (!dfn.hasAttr("element-attr")) {
throw new IllegalArgumentException("Does not define an attribute: " + dfn);
}
if (dfn.childrenSize() != 1) {
throw new IllegalArgumentException("Attribute cannot have multiple definitions: " + dfn);
}
this.dfn = dfn;
}
private Reference reference() {
return Reference.from(dfn.childNodes());
}
@Override
public String name() {
return reference().text;
}
private List<String> targets() {
if (dfn.hasAttr("for")) {
return Arrays.asList(dfn.attr("for").trim().split(","));
}
return new ArrayList<>();
}
@Override
public boolean appliesTo(ElementDefinition element) {
return targets().contains(element.name());
}
@Override
public boolean isObsolete() {
return obsolete.contains(reference());
}
}
private static class Reference {
private final String key;
private final String text;
Reference(String key, String text) {
this.key = key;
this.text = text;
}
@Override
public String toString() {
return key + "[" + text + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reference reference = (Reference) o;
return key.equals(reference.key);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
public static Reference from(List<Node> nodes) {
if (nodes.stream().allMatch(n -> n instanceof TextNode)) {
String txt = nodes.stream()
.map(n -> (TextNode) n)
.map(TextNode::text)
.collect(Collectors.joining(" "));
return new Reference(txt, txt);
}
for (Node node : nodes) {
if (node instanceof Element) {
Element element = (Element) node;
if (element.is("code") || element.is("span")) {
if (element.hasAttr("data-x")) {
return new Reference(element.attr("data-x").toLowerCase(), element.text());
} else {
return new Reference(element.text().toLowerCase(), element.text());
}
}
}
}
return null;
}
}
}

View file

@ -0,0 +1,16 @@
package com.j2html.codegen;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class AppTest
{
@Test
public void shouldAnswerWithTrue()
{
//dummy, just to conform to the default mvn
//directory layout
assertTrue( true );
}
}

View file

@ -0,0 +1,77 @@
package com.j2html.codegen;
import com.j2html.codegen.generators.TagCreatorCodeGenerator;
import com.j2html.codegen.wattsi.ElementDefinition;
import com.j2html.codegen.wattsi.WattsiSource;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals;
public class CodeGeneratorComplianceTests {
private WattsiSource specification;
@Before
public void setUp() throws IOException {
Path source = Paths.get("src","test","resources","2022-01.wattsi");
Document doc = Jsoup.parse(source.toFile(), "UTF-8", "https://html.spec.whatwg.org/");
specification = new WattsiSource(doc);
}
private Set<String> generatedElements(){
Set<String> elements = new HashSet<>();
elements.addAll(TagCreatorCodeGenerator.emptyTags());
elements.addAll(TagCreatorCodeGenerator.containerTags());
return elements;
}
private Set<String> specifiedElements(WattsiSource source){
Set<String> elements = new HashSet<>();
for(ElementDefinition element : source.elementDefinitions()){
elements.add(element.name());
}
return elements;
}
@Test
@Ignore
// TODO restore this test once a policy has been determined for obsolete elements.
public void all_wattsi_elements_are_defined_in_the_code_generator() {
Set<String> generated = generatedElements();
List<String> undefined = specification.elementDefinitions().stream()
.filter(element -> !element.isObsolete())
.filter(element -> !generated.contains(element.name()))
.map(ElementDefinition::name)
.collect(toList());
assertEquals("HTML elements are missing", emptyList(), undefined);
// Currently missing (and mostly deprecated):
// hgroup
}
@Test
public void only_wattsi_elements_are_defined_in_the_code_generator(){
Set<String> specified = specifiedElements(specification);
List<String> invalid = generatedElements().stream()
.filter(element -> !specified.contains(element))
.collect(toList());
assertEquals("HTML elements are invalid", emptyList(), invalid);
}
}

View file

@ -0,0 +1,69 @@
package com.j2html.codegen;
import org.junit.Test;
import org.mockito.InOrder;
import java.util.function.Consumer;
import static org.mockito.Mockito.*;
public class ParserTest {
private void verifyParsing(String txt, Consumer<Parser.Listener> checks) {
Parser.Listener listener = mock(Parser.Listener.class);
Parser.parse(txt, listener);
checks.accept(listener);
}
@Test
public void an_empty_input_has_no_events() {
verifyParsing("", listener -> {
verifyNoInteractions(listener);
});
}
@Test
public void whitespace_has_no_events() {
verifyParsing(" \t\t\t\t", listener -> {
verifyNoInteractions(listener);
});
}
@Test
public void commented_lines_are_signaled() {
verifyParsing("#Comment 1.\n# Comment B?", listener -> {
InOrder order = inOrder(listener);
order.verify(listener).lineCommented(1, "#Comment 1.");
order.verify(listener).lineCommented(2, "# Comment B?");
});
}
@Test
public void node_definitions_are_signaled() {
verifyParsing("ELEMENT[a]\nEMPTY-ELEMENT[b]\nBOOLEAN[c]\nONOFF[d]\nSTRING[e]", listener -> {
InOrder order = inOrder(listener);
order.verify(listener).elementDefined(1, "a");
order.verify(listener).emptyElementDefined(2, "b");
order.verify(listener).booleanDefined(3, "c");
order.verify(listener).onOffDefined(4, "d");
order.verify(listener).stringDefined(5, "e");
});
}
@Test
public void attribute_definitions_are_signaled() {
verifyParsing("ATTRIBUTE[a:b]", listener -> {
InOrder order = inOrder(listener);
order.verify(listener).attributeDefined(1, "a", "b");
});
}
@Test
public void invalid_lines_are_signaled() {
verifyParsing("lol, I dunno!\nIt Broke...", listener -> {
InOrder order = inOrder(listener);
order.verify(listener).invalidLine(1, "lol, I dunno!");
order.verify(listener).invalidLine(2, "It Broke...");
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,622 @@
EMPTY-ELEMENT[area]
EMPTY-ELEMENT[base]
EMPTY-ELEMENT[br]
EMPTY-ELEMENT[col]
EMPTY-ELEMENT[embed]
EMPTY-ELEMENT[hr]
EMPTY-ELEMENT[img]
EMPTY-ELEMENT[input]
EMPTY-ELEMENT[keygen]
EMPTY-ELEMENT[link]
EMPTY-ELEMENT[meta]
EMPTY-ELEMENT[param]
EMPTY-ELEMENT[source]
EMPTY-ELEMENT[track]
EMPTY-ELEMENT[wbr]
ELEMENT[a]
ELEMENT[abbr]
ELEMENT[address]
ELEMENT[article]
ELEMENT[aside]
ELEMENT[audio]
ELEMENT[b]
ELEMENT[bdi]
ELEMENT[bdo]
ELEMENT[blockquote]
ELEMENT[body]
ELEMENT[button]
ELEMENT[canvas]
ELEMENT[caption]
ELEMENT[cite]
ELEMENT[code]
ELEMENT[colgroup]
ELEMENT[data]
ELEMENT[datalist]
ELEMENT[dd]
ELEMENT[del]
ELEMENT[details]
ELEMENT[dfn]
ELEMENT[dialog]
ELEMENT[div]
ELEMENT[dl]
ELEMENT[dt]
ELEMENT[em]
ELEMENT[fieldset]
ELEMENT[figcaption]
ELEMENT[figure]
ELEMENT[footer]
ELEMENT[form]
ELEMENT[h1]
ELEMENT[h2]
ELEMENT[h3]
ELEMENT[h4]
ELEMENT[h5]
ELEMENT[h6]
ELEMENT[head]
ELEMENT[header]
ELEMENT[html]
ELEMENT[i]
ELEMENT[iframe]
ELEMENT[ins]
ELEMENT[kbd]
ELEMENT[label]
ELEMENT[legend]
ELEMENT[li]
ELEMENT[main]
ELEMENT[map]
ELEMENT[mark]
ELEMENT[menu]
ELEMENT[menuitem]
ELEMENT[meter]
ELEMENT[nav]
ELEMENT[noscript]
ELEMENT[object]
ELEMENT[ol]
ELEMENT[optgroup]
ELEMENT[option]
ELEMENT[output]
ELEMENT[p]
ELEMENT[picture]
ELEMENT[pre]
ELEMENT[progress]
ELEMENT[q]
ELEMENT[rp]
ELEMENT[rt]
ELEMENT[ruby]
ELEMENT[s]
ELEMENT[samp]
ELEMENT[script]
ELEMENT[section]
ELEMENT[select]
ELEMENT[slot]
ELEMENT[small]
ELEMENT[span]
ELEMENT[strong]
ELEMENT[style]
ELEMENT[sub]
ELEMENT[summary]
ELEMENT[sup]
ELEMENT[table]
ELEMENT[tbody]
ELEMENT[td]
ELEMENT[template]
ELEMENT[textarea]
ELEMENT[tfoot]
ELEMENT[th]
ELEMENT[thead]
ELEMENT[time]
ELEMENT[title]
ELEMENT[tr]
ELEMENT[u]
ELEMENT[ul]
ELEMENT[var]
ELEMENT[video]
STRING[accept]
ATTRIBUTE[input:accept]
STRING[action]
ATTRIBUTE[form:action]
STRING[alt]
ATTRIBUTE[area:alt]
ATTRIBUTE[img:alt]
ATTRIBUTE[input:alt]
BOOLEAN[async]
ATTRIBUTE[script:async]
ONOFF[autocomplete]
ATTRIBUTE[form:autocomplete]
ATTRIBUTE[input:autocomplete]
BOOLEAN[autofocus]
ATTRIBUTE[button:autofocus]
ATTRIBUTE[input:autofocus]
ATTRIBUTE[select:autofocus]
ATTRIBUTE[textarea:autofocus]
BOOLEAN[autoplay]
ATTRIBUTE[audio:autoplay]
ATTRIBUTE[video:autoplay]
STRING[charset]
ATTRIBUTE[meta:charset]
ATTRIBUTE[script:charset]
BOOLEAN[checked]
ATTRIBUTE[input:checked]
STRING[cite]
ATTRIBUTE[blockquote:cite]
ATTRIBUTE[del:cite]
ATTRIBUTE[ins:cite]
ATTRIBUTE[q:cite]
STRING[cols]
ATTRIBUTE[textarea:cols]
STRING[colspan]
ATTRIBUTE[td:colspan]
ATTRIBUTE[th:colspan]
STRING[content]
ATTRIBUTE[meta:content]
BOOLEAN[controls]
ATTRIBUTE[audio:controls]
ATTRIBUTE[video:controls]
STRING[coords]
ATTRIBUTE[area:coords]
STRING[data]
ATTRIBUTE[object:data]
STRING[datetime]
ATTRIBUTE[del:datetime]
ATTRIBUTE[ins:datetime]
ATTRIBUTE[time:datetime]
BOOLEAN[default]
ATTRIBUTE[track:default]
BOOLEAN[defer]
ATTRIBUTE[script:defer]
STRING[dirname]
ATTRIBUTE[input:dirname]
ATTRIBUTE[textarea:dirname]
BOOLEAN[disabled]
ATTRIBUTE[button:disabled]
ATTRIBUTE[fieldset:disabled]
ATTRIBUTE[input:disabled]
ATTRIBUTE[optgroup:disabled]
ATTRIBUTE[option:disabled]
ATTRIBUTE[select:disabled]
ATTRIBUTE[textarea:disabled]
BOOLEAN[download]
ATTRIBUTE[a:download]
ATTRIBUTE[area:download]
STRING[enctype]
ATTRIBUTE[form:enctype]
STRING[for]
ATTRIBUTE[label:for]
ATTRIBUTE[output:for]
STRING[form]
ATTRIBUTE[button:form]
ATTRIBUTE[fieldset:form]
ATTRIBUTE[input:form]
ATTRIBUTE[label:form]
ATTRIBUTE[meter:form]
ATTRIBUTE[object:form]
ATTRIBUTE[output:form]
ATTRIBUTE[select:form]
ATTRIBUTE[textarea:form]
STRING[formaction]
ATTRIBUTE[button:formaction]
ATTRIBUTE[input:formaction]
STRING[headers]
ATTRIBUTE[td:headers]
ATTRIBUTE[th:headers]
STRING[height]
ATTRIBUTE[canvas:height]
ATTRIBUTE[embed:height]
ATTRIBUTE[iframe:height]
ATTRIBUTE[img:height]
ATTRIBUTE[input:height]
ATTRIBUTE[object:height]
ATTRIBUTE[video:height]
STRING[high]
ATTRIBUTE[meter:high]
STRING[href]
ATTRIBUTE[a:href]
ATTRIBUTE[area:href]
ATTRIBUTE[base:href]
ATTRIBUTE[link:href]
STRING[hreflang]
ATTRIBUTE[a:hreflang]
ATTRIBUTE[area:hreflang]
ATTRIBUTE[link:hreflang]
BOOLEAN[ismap]
ATTRIBUTE[img:ismap]
STRING[kind]
ATTRIBUTE[track:kind]
STRING[label]
ATTRIBUTE[track:label]
ATTRIBUTE[option:label]
ATTRIBUTE[optgroup:label]
STRING[list]
ATTRIBUTE[input:list]
BOOLEAN[loop]
ATTRIBUTE[audio:loop]
ATTRIBUTE[video:loop]
STRING[low]
ATTRIBUTE[meter:low]
STRING[max]
ATTRIBUTE[input:max]
ATTRIBUTE[meter:max]
ATTRIBUTE[progress:max]
STRING[maxlength]
ATTRIBUTE[input:maxlength]
ATTRIBUTE[textarea:maxlength]
STRING[media]
ATTRIBUTE[a:media]
ATTRIBUTE[area:media]
ATTRIBUTE[link:media]
ATTRIBUTE[source:media]
ATTRIBUTE[style:media]
STRING[method]
ATTRIBUTE[form:method]
STRING[min]
ATTRIBUTE[input:min]
ATTRIBUTE[meter:min]
BOOLEAN[multiple]
ATTRIBUTE[input:multiple]
ATTRIBUTE[select:multiple]
BOOLEAN[muted]
ATTRIBUTE[video:muted]
ATTRIBUTE[audio:muted]
STRING[name]
ATTRIBUTE[button:name]
ATTRIBUTE[fieldset:name]
ATTRIBUTE[form:name]
ATTRIBUTE[iframe:name]
ATTRIBUTE[input:name]
ATTRIBUTE[map:name]
ATTRIBUTE[meta:name]
ATTRIBUTE[object:name]
ATTRIBUTE[output:name]
ATTRIBUTE[param:name]
ATTRIBUTE[select:name]
ATTRIBUTE[slot:name]
ATTRIBUTE[textarea:name]
BOOLEAN[novalidate]
ATTRIBUTE[form:novalidate]
STRING[onabort]
ATTRIBUTE[audio:onabort]
ATTRIBUTE[embed:onabort]
ATTRIBUTE[img:onabort]
ATTRIBUTE[object:onabort]
ATTRIBUTE[video:onabort]
STRING[onafterprint]
ATTRIBUTE[body:onafterprint]
STRING[onbeforeprint]
ATTRIBUTE[body:onbeforeprint]
STRING[onbeforeunload]
ATTRIBUTE[body:onbeforeunload]
STRING[oncanplay]
ATTRIBUTE[audio:oncanplay]
ATTRIBUTE[embed:oncanplay]
ATTRIBUTE[object:oncanplay]
ATTRIBUTE[video:oncanplay]
STRING[oncanplaythrough]
ATTRIBUTE[audio:oncanplaythrough]
ATTRIBUTE[video:oncanplaythrough]
STRING[oncuechange]
ATTRIBUTE[track:oncuechange]
STRING[ondurationchange]
ATTRIBUTE[audio:ondurationchange]
ATTRIBUTE[video:ondurationchange]
STRING[onemptied]
ATTRIBUTE[audio:onemptied]
ATTRIBUTE[video:onemptied]
STRING[onended]
ATTRIBUTE[audio:onended]
ATTRIBUTE[video:onended]
STRING[onerror]
ATTRIBUTE[audio:onerror]
ATTRIBUTE[body:onerror]
ATTRIBUTE[embed:onerror]
ATTRIBUTE[img:onerror]
ATTRIBUTE[object:onerror]
ATTRIBUTE[script:onerror]
ATTRIBUTE[style:onerror]
ATTRIBUTE[video:onerror]
STRING[onhashchange]
ATTRIBUTE[body:onhashchange]
STRING[onload]
ATTRIBUTE[body:onload]
ATTRIBUTE[iframe:onload]
ATTRIBUTE[img:onload]
ATTRIBUTE[input:onload]
ATTRIBUTE[link:onload]
ATTRIBUTE[script:onload]
ATTRIBUTE[style:onload]
STRING[onloadeddata]
ATTRIBUTE[audio:onloadeddata]
ATTRIBUTE[video:onloadeddata]
STRING[onloadedmetadata]
ATTRIBUTE[audio:onloadedmetadata]
ATTRIBUTE[video:onloadedmetadata]
STRING[onloadstart]
ATTRIBUTE[audio:onloadstart]
ATTRIBUTE[video:onloadstart]
STRING[onoffline]
ATTRIBUTE[body:onoffline]
STRING[ononline]
ATTRIBUTE[body:ononline]
STRING[onpagehide]
ATTRIBUTE[body:onpagehide]
STRING[onpageshow]
ATTRIBUTE[body:onpageshow]
STRING[onpause]
ATTRIBUTE[audio:onpause]
ATTRIBUTE[video:onpause]
STRING[onplay]
ATTRIBUTE[audio:onplay]
ATTRIBUTE[video:onplay]
STRING[onplaying]
ATTRIBUTE[audio:onplaying]
ATTRIBUTE[video:onplaying]
STRING[onpopstate]
ATTRIBUTE[body:onpopstate]
STRING[onprogress]
ATTRIBUTE[audio:onprogress]
ATTRIBUTE[video:onprogress]
STRING[onratechange]
ATTRIBUTE[audio:onratechange]
ATTRIBUTE[video:onratechange]
STRING[onreset]
ATTRIBUTE[form:onreset]
STRING[onresize]
ATTRIBUTE[body:onresize]
STRING[onsearch]
ATTRIBUTE[input:onsearch]
STRING[onseeked]
ATTRIBUTE[audio:onseeked]
ATTRIBUTE[video:onseeked]
STRING[onseeking]
ATTRIBUTE[audio:onseeking]
ATTRIBUTE[video:onseeking]
STRING[onstalled]
ATTRIBUTE[audio:onstalled]
ATTRIBUTE[video:onstalled]
STRING[onstorage]
ATTRIBUTE[body:onstorage]
STRING[onsubmit]
ATTRIBUTE[form:onsubmit]
STRING[onsuspend]
ATTRIBUTE[audio:onsuspend]
ATTRIBUTE[video:onsuspend]
STRING[ontimeupdate]
ATTRIBUTE[audio:ontimeupdate]
ATTRIBUTE[video:ontimeupdate]
STRING[ontoggle]
ATTRIBUTE[details:ontoggle]
STRING[onunload]
ATTRIBUTE[body:onunload]
STRING[onvolumechanged]
ATTRIBUTE[audio:onvolumechanged]
ATTRIBUTE[video:onvolumechanged]
STRING[onwaiting]
ATTRIBUTE[audio:onwaiting]
ATTRIBUTE[video:onwaiting]
BOOLEAN[open]
ATTRIBUTE[details:open]
STRING[optimum]
ATTRIBUTE[meter:optimum]
STRING[pattern]
ATTRIBUTE[input:pattern]
STRING[placeholder]
ATTRIBUTE[input:placeholder]
ATTRIBUTE[textarea:placeholder]
STRING[poster]
ATTRIBUTE[video:poster]
STRING[preload]
ATTRIBUTE[audio:preload]
ATTRIBUTE[video:preload]
BOOLEAN[readonly]
ATTRIBUTE[input:readonly]
ATTRIBUTE[textarea:readonly]
STRING[rel]
ATTRIBUTE[a:rel]
ATTRIBUTE[area:rel]
ATTRIBUTE[form:rel]
ATTRIBUTE[link:rel]
BOOLEAN[required]
ATTRIBUTE[input:required]
ATTRIBUTE[select:required]
ATTRIBUTE[textarea:required]
BOOLEAN[reversed]
ATTRIBUTE[ol:reversed]
STRING[rows]
ATTRIBUTE[textarea:rows]
STRING[rowspan]
ATTRIBUTE[td:rowspan]
ATTRIBUTE[th:rowspan]
BOOLEAN[sandbox]
ATTRIBUTE[iframe:sandbox]
STRING[scope]
ATTRIBUTE[th:scope]
BOOLEAN[selected]
ATTRIBUTE[option:selected]
STRING[shape]
ATTRIBUTE[area:shape]
STRING[size]
ATTRIBUTE[input:size]
ATTRIBUTE[select:size]
STRING[sizes]
ATTRIBUTE[img:sizes]
ATTRIBUTE[link:sizes]
ATTRIBUTE[source:sizes]
STRING[span]
ATTRIBUTE[col:span]
ATTRIBUTE[colgroup:span]
STRING[src]
ATTRIBUTE[audio:src]
ATTRIBUTE[embed:src]
ATTRIBUTE[iframe:src]
ATTRIBUTE[img:src]
ATTRIBUTE[input:src]
ATTRIBUTE[script:src]
ATTRIBUTE[source:src]
ATTRIBUTE[track:src]
ATTRIBUTE[video:src]
STRING[srcdoc]
ATTRIBUTE[iframe:srcdoc]
STRING[srclang]
ATTRIBUTE[track:srclang]
STRING[srcset]
ATTRIBUTE[img:srcset]
ATTRIBUTE[source:srcset]
STRING[start]
ATTRIBUTE[ol:start]
STRING[step]
ATTRIBUTE[input:step]
STRING[target]
ATTRIBUTE[a:target]
ATTRIBUTE[area:target]
ATTRIBUTE[base:target]
ATTRIBUTE[form:target]
STRING[type]
ATTRIBUTE[a:type]
ATTRIBUTE[button:type]
ATTRIBUTE[embed:type]
ATTRIBUTE[input:type]
ATTRIBUTE[link:type]
ATTRIBUTE[menu:type]
ATTRIBUTE[object:type]
ATTRIBUTE[script:type]
ATTRIBUTE[source:type]
ATTRIBUTE[style:type]
STRING[usemap]
ATTRIBUTE[img:usemap]
ATTRIBUTE[object:usemap]
STRING[value]
ATTRIBUTE[button:value]
ATTRIBUTE[data:value]
ATTRIBUTE[input:value]
ATTRIBUTE[li:value]
ATTRIBUTE[option:value]
ATTRIBUTE[meter:value]
ATTRIBUTE[progress:value]
ATTRIBUTE[param:value]
STRING[width]
ATTRIBUTE[canvas:width]
ATTRIBUTE[embed:width]
ATTRIBUTE[iframe:width]
ATTRIBUTE[img:width]
ATTRIBUTE[input:width]
ATTRIBUTE[object:width]
ATTRIBUTE[video:width]
STRING[wrap]
ATTRIBUTE[textarea:wrap]

View file

@ -0,0 +1,10 @@
This work is based on
https://github.com/kordamp/jdeps-gradle-plugin
by Andres Almiray
as of Feb 17, 2024
License: Apache 2.0

View file

@ -0,0 +1,32 @@
plugins {
id 'java-gradle-plugin'
alias(libs.plugins.publish)
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'com.gradle.plugin-publish'
apply from: rootProject.file('gradle/compile/groovy.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
dependencies {
api gradleApi()
testImplementation gradleTestKit()
}
if (project.hasProperty('gradle.publish.key')) {
gradlePlugin {
website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-jdeps'
vcsUrl = 'https://xbib.org/joerg/gradle-plugins'
plugins {
jdepsPlugin {
id = 'org.xbib.gradle.plugin.jdeps'
implementationClass = 'org.xbib.gradle.plugin.jdeps.JDepsPlugin'
version = project.version
description = 'Gradle JDeps plugin'
displayName = 'Gradle JDeps plugin'
tags.set(['jdeps'])
}
}
}
}

View file

@ -0,0 +1,38 @@
package org.xbib.gradle.plugin.jdeps
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.JavaCompile
class JDepsPlugin implements Plugin<Project> {
void apply(Project project) {
project.plugins.apply(JavaPlugin)
TaskProvider<JDepsReportTask> report = project.tasks.register('jdepsReport', JDepsReportTask,
new Action<JDepsReportTask>() {
@Override
void execute(JDepsReportTask t) {
t.dependsOn(project.tasks.named('classes'))
t.group = BasePlugin.BUILD_GROUP
t.description = 'Generate a jdeps report on project classes and dependencies'
t.compileJava = project.tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile)
t.javaPluginConvention.set(project.convention.getPlugin(JavaPluginConvention))
t.projectName.set(project.name)
t.reportDir.convention(project.layout.buildDirectory.dir('reports/jdeps'))
t.projectConfigurations = project.configurations
}
})
project.tasks.named('check').configure(new Action<Task>() {
@Override
void execute(Task t) {
t.dependsOn(report)
}
})
}
}

View file

@ -0,0 +1,674 @@
package org.xbib.gradle.plugin.jdeps
import org.gradle.api.DefaultTask
import org.gradle.api.JavaVersion
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.options.Option
import org.gradle.process.ExecOperations
import org.xbib.gradle.plugin.jdeps.util.BooleanState
import org.xbib.gradle.plugin.jdeps.util.DirectoryState
import org.xbib.gradle.plugin.jdeps.util.ListState
import org.xbib.gradle.plugin.jdeps.util.MapState
import org.xbib.gradle.plugin.jdeps.util.SimpleBooleanState
import org.xbib.gradle.plugin.jdeps.util.SimpleDirectoryState
import org.xbib.gradle.plugin.jdeps.util.SimpleListState
import org.xbib.gradle.plugin.jdeps.util.SimpleMapState
import org.xbib.gradle.plugin.jdeps.util.SimpleStringState
import org.xbib.gradle.plugin.jdeps.util.StringState
import javax.inject.Inject
import java.util.regex.Pattern
class JDepsReportTask extends DefaultTask {
private static final Pattern WARNING = Pattern.compile("^(?:Warning:.*)|(?:.+->\\s([a-zA-Z\\.]+)\\s+JDK internal API.*)")
private static final Pattern ERROR = Pattern.compile("^(?:Error:.*)|Exception in thread.*")
@Inject
protected ExecOperations execOperations;
private final BooleanState listDeps
private final BooleanState listReducedDeps
private final BooleanState printModuleDeps
private final BooleanState verbose
private final BooleanState modular
private final BooleanState summary
private final BooleanState profile
private final BooleanState recursive
private final BooleanState jdkinternals
private final BooleanState consoleOutput
private final BooleanState apionly
private final BooleanState failOnWarning
private final BooleanState missingDeps
private final BooleanState ignoreMissingDeps
private final ListState pkgs
private final ListState requires
private final StringState include
private final StringState regex
private final StringState filter
private final ListState configurations
private final ListState classpaths
private final ListState sourceSets
private final StringState multiRelease
private final MapState multiReleaseJars
private final DirectoryState dotOutput
@OutputDirectory
final DirectoryProperty reportDir
@Internal
TaskProvider<JavaCompile> compileJava
@Internal
final Property<JavaPluginConvention> javaPluginConvention
@Internal
final Property<String> projectName
@Internal
ConfigurationContainer projectConfigurations
@Inject
JDepsReportTask(ObjectFactory objects) {
extensions.create('moduleOptions', ModuleOptions)
reportDir = objects.directoryProperty()
projectName = objects.property(String)
javaPluginConvention = objects.property(JavaPluginConvention)
listDeps = SimpleBooleanState.of(this, 'jdeps.list.deps', false)
listReducedDeps = SimpleBooleanState.of(this, 'jdeps.list.reduced.deps', false)
printModuleDeps = SimpleBooleanState.of(this, 'jdeps.print.module.deps', false)
verbose = SimpleBooleanState.of(this, 'jdeps.verbose', false)
modular = SimpleBooleanState.of(this, 'jdeps.modular', false)
summary = SimpleBooleanState.of(this, 'jdeps.summary', false)
profile = SimpleBooleanState.of(this, 'jdeps.profile', false)
recursive = SimpleBooleanState.of(this, 'jdeps.recursive', false)
jdkinternals = SimpleBooleanState.of(this, 'jdeps.jdkinternals', false)
consoleOutput = SimpleBooleanState.of(this, 'jdeps.console.output', true)
apionly = SimpleBooleanState.of(this, 'jdeps.apionly', false)
failOnWarning = SimpleBooleanState.of(this, 'jdeps.fail.on.warning', false)
missingDeps = SimpleBooleanState.of(this, 'jdeps.missing.deps', false)
ignoreMissingDeps = SimpleBooleanState.of(this, 'jdeps.ignore.missing.deps', false)
include = SimpleStringState.of(this, 'jdeps.include', "")
regex = SimpleStringState.of(this, 'jdeps.regex', "")
filter = SimpleStringState.of(this, 'jdeps.filter', "")
pkgs = SimpleListState.of(this, 'jdeps.package', [])
requires = SimpleListState.of(this, 'jdeps.require', [])
configurations = SimpleListState.of(this, 'jdeps.configurations', [])
classpaths = SimpleListState.of(this, 'jdeps.classpaths', [])
sourceSets = SimpleListState.of(this, 'jdeps.sourcesets', ['main'])
multiRelease = SimpleStringState.of(this, 'jdeps.multi.release', 'base')
multiReleaseJars = SimpleMapState.of(this, 'jdeps.multi.release.jars', [:])
dotOutput = SimpleDirectoryState.of(this, 'jdeps.dot.output', (Directory) null)
}
@Option(option = 'list-deps', description = 'Lists the module dependences')
void setListDeps(boolean value) { listDeps.property.set(value) }
@Option(option = 'list-reduced-deps', description = 'Lists the module dependences')
void setListReducedDeps(boolean value) { listReducedDeps.property.set(value) }
@Option(option = 'print-module-deps', description = 'Comma-separated list of module dependences')
void setPrintModuleDeps(boolean value) { printModuleDeps.property.set(value) }
@Option(option = 'verbose', description = 'Print all class level dependences')
void setVerbose(boolean value) { verbose.property.set(value) }
@Option(option = 'modular', description = 'Uses the module path instead of the classpath')
void setModular(boolean value) { modular.property.set(value) }
@Option(option = 'summary', description = 'Print dependency summary only')
void setSummary(boolean value) { summary.property.set(value) }
@Option(option = 'profile', description = 'Show profile containing a package')
void setProfile(boolean value) { profile.property.set(value) }
@Option(option = 'recursive', description = 'Recursively traverse all run-time dependences')
void setRecursive(boolean value) { recursive.property.set(value) }
@Option(option = 'jdkinternals', description = 'Finds class-level dependences on JDK internal APIs')
void setJdkinternals(boolean value) { jdkinternals.property.set(value) }
@Option(option = 'console-output', description = 'Print out report to console')
void setConsoleOutput(boolean value) { consoleOutput.property.set(value) }
@Option(option = 'apionly', description = 'Restrict analysis to APIs')
void setApionly(boolean value) { apionly.property.set(value) }
@Option(option = 'fail-on-warning', description = 'Fails the build if jdeps finds any warnings')
void setFailOnWarning(boolean value) { failOnWarning.property.set(value) }
@Option(option = 'missing-deps', description = 'Finds missing dependences')
void setMissingDeps(boolean value) { missingDeps.property.set(value) }
@Option(option = 'ignore-missing-deps', description = 'Ignore missing dependences')
void setIgnoreMissingDeps(boolean value) { ignoreMissingDeps.property.set(value) }
@Option(option = 'include', description = 'Restrict analysis to classes matching pattern')
void setInclude(String value) { include.property.set(value) }
@Option(option = 'regex', description = 'Finds dependences matching the given pattern')
void setRegex(String value) { regex.property.set(value) }
@Option(option = 'filter', description = 'Filter dependences matching the given pattern')
void setFilter(String value) { filter.property.set(value) }
@Option(option = 'package', description = 'Finds dependences matching the given package name. REPEATABLE')
void setPackage(String value) { pkgs.property.add(value) }
@Option(option = 'require', description = 'Finds dependences matching the given module name. REPEATABLE')
void setRequire(String value) { requires.property.add(value) }
@Option(option = 'configurations', description = 'Configurations to be analyzed')
void setConfigurations(String value) { configurations.property.set(value.split(',').toList()) }
@Option(option = 'classpaths', description = 'Classpaths to be analyzed')
void setClasspaths(String value) { classpaths.property.set(value.split(',').toList()) }
@Option(option = 'sourcesets', description = 'SourceSets to be analyzed')
void setSourceSets(String value) { sourceSets.property.set(value.split(',').toList()) }
@Option(option = 'multi-release', description = 'Set the multi-release level')
void setMultiRelease(String value) { multiRelease.property.set(value) }
@Option(option = 'dot-output', description = 'Destination directory for DOT file output')
void setDotOutput(String value) { dotOutput.property.set(new File(value)) }
@Internal
Property<Boolean> getListDeps() { listDeps.property }
@Input
Provider<Boolean> getResolvedListDeps() { listDeps.provider }
@Internal
Property<Boolean> getListReducedDeps() { listReducedDeps.property }
@Input
Provider<Boolean> getResolvedListReducedDeps() { listReducedDeps.provider }
@Internal
Property<Boolean> getPrintModuleDeps() { printModuleDeps.property }
@Input
Provider<Boolean> getResolvedPrintModuleDeps() { printModuleDeps.provider }
@Internal
Property<Boolean> getVerbose() { verbose.property }
@Input
Provider<Boolean> getResolvedVerbose() { verbose.provider }
@Internal
Property<Boolean> getModular() { modular.property }
@Input
Provider<Boolean> getResolvedModular() { modular.provider }
@Internal
Property<Boolean> getSummary() { summary.property }
@Input
Provider<Boolean> getResolvedSummary() { summary.provider }
@Internal
Property<Boolean> getProfile() { profile.property }
@Input
Provider<Boolean> getResolvedProfile() { profile.provider }
@Internal
Property<Boolean> getRecursive() { recursive.property }
@Input
Provider<Boolean> getResolvedRecursive() { recursive.provider }
@Internal
Property<Boolean> getJdkinternals() { jdkinternals.property }
@Input
Provider<Boolean> getResolvedJdkinternals() { jdkinternals.provider }
@Internal
Property<Boolean> getConsoleOutput() { consoleOutput.property }
@Input
Provider<Boolean> getResolvedConsoleOutput() { consoleOutput.provider }
@Internal
Property<Boolean> getApionly() { apionly.property }
@Input
Provider<Boolean> getResolvedApionly() { apionly.provider }
@Internal
Property<Boolean> getFailOnWarning() { failOnWarning.property }
@Input
Provider<Boolean> getResolvedFailOnWarning() { failOnWarning.provider }
@Internal
Property<Boolean> getMissingDeps() { missingDeps.property }
@Input
Provider<Boolean> getResolvedMissingDeps() { missingDeps.provider }
@Internal
Property<Boolean> getIgnoreMissingDeps() { ignoreMissingDeps.property }
@Input
Provider<Boolean> getResolvedIgnoreMissingDeps() { ignoreMissingDeps.provider }
@Internal
Property<String> getInclude() { include.property }
@Input
@Optional
Provider<String> getResolvedInclude() { include.provider }
@Internal
Property<String> getRegex() { regex.property }
@Input
@Optional
Provider<String> getResolvedRegex() { regex.provider }
@Internal
Property<String> getFilter() { filter.property }
@Input
@Optional
Provider<String> getResolvedFilter() { filter.provider }
@Internal
ListProperty<String> getPackages() { pkgs.property }
@Input
@Optional
Provider<List<String>> getResolvedPackages() { pkgs.provider }
@Internal
ListProperty<String> getRequires() { requires.property }
@Input
@Optional
Provider<List<String>> getResolvedRequires() { requires.provider }
@Internal
ListProperty<String> getConfigurations() { configurations.property }
@Input
@Optional
Provider<List<String>> getResolvedConfigurations() { configurations.provider }
@Internal
ListProperty<String> getClasspaths() { classpaths.property }
@Input
@Optional
Provider<List<String>> getResolvedClasspaths() { classpaths.provider }
@Internal
ListProperty<String> getSourceSets() { sourceSets.property }
@Input
@Optional
Provider<List<String>> getResolvedSourceSets() { sourceSets.provider }
@Internal
Property<String> getMultiRelease() { multiRelease.property }
@Input
Provider<String> getResolvedMultiRelease() { multiRelease.provider }
@Internal
MapProperty<String, String> getMultiReleaseJars() { multiReleaseJars.property }
@Input
@Optional
Provider<Map<String, String>> getResolvedMultiReleaseJars() { multiReleaseJars.provider }
@Internal
DirectoryProperty getDotOutput() { dotOutput.property }
@InputDirectory
@Optional
Provider<Directory> getResolvedDotOutput() { dotOutput.provider }
@TaskAction
void evaluate() {
ModuleOptions moduleOptions = extensions.getByType(ModuleOptions)
String classpath = compileJava.get().classpath.asPath
List<String> compilerArgs = compileJava.get().options.compilerArgs
List<String> commandOutput = []
int explicitCommand = 0
if (resolvedListDeps.get()) {
explicitCommand++
}
if (resolvedListReducedDeps.get()) {
explicitCommand++
}
if (resolvedPrintModuleDeps.get()) {
explicitCommand++
}
if (explicitCommand > 1) {
throw new IllegalArgumentException("--list-deps, --list-reduced-deps, --print-module-deps are mutually exclusive")
}
final List<String> baseCmd = ['jdeps']
if (resolvedSummary.get()) {
if (resolvedMissingDeps.get()) {
throw new IllegalArgumentException("-s, --missing-deps are mutually exclusive")
}
baseCmd << '-s'
}
if (resolvedVerbose.get()) {
baseCmd << '-v'
}
if (resolvedProfile.get()) {
baseCmd << '-P'
}
if (resolvedRecursive.get()) {
baseCmd << '-R'
}
if (resolvedJdkinternals.get()) {
baseCmd << '-jdkinternals'
}
if (resolvedApionly.get()) {
baseCmd << '-apionly'
}
if (getResolvedDotOutput().present) {
baseCmd << '-dotoutput'
baseCmd << resolvedDotOutput.get().asFile.absolutePath
resolvedDotOutput.get().asFile.mkdirs()
}
if (resolvedIgnoreMissingDeps.get()) {
baseCmd << '--ignore-missing-deps'
}
if (JavaVersion.current().java9Compatible) {
if (resolvedMultiRelease.present) {
baseCmd << '--multi-release'
baseCmd << resolvedMultiRelease.get()
}
if (resolvedModular.get()) {
int modulePathIndex = compilerArgs.indexOf('--module-path')
if (modulePathIndex > -1) {
baseCmd << '--module-path'
baseCmd << compilerArgs[modulePathIndex + 1]
} else {
baseCmd << '--module-path'
baseCmd << classpath
}
} else if (classpath) {
baseCmd << '--class-path'
baseCmd << classpath
} else {
int modulePathIndex = compilerArgs.indexOf('--module-path')
if (modulePathIndex > -1) {
baseCmd << '--module-path'
baseCmd << compilerArgs[modulePathIndex + 1]
}
}
if (!moduleOptions.addModules.empty) {
baseCmd << '--add-modules'
baseCmd << moduleOptions.addModules.join(',')
} else {
int addModulesIndex = compilerArgs.indexOf('--add-modules')
if (addModulesIndex > -1) {
baseCmd << '--add-modules'
baseCmd << compilerArgs[addModulesIndex + 1]
}
}
List<String> requires = resolvedRequires.get()
List<String> packages = resolvedPackages.get()
String regex = resolvedRegex.orNull
int exclusive = 0
if (!requires.isEmpty()) {
exclusive++
}
if (!packages.isEmpty()) {
exclusive++
}
if (regex) {
exclusive++
}
if (exclusive > 1) {
throw new IllegalArgumentException("--package, --regex, --require are mutually exclusive")
}
if (resolvedMissingDeps.get()) {
exclusive = 1
if (!packages.isEmpty()) {
exclusive++
}
if (regex) {
exclusive++
}
if (exclusive > 1) {
throw new IllegalArgumentException("--package, --regex, --missing-deps are mutually exclusive")
}
}
requires.each { s ->
baseCmd << '--require'
baseCmd << s
}
packages.each { s ->
baseCmd << '--package'
baseCmd << s
}
if (regex) {
baseCmd << '--regex'
baseCmd << regex
}
if (resolvedFilter.get()) {
String filter = resolvedFilter.get()
if (filter in [':package', ':archive', ':module', ':none']) {
baseCmd << '-filter' + filter
} else {
baseCmd << '-filter'
baseCmd << filter
}
}
}
// compileJava.get().classpath = project.files()
//logger.info("jdeps version is ${executeCommand(['jdeps', '-version'])}")
//Map<String, String> outputs = [:]
resolvedSourceSets.get().each { sc ->
SourceSet sourceSet = javaPluginConvention.get().sourceSets.findByName(sc)
logger.info("Running jdeps on sourceSet ${sourceSet.name}")
sourceSet.output.files.each { File file ->
if (!file.exists()) {
return // skip
}
List<String> cmd = applyExplicitCommand(baseCmd)
logger.info("jdeps command set to ${cmd.join(' ')}")
executeCommandOn(cmd, file.absolutePath)
/*if (output) {
commandOutput << "\nProject: ${projectName.get()}\n${output}".toString()
outputs[sourceSet.name] = output
}*/
List<String> warnings = getWarnings(output)
if (warnings && getResolvedFailOnWarning().get()) {
throw new IllegalStateException("jdeps reported warnings: " +
System.lineSeparator() +
warnings.join(System.lineSeparator()))
}
List<String> errors = getErrors(output)
if (errors) {
throw new IllegalStateException("jdeps reported errors: " +
System.lineSeparator() +
errors.join(System.lineSeparator()))
}
}
}
for (String c : resolvedConfigurations.get()) {
inspectConfiguration(projectConfigurations[c.trim()], baseCmd)
}
for (String c : resolvedClasspaths.get()) {
inspectConfiguration(projectConfigurations[c.trim()], baseCmd)
}
/*if (commandOutput) {
commandOutput = commandOutput.unique()
if (resolvedConsoleOutput.get()) {
println commandOutput.join('\n')
}
File parentFile = reportDir.get().asFile
if (!parentFile.exists()) {
parentFile.mkdirs()
}
File logFile = new File(parentFile, 'jdeps-report.txt')
logFile.append(commandOutput.join('\n'))
String prefix = 'jdeps-'
if (resolvedListDeps.get()) {
prefix = 'list-deps-'
} else if (resolvedListReducedDeps.get()) {
prefix = 'list-reduced-deps-'
} else if (resolvedPrintModuleDeps.get()) {
prefix = 'print-module-deps-'
}
outputs.each { k, v ->
logFile = new File(parentFile, prefix + k + '.txt')
logFile.append(v)
}
}*/
}
private void inspectConfiguration(Configuration configuration,
List<String> baseCmd) {
logger.info("Running jdeps on configuration ${configuration.name}")
configuration.resolve().each { File file ->
if (!file.exists()) {
return // skip
}
List<String> command = new ArrayList<>(baseCmd)
if (JavaVersion.current().java9Compatible) {
String multiReleaseVersion = resolveMultiReleaseVersion(file.name, resolvedMultiReleaseJars.get())
if (multiReleaseVersion) {
command.add(1, multiReleaseVersion)
command.add(1, '--multi-release')
}
}
command = applyExplicitCommand(command)
logger.info("jdeps command set to: ${command.join(' ')} ${file.absolutePath}")
executeCommandOn(command, file.absolutePath)
//if (output) {
//commandOutput << "\nDependency: ${file.name}\n${output}".toString()
//outputs[configuration.name] = output
//}
/*List<String> warnings = getWarnings(output)
if (warnings && getResolvedFailOnWarning().get()) {
throw new IllegalStateException("jdeps reported errors/warnings: " +
System.lineSeparator() +
warnings.join(System.lineSeparator()))
}*/
}
}
private List<String> applyExplicitCommand(List<String> cmd) {
List<String> c = new ArrayList<>(cmd)
String subcommand = ''
if (resolvedListDeps.get()) {
subcommand = '--list-deps'
} else if (resolvedListReducedDeps.get()) {
subcommand = '--list-reduced-deps'
} else if (resolvedPrintModuleDeps.get()) {
subcommand = '--print-module-deps'
}
if (subcommand) {
if (c.contains('--class-path')) {
c.add(c.indexOf('--class-path'), subcommand)
} else if (c.contains('--module-path')) {
c.add(c.indexOf('--module-path'), subcommand)
} else {
c << subcommand
}
}
c
}
private String executeCommandOn(List<String> baseCmd, String path) {
List<String> cmd = []
cmd.addAll(baseCmd)
cmd.add(path)
return executeCommand(cmd)
}
private String executeCommand(List<String> cmd) {
ByteArrayOutputStream out = new ByteArrayOutputStream()
execOperations.exec(e -> {
e.setErrorOutput(out)
e.commandLine(cmd)
})
return out.toString().trim()
}
private static String resolveMultiReleaseVersion(String artifactName, Map<String, String> multiReleaseJars) {
for (Map.Entry<String, String> e : multiReleaseJars.entrySet()) {
if (artifactName.matches(e.key.trim())) {
return e.value.trim()
}
}
null
}
private static List<String> getWarnings(String output) {
List<String> warnings = []
output.eachLine { String line ->
if (WARNING.matcher(line).matches()) {
warnings.add(line)
}
}
warnings
}
private static List<String> getErrors(String output) {
List<String> errors = []
output.eachLine { String line ->
if (ERROR.matcher(line).matches()) {
errors.add(line)
}
}
errors
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.gradle.plugin.jdeps
import groovy.transform.Canonical
@Canonical
class ModuleOptions {
List<String> addModules = []
}

View file

@ -0,0 +1,13 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
interface BooleanState {
Property<Boolean> getProperty()
Provider<Boolean> getProvider()
boolean getValue()
}

View file

@ -0,0 +1,14 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Provider
interface DirectoryState {
DirectoryProperty getProperty()
Provider<Directory> getProvider()
Directory getValue()
}

View file

@ -0,0 +1,13 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
interface ListState {
ListProperty<String> getProperty()
Provider<List<String>> getProvider()
List<String> getValue()
}

View file

@ -0,0 +1,13 @@
package org.xbib.gradle.plugin.jdeps.util;
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Provider
interface MapState {
MapProperty<String, String> getProperty()
Provider<Map<String, String>> getProvider()
Map<String, String> getValue()
}

View file

@ -0,0 +1,100 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.provider.Providers
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import static java.util.Objects.requireNonNull
final class SimpleBooleanState implements BooleanState {
final Property<Boolean> property
final Provider<Boolean> provider
private final Project project
SimpleBooleanState(Project project,
Property<Boolean> property,
Provider<Boolean> provider) {
this.project = requireNonNull(project, "Argument 'project' must not be null")
this.property = requireNonNull(property, "Argument 'property' must not be null")
this.provider = requireNonNull(provider, "Argument 'provider' must not be null")
}
@Override
boolean getValue() {
booleanProvider(project.providers, property, provider, false).get()
}
static SimpleBooleanState of(Task task,
String key,
boolean defaultValue) {
requireNonNull(task, "Argument 'task' must not be null")
Project project = task.project
Property<Boolean> property = project.objects.property(Boolean).convention(Providers.<Boolean>notDefined())
Provider<Boolean> provider = booleanProvider(key, property, project, defaultValue)
new SimpleBooleanState(project, property, provider)
}
static Provider<Boolean> booleanProvider(ProviderFactory providers,
Property<Boolean> property,
Provider<Boolean> provider,
boolean defaultValue) {
providers.provider {
return provider ? provider.getOrElse(property.getOrElse(defaultValue)) : property.getOrElse(defaultValue)
}
}
static Provider<Boolean> booleanProvider(String key,
Property<Boolean> property,
Project project,
boolean defaultValue) {
booleanProvider(toEnv(key), toProperty(key), property, project, defaultValue)
}
static Provider<Boolean> booleanProvider(String envKey,
String propertyKey,
Property<Boolean> property,
Project project,
boolean defaultValue) {
project.providers.provider {
String value = resolveValue(envKey, propertyKey, project)
return !isBlank(value) ? Boolean.parseBoolean(value) : property.getOrElse(defaultValue)
}
}
private static String resolveValue(String envKey, String propertyKey, Project project) {
String value = System.getenv(envKey)
if (isBlank(value)) {
value = System.getProperty(propertyKey)
}
if (isBlank(value)) {
value = project.findProperty(propertyKey) as String
}
return value
}
private static boolean isBlank(String str) {
if (str == null || str.length() == 0) {
return true
}
for (char c : str.toCharArray()) {
if (!Character.isWhitespace(c)) {
return false
}
}
return true
}
private static String toEnv(String key) {
key.toUpperCase().replace('.', '_')
}
private static String toProperty(String key) {
key.uncapitalize().replace('_', '.')
}
}

View file

@ -0,0 +1,107 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.internal.provider.Providers
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import java.nio.file.Paths
import static java.util.Objects.requireNonNull
final class SimpleDirectoryState implements DirectoryState {
final DirectoryProperty property
final Provider<Directory> provider
private final Project project
SimpleDirectoryState(Project project,
DirectoryProperty property,
Provider<Directory> provider) {
this.project = requireNonNull(project, "Argument 'project' must not be null")
this.property = requireNonNull(property, "Argument 'property' must not be null")
this.provider = requireNonNull(provider, "Argument 'provider' must not be null")
}
@Override
Directory getValue() {
directoryProvider(project.providers, property, provider, (Directory) null).get()
}
static SimpleDirectoryState of(Task task, String key, Directory defaultValue) {
requireNonNull(task, "Argument 'task' must not be null")
Project project = task.project
DirectoryProperty property = project.objects.directoryProperty().convention(Providers.<Directory>notDefined())
Provider<Directory> provider = directoryProvider(key, property, project, defaultValue)
new SimpleDirectoryState(project, property, provider)
}
static Provider<Directory> directoryProvider(ProviderFactory providers,
DirectoryProperty property,
Provider<Directory> provider,
Directory defaultValue) {
providers.provider {
return provider ? provider.getOrElse(property.getOrElse(defaultValue)) : property.getOrElse(defaultValue)
}
}
static Provider<Directory> directoryProvider(String key,
DirectoryProperty property,
Project project,
Directory defaultValue) {
directoryProvider(toEnv(key), toProperty(key), property, project, defaultValue)
}
static Provider<Directory> directoryProvider(String envKey,
String propertyKey,
DirectoryProperty property,
Project project,
Directory defaultValue) {
project.providers.provider {
String value = resolveValue(envKey, propertyKey, project)
if (!isBlank(value)) {
DirectoryProperty p = project.objects.directoryProperty()
p.set(Paths.get(value).toFile())
return p.get()
}
return property.getOrElse(defaultValue)
}
}
private static String resolveValue(String envKey, String propertyKey, Project project) {
String value = System.getenv(envKey)
if (isBlank(value)) {
value = System.getProperty(propertyKey)
}
if (isBlank(value)) {
value = project.findProperty(propertyKey) as String
}
return value
}
private static boolean isBlank(String str) {
if (str == null || str.length() == 0) {
return true
}
for (char c : str.toCharArray()) {
if (!Character.isWhitespace(c)) {
return false
}
}
return true
}
private static String toEnv(String key) {
key.toUpperCase().replace('.', '_')
}
private static String toProperty(String key) {
key.uncapitalize().replace('_', '.')
}
}

View file

@ -0,0 +1,108 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.provider.Providers
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import static java.util.Objects.requireNonNull
final class SimpleListState implements ListState {
final ListProperty<String> property
final Provider<List<String>> provider
private final Project project
SimpleListState(Project project,
ListProperty<String> property,
Provider<List<String>> provider) {
this.project = requireNonNull(project, "Argument 'project' must not be null")
this.property = requireNonNull(property, "Argument 'property' must not be null")
this.provider = requireNonNull(provider, "Argument 'provider' must not be null")
}
@Override
List<String> getValue() {
listProvider(project.providers, property, provider, Collections.<String> emptyList()).get()
}
static SimpleListState of(Task task, String key, List<String> defaultValue) {
requireNonNull(task, "Argument 'task' must not be null")
Project project = task.project
ListProperty<String> property = project.objects.listProperty(String).convention(Providers.<List<String>>notDefined())
Provider<List<String>> provider = listProvider(key, property, project, defaultValue)
new SimpleListState(project, property, provider)
}
static Provider<List<String>> listProvider(ProviderFactory providers,
ListProperty<String> property,
Provider<List<String>> provider,
List<String> defaultValue) {
providers.provider {
return provider ? provider.getOrElse(property.getOrElse(defaultValue)) : property.getOrElse(defaultValue)
}
}
static Provider<List<String>> listProvider(String key,
ListProperty<String> property,
Project project,
List<String> defaultValue) {
listProvider(toEnv(key), toProperty(key), property, project, defaultValue)
}
static Provider<List<String>> listProvider(String envKey,
String propertyKey,
ListProperty<String> property,
Project project,
List<String> defaultValue) {
project.providers.provider {
if (property.present) {
return property.get()
}
String value = resolveValue(envKey, propertyKey, project)
if (!isBlank(value)) {
List<String> list = new ArrayList<>()
for (String v : value.split(',')) {
if (!isBlank(v)) list.add(v.trim())
}
return list
}
defaultValue
}
}
private static String resolveValue(String envKey, String propertyKey, Project project) {
String value = System.getenv(envKey)
if (isBlank(value)) {
value = System.getProperty(propertyKey)
}
if (isBlank(value)) {
value = project.findProperty(propertyKey) as String
}
return value
}
private static boolean isBlank(String str) {
if (str == null || str.length() == 0) {
return true
}
for (char c : str.toCharArray()) {
if (!Character.isWhitespace(c)) {
return false
}
}
return true
}
private static String toEnv(String key) {
key.toUpperCase().replace('.', '_')
}
private static String toProperty(String key) {
key.uncapitalize().replace('_', '.')
}
}

View file

@ -0,0 +1,115 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.provider.Providers
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import static java.util.Objects.requireNonNull
final class SimpleMapState implements MapState {
final MapProperty<String, String> property
final Provider<Map<String, String>> provider
private final Project project
SimpleMapState(Project project,
MapProperty<String, String> property,
Provider<Map<String, String>> provider) {
this.project = requireNonNull(project, "Argument 'project' must not be null")
this.property = requireNonNull(property, "Argument 'property' must not be null")
this.provider = requireNonNull(provider, "Argument 'provider' must not be null")
}
@Override
Map<String, String> getValue() {
mapProvider(project.providers, property, provider, Collections.<String, String> emptyMap()).get()
}
static SimpleMapState of(Task task, String key, Map<String, String> defaultValue) {
requireNonNull(task, "Argument 'task' must not be null")
Project project = task.project
MapProperty<String, String> property = project.objects.mapProperty(String, String).convention(Providers.<Map<String, String>>notDefined())
Provider<Map<String, String>> provider = mapProvider(key, property, project, defaultValue)
new SimpleMapState(project, property, provider)
}
static <K, V> Provider<Map<K, V>> mapProvider(ProviderFactory providers,
MapProperty<K, V> property,
Provider<Map<K, V>> provider,
Map<K, V> defaultValue) {
providers.provider {
Map<K, V> map = new LinkedHashMap<>(defaultValue)
if (property.present) {
map.putAll(property.get())
}
if (provider) {
map.putAll(provider.get())
}
return map
}
}
static <K,V> Provider<Map<K, V>> mapProvider(String key,
MapProperty<K, V> property,
Project project,
Map<K, V> defaultValue) {
mapProvider(toEnv(key), toProperty(key), property, project, defaultValue)
}
static <K, V> Provider<Map<K, V>> mapProvider(String envKey,
String propertyKey,
MapProperty<K, V> property,
Project project,
Map<K, V> defaultValue) {
project.providers.provider {
String value = resolveValue(envKey, propertyKey, project)
if (i!sBlank(value)) {
Map<K, V> map = new LinkedHashMap<>()
for (String val : value.split(',')) {
String[] kv = val.split('=')
if (kv.length == 2) {
map.put(kv[0] as K, kv[1] as V)
}
}
return map
}
return property.orElse(defaultValue).get()
}
}
private static String resolveValue(String envKey, String propertyKey, Project project) {
String value = System.getenv(envKey)
if (isBlank(value)) {
value = System.getProperty(propertyKey)
}
if (isBlank(value)) {
value = project.findProperty(propertyKey) as String
}
return value
}
private static boolean isBlank(String str) {
if (str == null || str.length() == 0) {
return true
}
for (char c : str.toCharArray()) {
if (!Character.isWhitespace(c)) {
return false
}
}
return true
}
private static String toEnv(String key) {
key.toUpperCase().replace('.', '_')
}
private static String toProperty(String key) {
key.uncapitalize().replace('_', '.')
}
}

View file

@ -0,0 +1,98 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.provider.Providers
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import static java.util.Objects.requireNonNull
final class SimpleStringState implements StringState {
final Property<String> property
final Provider<String> provider
private final Project project
@Override
String getValue() {
stringProvider(project.providers, property, provider, '').get()
}
SimpleStringState(Project project,
Property<String> property,
Provider<String> provider) {
this.project = requireNonNull(project, "Argument 'project' must not be null")
this.property = requireNonNull(property, "Argument 'property' must not be null")
this.provider = requireNonNull(provider, "Argument 'provider' must not be null")
}
static SimpleStringState of(Task task, String key, String defaultValue) {
requireNonNull(task, "Argument 'task' must not be null")
Project project = task.project
Property<String> property = project.objects.property(String).convention(Providers.<String>notDefined())
Provider<String> provider = stringProvider(key, property, project, defaultValue)
new SimpleStringState(project, property, provider)
}
static Provider<String> stringProvider(ProviderFactory providers,
Property<String> property,
Provider<String> provider,
String defaultValue) {
providers.provider {
return provider ? provider.getOrElse(property.getOrElse(defaultValue)) : property.getOrElse(defaultValue)
}
}
static Provider<String> stringProvider(String key,
Property<String> property,
Project project,
String defaultValue) {
stringProvider(toEnv(key), toProperty(key), property, project, defaultValue)
}
static Provider<String> stringProvider(String envKey,
String propertyKey,
Property<String> property,
Project project,
String defaultValue) {
project.providers.provider {
String value = resolveValue(envKey, propertyKey, project)
return !isBlank(value) ? value : property.getOrElse(defaultValue)
}
}
private static String resolveValue(String envKey, String propertyKey, Project project) {
String value = System.getenv(envKey)
if (isBlank(value)) {
value = System.getProperty(propertyKey)
}
if (isBlank(value)) {
value = project.findProperty(propertyKey) as String
}
return value
}
private static boolean isBlank(String str) {
if (str == null || str.length() == 0) {
return true
}
for (char c : str.toCharArray()) {
if (!Character.isWhitespace(c)) {
return false
}
}
return true
}
private static String toEnv(String key) {
key.toUpperCase().replace('.', '_')
}
private static String toProperty(String key) {
key.uncapitalize().replace('_', '.')
}
}

View file

@ -0,0 +1,13 @@
package org.xbib.gradle.plugin.jdeps.util
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
interface StringState {
Property<String> getProperty()
Provider<String> getProvider()
String getValue()
}

View file

@ -0,0 +1,32 @@
plugins {
id 'java-gradle-plugin'
alias(libs.plugins.publish)
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'com.gradle.plugin-publish'
apply from: rootProject.file('gradle/compile/groovy.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
dependencies {
api gradleApi()
testImplementation gradleTestKit()
}
if (project.hasProperty('gradle.publish.key')) {
gradlePlugin {
website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-jlink'
vcsUrl = 'https://xbib.org/joerg/gradle-plugins'
plugins {
jlinkPlugin {
id = 'org.xbib.gradle.plugin.jlink'
implementationClass = 'org.xbib.gradle.plugin.jlink.JlinkPlugin'
version = project.version
description = 'Gradle Jlink plugin'
displayName = 'Gradle Jlink plugin'
tags.set(['jlink'])
}
}
}
}

View file

@ -0,0 +1,9 @@
This work is based on
https://github.com/gradlex-org/java-module-packaging
by Jendrik Johannes
as of Feb 11, 2024
License: Apache 2.0

View file

@ -12,6 +12,8 @@ apply from: rootProject.file('gradle/test/junit5.gradle')
dependencies { dependencies {
api gradleApi() api gradleApi()
testImplementation gradleTestKit() testImplementation gradleTestKit()
testImplementation testLibs.spock.core
testImplementation testLibs.spock.junit4
} }
if (project.hasProperty('gradle.publish.key')) { if (project.hasProperty('gradle.publish.key')) {
@ -19,7 +21,7 @@ if (project.hasProperty('gradle.publish.key')) {
website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-jpackage' website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-jpackage'
vcsUrl = 'https://xbib.org/joerg/gradle-plugins' vcsUrl = 'https://xbib.org/joerg/gradle-plugins'
plugins { plugins {
jaccPlugin { jpackagePlugin {
id = 'org.xbib.gradle.plugin.jpackage' id = 'org.xbib.gradle.plugin.jpackage'
implementationClass = 'org.xbib.gradle.plugin.jpackage.JPackagePlugin' implementationClass = 'org.xbib.gradle.plugin.jpackage.JPackagePlugin'
version = project.version version = project.version

View file

@ -94,7 +94,7 @@ public abstract class JPackageExtension {
target.getPackageTypes().convention(target.getOperatingSystem().map(os -> switch (os) { target.getPackageTypes().convention(target.getOperatingSystem().map(os -> switch (os) {
case WINDOWS -> Arrays.asList("exe", "msi"); case WINDOWS -> Arrays.asList("exe", "msi");
case MACOS -> Arrays.asList("pkg", "dmg"); case MACOS -> Arrays.asList("pkg", "dmg");
case LINUX -> Arrays.asList("rpm", "deb"); case LINUX -> Arrays.asList("rpm");
default -> Collections.emptyList(); default -> Collections.emptyList();
})); }));
ConfigurationContainer configurations = getProject().getConfigurations(); ConfigurationContainer configurations = getProject().getConfigurations();

View file

@ -1,19 +1,3 @@
/*
* Copyright the GradleX team.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbib.gradle.plugin.jpackage; package org.xbib.gradle.plugin.jpackage;
import org.gradle.api.NonNullApi; import org.gradle.api.NonNullApi;

View file

@ -115,7 +115,9 @@ public abstract class JPackageTask extends DefaultTask {
c.rename(f -> f.replace("icon", getApplicationName().get())); c.rename(f -> f.replace("icon", getApplicationName().get()));
}); });
String executableName = WINDOWS.equals(os) ? "jpackage.exe" : "jpackage"; String executableName = WINDOWS.equals(os) ? "jpackage.exe" : "jpackage";
String jpackage = getJavaInstallation().get().getInstallationPath().file("bin/" + executableName).getAsFile().getAbsolutePath(); String jpackage = getJavaInstallation().get()
.getInstallationPath().file("bin/" + executableName)
.getAsFile().getAbsolutePath();
getExec().exec(e -> { getExec().exec(e -> {
e.commandLine( e.commandLine(
jpackage, jpackage,

View file

@ -0,0 +1,58 @@
package org.xbib.gradle.plugin.jpackage
import org.xbib.gradle.plugin.jpackage.fixture.GradleBuild
import spock.lang.Specification
import static org.gradle.testkit.runner.TaskOutcome.FAILED
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
import static org.xbib.gradle.plugin.jpackage.fixture.GradleBuild.hostOs
import static org.xbib.gradle.plugin.jpackage.fixture.GradleBuild.runsOnLinux
import static org.xbib.gradle.plugin.jpackage.fixture.GradleBuild.runsOnMacos
import static org.xbib.gradle.plugin.jpackage.fixture.GradleBuild.runsOnWindows
class JPackageTest extends Specification {
@Delegate
GradleBuild build = new GradleBuild()
def "can use plugin on #os with success=#success"() {
given:
def taskToRun = ":app:assemble${label.capitalize()}"
def taskToCheck = ":app:jpackage${label.capitalize()}"
def macosArch = System.getProperty('os.arch').contains('aarch') ? 'aarch64' : 'x86-64'
appBuildFile << """
version = "1.0"
jpackage {
target("fedora") {
operatingSystem = "linux"
architecture = "x86-64"
}
target("macos") {
operatingSystem = "macos"
architecture = "$macosArch"
}
target("windows") {
operatingSystem = "windows"
architecture = "x86-64"
}
}
"""
appModuleInfoFile << '''
module org.example.app {
}
'''
when:
def result = success ? build(taskToRun) : fail(taskToRun)
then:
result.task(taskToCheck).outcome == (success ? SUCCESS : FAILED)
success || result.output.contains("> Running on ${hostOs()}; cannot build for $os")
where:
label | os | success
'fedora' | 'linux' | runsOnLinux()
'windows' | 'windows' | runsOnWindows()
'macos' | 'macos' | runsOnMacos()
}
}

View file

@ -0,0 +1,121 @@
package org.xbib.gradle.plugin.jpackage.fixture
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import java.lang.management.ManagementFactory
import java.nio.file.Files
class GradleBuild {
final File projectDir
final File settingsFile
final File appBuildFile
final File appModuleInfoFile
final File libBuildFile
final File libModuleInfoFile
final String gradleVersionUnderTest = System.getProperty('gradleVersionUnderTest')
GradleBuild(File projectDir = Files.createTempDirectory('gradle-build').toFile()) {
this.projectDir = projectDir
this.settingsFile = file('settings.gradle.kts')
this.appBuildFile = file('app/build.gradle.kts')
this.appModuleInfoFile = file('app/src/main/java/module-info.java')
this.libBuildFile = file('lib/build.gradle.kts')
this.libModuleInfoFile = file('lib/src/main/java/module-info.java')
file('app/src/main/resourcesPackage/windows').mkdirs()
file('app/src/main/resourcesPackage/macos').mkdirs()
file('app/src/main/resourcesPackage/linux').mkdirs()
settingsFile << '''
dependencyResolutionManagement { repositories.mavenCentral() }
includeBuild(".")
rootProject.name = "test-project"
include("app", "lib")
'''
appBuildFile << '''
plugins {
id("org.xbib.gradle.plugin.jpackage")
id("application")
}
group = "org.example"
application {
mainModule.set("org.example.app")
mainClass.set("org.example.app.Main")
}
'''
file("app/src/main/java/org/example/app/Main.java") << '''
package org.example.app;
public class Main {
public static void main(String... args) {
}
}
'''
file("app/src/test/java/org/example/app/test/MainTest.java") << '''
package org.example.app.test;
import org.junit.jupiter.api.Test;
import org.example.app.Main;
public class MainTest {
@Test
void testApp() {
new Main();
}
}
'''
libBuildFile << '''
plugins {
id("org.xbib.gradle.plugin.jpackage")
id("java-library")
}
group = "org.example"
'''
}
File file(String path) {
new File(projectDir, path).tap {
it.getParentFile().mkdirs()
}
}
static boolean runsOnWindows() {
hostOs().contains('win')
}
static boolean runsOnMacos() {
hostOs().contains('mac')
}
static boolean runsOnLinux() {
!runsOnWindows() && !runsOnMacos()
}
static String hostOs() {
System.getProperty("os.name").replace(" ", "").toLowerCase()
}
BuildResult build(taskToRun) {
runner(taskToRun).build()
}
BuildResult fail(taskToRun) {
runner(taskToRun).buildAndFail()
}
GradleRunner runner(String... args) {
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withProjectDir(projectDir)
.withArguments(Arrays.asList(args) + '-s')
.withDebug(ManagementFactory.getRuntimeMXBean().getInputArguments().toString().contains("-agentlib:jdwp")).with {
gradleVersionUnderTest ? it.withGradleVersion(gradleVersionUnderTest) : it
}
}
}

View file

@ -22,6 +22,8 @@ dependencyResolutionManagement {
library('rpm', 'org.xbib', 'rpm-core').version('4.0.0') library('rpm', 'org.xbib', 'rpm-core').version('4.0.0')
// MUST be groovy 3 dependency // MUST be groovy 3 dependency
library('groovy-git', 'org.xbib.groovy', 'groovy-git').version('3.0.17.0') library('groovy-git', 'org.xbib.groovy', 'groovy-git').version('3.0.17.0')
library('jsoup', 'org.jsoup', 'jsoup').version('1.17.2')
library('javapoet', 'com.squareup', 'javapoet').version('1.13.0')
plugin('publish', 'com.gradle.plugin-publish').version('1.2.1') plugin('publish', 'com.gradle.plugin-publish').version('1.2.1')
} }
testLibs { testLibs {
@ -47,8 +49,11 @@ include 'gradle-plugin-c'
include 'gradle-plugin-cmake' include 'gradle-plugin-cmake'
include 'gradle-plugin-docker' include 'gradle-plugin-docker'
include 'gradle-plugin-git' include 'gradle-plugin-git'
include 'gradle-plugin-j2html'
include 'gradle-plugin-jacc' include 'gradle-plugin-jacc'
include 'gradle-plugin-jdeps'
include 'gradle-plugin-jflex' include 'gradle-plugin-jflex'
include 'gradle-plugin-jlink'
include 'gradle-plugin-jpackage' include 'gradle-plugin-jpackage'
include 'gradle-plugin-rpm' include 'gradle-plugin-rpm'
include 'gradle-plugin-shadow' include 'gradle-plugin-shadow'