diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 7a533d5c33..78654cf628 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -14,8 +14,10 @@ 9.1.0 16.1.2 + 26.0.3 20.2.1 0.1.0 + 21.0.1 5.0.1 diff --git a/cucumber-java/pom.xml b/cucumber-java/pom.xml index c48d9003c4..94bc46c77d 100644 --- a/cucumber-java/pom.xml +++ b/cucumber-java/pom.xml @@ -89,47 +89,96 @@ ${hamcrest.version} test + + org.freemarker + freemarker + 2.3.31 + test + + - org.codehaus.gmaven - groovy-maven-plugin - - - io.cucumber - gherkin - ${gherkin.version} - - + maven-resources-plugin - generate-i18n-sources + generate-i18n + generate-sources - execute + copy-resources + + ${project.build.directory}/codegen-classes + + + ${project.basedir}/src/codegen/resources + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + + generate-i18n generate-sources + + testCompile + - ${basedir}/src/main/groovy/generate-annotations.groovy - compile + ${project.basedir}/src/codegen/java + ${project.build.directory}/codegen-classes + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + generate-i18n + generate-sources + + java + + + + + test + false + false + ${project.build.directory}/codegen-classes + GenerateI18n + + ${project.build.directory}/generated-sources/i18n + io/cucumber/java + + + org.codehaus.mojo build-helper-maven-plugin - add-source + generate-i18n generate-sources add-source - ${basedir}/target/generated-sources/i18n/java + ${project.build.directory}/generated-sources/i18n diff --git a/cucumber-java/src/codegen/java/GenerateI18n.java b/cucumber-java/src/codegen/java/GenerateI18n.java new file mode 100644 index 0000000000..99c944253e --- /dev/null +++ b/cucumber-java/src/codegen/java/GenerateI18n.java @@ -0,0 +1,142 @@ +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import io.cucumber.gherkin.GherkinDialect; +import io.cucumber.gherkin.GherkinDialectProvider; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.Normalizer; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static java.nio.file.Files.newBufferedWriter; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +/* This class generates the cucumber-java Interfaces and package-info + * based on the languages and keywords from the GherkinDialectProvider + * using the FreeMarker template engine and provided templates. + */ +public class GenerateI18n { + + // The generated files for and Emoij do not compile :( + private static final List unsupported = Arrays.asList("em", "en-tx"); + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + throw new IllegalArgumentException("Usage: "); + } + + DialectWriter dialectWriter = new DialectWriter(args[0], args[1]); + + // Generate annotation files for each dialect + GherkinDialectProvider dialectProvider = new GherkinDialectProvider(); + dialectProvider.getLanguages() + .stream() + .map(dialectProvider::getDialect) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(dialect -> !unsupported.contains(dialect.getLanguage())) + .forEach(dialectWriter::writeDialect); + } + + static class DialectWriter { + private final Template templateSource; + private final Template packageInfoSource; + private final String baseDirectory; + private final String packagePath; + + DialectWriter(String baseDirectory, String packagePath) throws IOException { + this.baseDirectory = baseDirectory; + this.packagePath = packagePath; + + Configuration cfg = new Configuration(Configuration.VERSION_2_3_21); + cfg.setClassForTemplateLoading(GenerateI18n.class, "templates"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setLocale(Locale.US); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + templateSource = cfg.getTemplate("annotation.java.ftl"); + packageInfoSource = cfg.getTemplate("package-info.ftl"); + } + + void writeDialect(GherkinDialect dialect) { + writeKeyWordAnnotations(dialect); + writePackageInfo(dialect); + } + + private void writeKeyWordAnnotations(GherkinDialect dialect) { + dialect.getStepKeywords().stream() + .filter(it -> !it.contains(String.valueOf('*'))) + .filter(it -> !it.matches("^\\d.*")) + .distinct() + .forEach(keyword -> writeKeyWordAnnotation(dialect, keyword)); + } + + private void writeKeyWordAnnotation(GherkinDialect dialect, String keyword) { + String normalizedLanguage = getNormalizedLanguage(dialect); + String normalizedKeyword = getNormalizedKeyWord(keyword); + + Map binding = new LinkedHashMap<>(); + binding.put("lang", normalizedLanguage); + binding.put("kw", normalizedKeyword); + + Path path = Paths.get(baseDirectory, packagePath, normalizedLanguage, normalizedKeyword + ".java"); + + if (Files.exists(path)) { + // Haitian has two translations that only differ by case - Sipozeke and SipozeKe + // Some file systems are unable to distinguish between them and + // overwrite the other one :-( + return; + } + + try { + Files.createDirectories(path.getParent()); + templateSource.process(binding, newBufferedWriter(path, CREATE, TRUNCATE_EXISTING)); + } catch (IOException | TemplateException e) { + throw new RuntimeException(e); + } + } + + private static String getNormalizedKeyWord(String keyword) { + return normalize(keyword.replaceAll("[\\s',!\u00AD]", "")); + } + + private static String normalize(CharSequence s) { + return Normalizer.normalize(s, Normalizer.Form.NFC); + } + + private void writePackageInfo(GherkinDialect dialect) { + String normalizedLanguage = getNormalizedLanguage(dialect); + String languageName = dialect.getName(); + if (!dialect.getName().equals(dialect.getNativeName())) { + languageName += " - " + dialect.getNativeName(); + } + + Map binding = new LinkedHashMap<>(); + binding.put("normalized_language", normalizedLanguage); + binding.put("language_name", languageName); + + Path path = Paths.get(baseDirectory, packagePath, normalizedLanguage, "package-info.java"); + + try { + Files.createDirectories(path.getParent()); + packageInfoSource.process(binding, newBufferedWriter(path, CREATE, TRUNCATE_EXISTING)); + } catch (IOException | TemplateException e) { + throw new RuntimeException(e); + } + } + + private static String getNormalizedLanguage(GherkinDialect dialect) { + return dialect.getLanguage().replaceAll("[\\s-]", "_").toLowerCase(); + } + } +} diff --git a/cucumber-java/src/main/groovy/annotation.java.gsp b/cucumber-java/src/codegen/resources/templates/annotation.java.ftl similarity index 100% rename from cucumber-java/src/main/groovy/annotation.java.gsp rename to cucumber-java/src/codegen/resources/templates/annotation.java.ftl diff --git a/cucumber-java/src/main/groovy/package-info.java.gsp b/cucumber-java/src/codegen/resources/templates/package-info.ftl similarity index 100% rename from cucumber-java/src/main/groovy/package-info.java.gsp rename to cucumber-java/src/codegen/resources/templates/package-info.ftl diff --git a/cucumber-java/src/main/groovy/generate-annotations.groovy b/cucumber-java/src/main/groovy/generate-annotations.groovy deleted file mode 100644 index efe53ffa9b..0000000000 --- a/cucumber-java/src/main/groovy/generate-annotations.groovy +++ /dev/null @@ -1,42 +0,0 @@ -import groovy.text.SimpleTemplateEngine -import io.cucumber.gherkin.GherkinDialectProvider - -import java.nio.file.Files -import java.text.Normalizer - -SimpleTemplateEngine engine = new SimpleTemplateEngine() -def templateSource = new File(project.basedir, "src/main/groovy/annotation.java.gsp").getText() -def packageInfoSource = new File(project.basedir, "src/main/groovy/package-info.java.gsp").getText() - -static def normalize(s) { - return Normalizer.normalize(s, Normalizer.Form.NFC) -} - -def unsupported = ["em", "en_tx"] // The generated files for Emoij and Texan do not compile. -GherkinDialectProvider dialectProvider = new GherkinDialectProvider() - -dialectProvider.getLanguages().each { language -> - def dialect = dialectProvider.getDialect(language).get() - def normalized_language = dialect.language.replaceAll("[\\s-]", "_").toLowerCase() - if (!unsupported.contains(normalized_language)) { - dialect.stepKeywords.findAll { !it.contains('*') && !it.matches("^\\d.*") }.unique().each { kw -> - def normalized_kw = normalize(kw.replaceAll("[\\s',!\u00AD]", "")) - def binding = ["lang": normalized_language, "kw": normalized_kw] - def template = engine.createTemplate(templateSource).make(binding) - def file = new File(project.basedir, "target/generated-sources/i18n/java/io/cucumber/java/${normalized_language}/${normalized_kw}.java") - if (!file.exists()) { - // Haitian has two translations that only differ by case - Sipozeke and SipozeKe - // Some file systems are unable to distiguish between them and overwrite the other one :-( - Files.createDirectories(file.parentFile.toPath()) - file.write(template.toString(), "UTF-8") - } - } - - // package-info.java - def name = dialect.name + ((dialect.name == dialect.nativeName) ? '' : ' - ' + dialect.nativeName) - def binding = ["normalized_language": normalized_language, "language_name": name] - def html = engine.createTemplate(packageInfoSource).make(binding).toString() - def file = new File(project.basedir, "target/generated-sources/i18n/java/io/cucumber/java/${normalized_language}/package-info.java") - file.write(html, "UTF-8") - } -} diff --git a/cucumber-java8/pom.xml b/cucumber-java8/pom.xml index 0da6ff2e57..bf19ab1023 100644 --- a/cucumber-java8/pom.xml +++ b/cucumber-java8/pom.xml @@ -84,47 +84,97 @@ ${hamcrest.version} test + + + org.freemarker + freemarker + 2.3.31 + test + + - org.codehaus.gmaven - groovy-maven-plugin - - - io.cucumber - gherkin - ${gherkin.version} - - + maven-resources-plugin - generate-i18n-sources + generate-i18n + generate-sources - execute + copy-resources + + ${project.build.directory}/codegen-classes + + + ${project.basedir}/src/codegen/resources + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + + generate-i18n generate-sources + + testCompile + - ${basedir}/src/main/groovy/generate-interfaces.groovy - compile + ${project.basedir}/src/codegen/java + ${project.build.directory}/codegen-classes + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + generate-i18n + generate-sources + + java + + + + + test + false + false + ${project.build.directory}/codegen-classes + GenerateI18n + + ${project.build.directory}/generated-sources/i18n + io/cucumber/java8 + + + org.codehaus.mojo build-helper-maven-plugin - add-source + generate-i18n generate-sources add-source - ${basedir}/target/generated-sources/i18n/java + ${project.build.directory}/generated-sources/i18n diff --git a/cucumber-java8/src/codegen/java/GenerateI18n.java b/cucumber-java8/src/codegen/java/GenerateI18n.java new file mode 100644 index 0000000000..344b389c99 --- /dev/null +++ b/cucumber-java8/src/codegen/java/GenerateI18n.java @@ -0,0 +1,120 @@ +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import io.cucumber.gherkin.GherkinDialect; +import io.cucumber.gherkin.GherkinDialectProvider; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.Normalizer; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static java.nio.file.Files.newBufferedWriter; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.util.stream.Collectors.toList; + +/* This class generates the cucumber-java Interfaces and package-info + * based on the languages and keywords from the GherkinDialectProvider + * using the FreeMarker template engine and provided templates. + */ +public class GenerateI18n { + + // The generated files for and Emoij do not compile :( + private static final List unsupported = Arrays.asList("em", "en-tx"); + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + throw new IllegalArgumentException("Usage: "); + } + + DialectWriter dialectWriter = new DialectWriter(args[0], args[1]); + + // Generate annotation files for each dialect + GherkinDialectProvider dialectProvider = new GherkinDialectProvider(); + dialectProvider.getLanguages() + .stream() + .map(dialectProvider::getDialect) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(dialect -> !unsupported.contains(dialect.getLanguage())) + .forEach(dialectWriter::writeDialect); + } + + static class DialectWriter { + private final Template templateSource; + private final String baseDirectory; + private final String packagePath; + + DialectWriter(String baseDirectory, String packagePath) throws IOException { + this.baseDirectory = baseDirectory; + this.packagePath = packagePath; + + Configuration cfg = new Configuration(Configuration.VERSION_2_3_21); + cfg.setClassForTemplateLoading(GenerateI18n.class, "templates"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setLocale(Locale.US); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + templateSource = cfg.getTemplate("lambda.java.ftl"); + } + + void writeDialect(GherkinDialect dialect) { + writeInterface(dialect); + } + + private void writeInterface(GherkinDialect dialect) { + String normalizedLanguage = getNormalizedLanguage(dialect); + String languageName = dialect.getName(); + if (!dialect.getName().equals(dialect.getNativeName())) { + languageName += " - " + dialect.getNativeName(); + } + String className = capitalize(normalizedLanguage); + + Map binding = new LinkedHashMap<>(); + binding.put("className", className); + binding.put("keywords", extractKeywords(dialect)); + binding.put("language_name", languageName); + + Path path = Paths.get(baseDirectory, packagePath, className + ".java"); + + try { + Files.createDirectories(path.getParent()); + templateSource.process(binding, newBufferedWriter(path, CREATE, TRUNCATE_EXISTING)); + } catch (IOException | TemplateException e) { + throw new RuntimeException(e); + } + } + + // Extract sorted keywords from the dialect, and normalize them + private static List extractKeywords(GherkinDialect dialect) { + return dialect.getStepKeywords().stream() + .sorted() + .filter(it -> !it.contains(String.valueOf('*'))) + .filter(it -> !it.matches("^\\d.*")).distinct() + .map(keyword -> keyword.replaceAll("[\\s',!\u00AD]", "")) + .map(DialectWriter::normalize) + .collect(toList()); + } + + private static String capitalize(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + static String normalize(CharSequence s) { + return Normalizer.normalize(s, Normalizer.Form.NFC); + } + + private static String getNormalizedLanguage(GherkinDialect dialect) { + return dialect.getLanguage().replaceAll("[\\s-]", "_").toLowerCase(); + } + } +} diff --git a/cucumber-java8/src/main/groovy/lambda.java.gsp b/cucumber-java8/src/codegen/resources/templates/lambda.java.ftl similarity index 67% rename from cucumber-java8/src/main/groovy/lambda.java.gsp rename to cucumber-java8/src/codegen/resources/templates/lambda.java.ftl index e444171858..857a14a153 100644 --- a/cucumber-java8/src/main/groovy/lambda.java.gsp +++ b/cucumber-java8/src/codegen/resources/templates/lambda.java.ftl @@ -33,36 +33,52 @@ import org.apiguardian.api.API; *

* The type of the data table or doc string argument is determined * by the argument name value. When none is provided cucumber will - * attempt to transform the data table or doc string to the the + * attempt to transform the data table or doc string to the * type of last argument. */ @API(status = API.Status.STABLE) public interface ${className} extends LambdaGlue { -<% i18n.stepKeywords.findAll { !it.contains('*') && !it.matches("^\\d.*") }.sort().unique().each { kw -> %> + <#list keywords as kw> + /** * Creates a new step definition. * * @param expression the cucumber expression * @param body a lambda expression with no parameters */ - default void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!]", ""), java.text.Normalizer.Form.NFC)}(String expression, A0 body) { + default void ${kw}(String expression, A0 body) { LambdaGlueRegistry.INSTANCE.get().addStepDefinition(Java8StepDefinition.create(expression, A0.class, body)); } - <% (1..9).each { arity -> - def ts = (1..arity).collect { n -> "T"+n } - def genericSignature = ts.join(",") %> + /** + * Creates a new step definition. + * + * @param expression the cucumber expression + * @param body a lambda expression with 1 parameter + * + * @param type of argument 1 + */ + default void ${kw}(String expression, A1 body) { + LambdaGlueRegistry.INSTANCE.get().addStepDefinition(Java8StepDefinition.create(expression, A1.class, body)); + } + + <#list 2..9 as arity> +<#-- TODO: use function or macro for genericSignature ? --> + <#assign repeat = arity -1> /** * Creates a new step definition. * * @param expression the cucumber expression * @param body a lambda expression with ${arity} parameters - * <% (1..arity).each { i -> %> - * @param type of argument ${i} <% } %> + * + <#list 1..arity as i> + * @param type of argument ${i} + */ - default <${genericSignature}> void ${java.text.Normalizer.normalize(kw.replaceAll("[\\s',!]", ""), java.text.Normalizer.Form.NFC)}(String expression, A${arity}<${genericSignature}> body) { + default <<#list 1..repeat as i>T${i},T${arity}> void ${kw}(String expression, A${arity}<<#list 1..repeat as i>T${i},T${arity}> body) { LambdaGlueRegistry.INSTANCE.get().addStepDefinition(Java8StepDefinition.create(expression, A${arity}.class, body)); } - <% } %> -<% } %> + + + } diff --git a/cucumber-java8/src/main/groovy/generate-interfaces.groovy b/cucumber-java8/src/main/groovy/generate-interfaces.groovy deleted file mode 100644 index 51856c30b9..0000000000 --- a/cucumber-java8/src/main/groovy/generate-interfaces.groovy +++ /dev/null @@ -1,24 +0,0 @@ -import groovy.text.SimpleTemplateEngine -import io.cucumber.gherkin.GherkinDialectProvider - -import java.nio.file.Files - -SimpleTemplateEngine engine = new SimpleTemplateEngine() - -def unsupported = ["em", "en_tx"] // The generated files for Emoij and Texan do not compile. -GherkinDialectProvider dialectProvider = new GherkinDialectProvider() - -dialectProvider.getLanguages().each { language -> - def dialect = dialectProvider.getDialect(language).get() - def normalized_language = dialect.language.replaceAll("[\\s-]", "_").toLowerCase() - if (!unsupported.contains(normalized_language)) { - def templateSource = new File(project.basedir, "src/main/groovy/lambda.java.gsp").getText() - def className = "${normalized_language}".capitalize() - def name = dialect.name + ((dialect.name == dialect.nativeName) ? '' : ' - ' + dialect.nativeName) - def binding = ["i18n": dialect, "className": className, "language_name": name] - def template = engine.createTemplate(templateSource).make(binding) - def file = new File(project.basedir, "target/generated-sources/i18n/java/io/cucumber/java8/${className}.java") - Files.createDirectories(file.parentFile.toPath()) - file.write(template.toString(), "UTF-8") - } -} diff --git a/pom.xml b/pom.xml index f86c649e99..4fce3a475f 100644 --- a/pom.xml +++ b/pom.xml @@ -19,14 +19,6 @@ 1.8 8 1674814830 - - - - 21.0.1 - 26.0.3 - - - 2.5.15 scm:git:git://github.com/cucumber/cucumber-jvm.git