diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java deleted file mode 100644 index e68c714b2f..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen; - -import com.diffplug.spotless.FormatExceptionPolicyStrict; -import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.LineEnding; -import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; -import com.diffplug.spotless.generic.EndWithNewlineStep; -import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; -import com.diffplug.spotless.java.ImportOrderStep; -import com.diffplug.spotless.java.RemoveUnusedImportsStep; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; -import org.opensearch.client.codegen.exceptions.JavaFormatterException; -import org.opensearch.client.codegen.utils.MavenArtifactResolver; - -public class JavaFormatter implements AutoCloseable { - private final Formatter formatter; - - public JavaFormatter(Path rootDir, File eclipseFormatterConfig) { - Objects.requireNonNull(rootDir, "rootDir must not be null"); - Objects.requireNonNull(eclipseFormatterConfig, "eclipseFormatterConfig must not be null"); - - Provisioner provisioner = MavenArtifactResolver.createDefault()::resolve; - - var steps = List.of( - importOrderStep(), - removeUnusedImportsStep(provisioner), - eclipseFormatter(provisioner, eclipseFormatterConfig), - trimTrailingWhitespaceStep(), - endWithNewlineStep() - ); - - this.formatter = Formatter.builder() - .name("java") - .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) - .encoding(StandardCharsets.UTF_8) - .rootDir(rootDir) - .steps(steps) - .exceptionPolicy(new FormatExceptionPolicyStrict()) - .build(); - } - - private static FormatterStep importOrderStep() { - return ImportOrderStep.forJava().createFrom(); - } - - private static FormatterStep removeUnusedImportsStep(Provisioner provisioner) { - return RemoveUnusedImportsStep.create(RemoveUnusedImportsStep.defaultFormatter(), provisioner); - } - - private static FormatterStep eclipseFormatter(Provisioner provisioner, File eclipseFormatterConfig) { - var eclipseFormatter = EclipseJdtFormatterStep.createBuilder(provisioner); - eclipseFormatter.setVersion(EclipseJdtFormatterStep.defaultVersion()); - eclipseFormatter.setPreferences(List.of(eclipseFormatterConfig)); - return eclipseFormatter.build(); - } - - private static FormatterStep endWithNewlineStep() { - return EndWithNewlineStep.create(); - } - - private static FormatterStep trimTrailingWhitespaceStep() { - return TrimTrailingWhitespaceStep.create(); - } - - public void format(File file) throws JavaFormatterException { - try { - formatter.applyTo(file); - } catch (Throwable e) { - throw new JavaFormatterException("Failed to format: " + file, e); - } - } - - @Override - public void close() { - formatter.close(); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java b/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java index c8830a3d5f..6785226d0c 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java @@ -27,6 +27,7 @@ import org.opensearch.client.codegen.exceptions.RenderException; import org.opensearch.client.codegen.model.Namespace; import org.opensearch.client.codegen.model.OperationGroup; +import org.opensearch.client.codegen.model.ShapeRenderingContext; import org.opensearch.client.codegen.model.SpecTransformer; import org.opensearch.client.codegen.openapi.OpenApiSpecification; @@ -84,10 +85,16 @@ public static void main(String[] args) { cleanDirectory(outputDir); - outputDir = new File(outputDir, root.getPackageName().replace('.', '/')); + final var rootPackageOutputDir = new File(outputDir, root.getPackageName().replace('.', '/')); - try (var formatter = new JavaFormatter(outputDir.toPath(), eclipseConfig)) { - root.render(outputDir, formatter); + try ( + var ctx = ShapeRenderingContext.builder() + .withOutputDir(rootPackageOutputDir) + .withJavaCodeFormatter(b -> b.withRootDir(rootPackageOutputDir.toPath()).withEclipseFormatterConfig(eclipseConfig)) + .withTemplateLoader(b -> b.withTemplatesResourceSubPath("/org/opensearch/client/codegen/templates")) + .build() + ) { + root.render(ctx); } } catch (ParseException e) { LOGGER.error("Argument Parsing Failed. Reason: {}", e.getMessage()); diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/Renderer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/Renderer.java deleted file mode 100644 index acb946a0d6..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/Renderer.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen; - -import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.MustacheException; -import com.samskivert.mustache.Template; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.StringWriter; -import java.io.Writer; -import java.util.HashMap; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import org.apache.commons.text.StringEscapeUtils; -import org.opensearch.client.codegen.exceptions.JavaFormatterException; -import org.opensearch.client.codegen.exceptions.RenderException; -import org.opensearch.client.codegen.model.Shape; -import org.opensearch.client.codegen.model.Type; -import org.opensearch.client.codegen.model.Types; -import org.opensearch.client.codegen.utils.Strings; - -public class Renderer { - private static final Mustache.Compiler BASE_COMPILER = Mustache.compiler().escapeHTML(false).withLoader(name -> { - var stream = Renderer.class.getResourceAsStream("templates/" + name + ".mustache"); - if (stream == null) { - throw new MissingResourceException("Unable to find template", Renderer.class.getName(), name); - } - return new InputStreamReader(stream); - }); - - public static Mustache.Lambda transformer(Function transform) { - return ((frag, out) -> out.write(transform.apply(frag.execute()))); - } - - private static final Map GlobalContext = new HashMap<>() { - { - put("quoted", transformer(s -> '\"' + StringEscapeUtils.escapeJava(s) + '\"')); - put("camelCase", transformer(Strings::toCamelCase)); - put("pascalCase", transformer(Strings::toPascalCase)); - put("toLower", transformer(String::toLowerCase)); - put("ERROR", (Mustache.Lambda) (frag, out) -> { throw new RuntimeException(frag.execute()); }); - put("TYPES", Types.asMap()); - } - }; - - private final Mustache.Compiler compiler; - private final Context context; - private final JavaFormatter javaFormatter; - - public Renderer(Consumer typeReferenceTracker, JavaFormatter javaFormatter) { - compiler = BASE_COMPILER.withFormatter(new ValueFormatter(typeReferenceTracker)); - this.context = new Context(this); - this.javaFormatter = javaFormatter; - } - - public void render(String templateName, Object context, Writer out) throws RenderException { - try { - compiler.loadTemplate(templateName).execute(context, this.context, out); - } catch (MustacheException e) { - throw new RenderException("Failed to render: " + context, e); - } - } - - public String render(String templateName, Object context) throws RenderException { - StringWriter writer = new StringWriter(); - render(templateName, context, writer); - return writer.toString(); - } - - public void renderJava(Shape shape, File outputFile) throws RenderException { - var classBody = render(shape.getClass().getSimpleName(), shape); - var classHeader = render("Partials/ClassHeader", shape); - - try (Writer fileWriter = new FileWriter(outputFile)) { - fileWriter.write(classHeader); - fileWriter.write("\n\n"); - fileWriter.write(classBody); - } catch (IOException e) { - throw new RenderException("Unable to write rendered output to: " + outputFile, e); - } - - try { - javaFormatter.format(outputFile); - } catch (JavaFormatterException e) { - throw new RenderException("Unable to format rendered output: " + outputFile, e); - } - } - - public static Mustache.Lambda templateLambda(String templateName, Function contextGetter) { - return (frag, out) -> { - try { - findContext(frag, Context.class).orElseThrow().getRenderer().render(templateName, contextGetter.apply(frag), out); - } catch (RenderException e) { - throw new RuntimeException(e); - } - }; - } - - @SuppressWarnings("unchecked") - public static Optional findContext(Template.Fragment fragment, Class clazz) { - var i = 0; - while (true) { - Object ctx = null; - - try { - ctx = fragment.context(i++); - } catch (NullPointerException ignored) {} - - if (ctx == null) return Optional.empty(); - - if (clazz.isAssignableFrom(ctx.getClass())) { - return Optional.of((T) ctx); - } - } - } - - private static class Context implements Mustache.CustomContext { - private final Renderer renderer; - - public Context(Renderer renderer) { - this.renderer = renderer; - } - - @Override - public Object get(String name) throws Exception { - if (GlobalContext.containsKey(name)) { - return GlobalContext.get(name); - } - - return null; - } - - public Renderer getRenderer() { - return renderer; - } - } - - private static class ValueFormatter implements Mustache.Formatter { - private final Consumer typeReferenceTracker; - - public ValueFormatter(Consumer typeReferenceTracker) { - this.typeReferenceTracker = typeReferenceTracker; - } - - @Override - public CharSequence format(Object o) { - if (o instanceof Type) { - typeReferenceTracker.accept((Type) o); - } - return String.valueOf(o); - } - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java index 816bb1bc18..6e313b0f48 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java @@ -8,7 +8,6 @@ package org.opensearch.client.codegen.model; -import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -16,7 +15,6 @@ import java.util.TreeMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.opensearch.client.codegen.JavaFormatter; import org.opensearch.client.codegen.exceptions.RenderException; import org.opensearch.client.codegen.utils.Lists; import org.opensearch.client.codegen.utils.Strings; @@ -69,15 +67,13 @@ public Namespace child(@Nullable String name) { } @Override - public void render(File outputDir, JavaFormatter formatter) throws RenderException { - outputDir.mkdirs(); - + public void render(ShapeRenderingContext ctx) throws RenderException { for (Namespace child : children.values()) { - child.render(new File(outputDir, child.getPackageNamePart()), formatter); + child.render(ctx.forSubDir(child.getPackageNamePart())); } for (Shape shape : shapes) { - shape.render(outputDir, formatter); + shape.render(ctx); } if (operations.isEmpty()) return; diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java index 91e9fe05d7..7b8fcb166e 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java @@ -8,14 +8,11 @@ package org.opensearch.client.codegen.model; -import java.io.File; import java.util.HashSet; import java.util.Set; import java.util.TreeSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.codegen.JavaFormatter; -import org.opensearch.client.codegen.Renderer; import org.opensearch.client.codegen.exceptions.RenderException; public abstract class Shape { @@ -51,10 +48,13 @@ public String getTypedefName() { return this.typedefName; } - public void render(File outputDir, JavaFormatter formatter) throws RenderException { - var outFile = new File(outputDir, this.className + ".java"); + public void render(ShapeRenderingContext ctx) throws RenderException { + var outFile = ctx.getOutputFile(className + ".java"); LOGGER.info("Rendering: {}", outFile); - var renderer = new Renderer(referencedTypes::add, formatter); + var renderer = ctx.getTemplateRenderer(b -> b.withFormatter(Type.class, t -> { + referencedTypes.add(t); + return t.toString(); + })); renderer.renderJava(this, outFile); } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java new file mode 100644 index 0000000000..14d81314cb --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.model; + +import java.io.File; +import java.util.Objects; +import java.util.function.Function; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.renderer.JavaCodeFormatter; +import org.opensearch.client.codegen.renderer.TemplateLoader; +import org.opensearch.client.codegen.renderer.TemplateRenderer; +import org.opensearch.client.codegen.renderer.TemplateValueFormatter; +import org.opensearch.client.codegen.utils.Strings; + +public final class ShapeRenderingContext implements AutoCloseable { + @Nonnull + private final File outputDir; + @Nonnull + private final TemplateLoader templateLoader; + @Nonnull + private final JavaCodeFormatter javaCodeFormatter; + private final boolean ownedJavaCodeFormatter; + + private ShapeRenderingContext(Builder builder) { + this.outputDir = Objects.requireNonNull(builder.outputDir, "outputDir must not be null"); + this.templateLoader = Objects.requireNonNull(builder.templateLoader, "templateLoader must not be null"); + this.javaCodeFormatter = Objects.requireNonNull(builder.javaCodeFormatter, "javaCodeFormatter must not be null"); + this.ownedJavaCodeFormatter = builder.ownedJavaCodeFormatter; + } + + @Nonnull + public ShapeRenderingContext forSubDir(@Nonnull String name) { + return builder() + .withOutputDir(new File(outputDir, Strings.requireNonBlank(name, "name must not be null"))) + .withTemplateLoader(templateLoader) + .withJavaCodeFormatter(javaCodeFormatter) + .build(); + } + + @Nonnull + public File getOutputFile(@Nonnull String name) { + outputDir.mkdirs(); + return new File(outputDir, Strings.requireNonBlank(name, "name must not be blank")); + } + + @Nonnull + public TemplateRenderer getTemplateRenderer(@Nonnull Function valueFormatterConfigurator) { + return TemplateRenderer.builder() + .withValueFormatter(valueFormatterConfigurator) + .withTemplateLoader(templateLoader) + .withJavaCodeFormatter(javaCodeFormatter) + .build(); + } + + @Override + public void close() throws Exception { + if (ownedJavaCodeFormatter) { + javaCodeFormatter.close(); + } + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private File outputDir; + private TemplateLoader templateLoader; + private JavaCodeFormatter javaCodeFormatter; + private boolean ownedJavaCodeFormatter; + + @Nonnull + public Builder withOutputDir(@Nonnull File outputDir) { + this.outputDir = Objects.requireNonNull(outputDir, "outputDir must not be null"); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull TemplateLoader templateLoader) { + this.templateLoader = Objects.requireNonNull(templateLoader, "templateLoader must not be null"); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull Function configurator) { + return withTemplateLoader(Objects.requireNonNull(configurator, "configurator must not be null").apply(TemplateLoader.builder()).build()); + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter, boolean owned) { + if (this.ownedJavaCodeFormatter && this.javaCodeFormatter != null) { + this.javaCodeFormatter.close(); + } + this.javaCodeFormatter = Objects.requireNonNull(javaCodeFormatter, "javaCodeFormatter must not be null"); + this.ownedJavaCodeFormatter = owned; + return this; + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter) { + return withJavaCodeFormatter(javaCodeFormatter, false); + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull Function configurator) { + return withJavaCodeFormatter(Objects.requireNonNull(configurator, "configurator must not be null").apply(JavaCodeFormatter.builder()).build(), true); + } + + @Nonnull + public ShapeRenderingContext build() { + return new ShapeRenderingContext(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java index 5a3aa9b1ac..623b8988de 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java @@ -8,16 +8,15 @@ package org.opensearch.client.codegen.model; -import static org.opensearch.client.codegen.Renderer.templateLambda; import static org.opensearch.client.codegen.model.Types.Client; import static org.opensearch.client.codegen.model.Types.Java; import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.Template; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; -import org.opensearch.client.codegen.Renderer; +import org.opensearch.client.codegen.renderer.lambdas.TypeQueryParamifyLambda; +import org.opensearch.client.codegen.renderer.lambdas.TypeSerializerLambda; public class Type { private static final Set PRIMITIVES = Set.of( @@ -171,19 +170,11 @@ public Type getNestedType(String name) { } public Mustache.Lambda serializer() { - return Renderer.templateLambda("Type/serializer", this::getSerializerLambdaContext); + return new TypeSerializerLambda(this, false); } public Mustache.Lambda directSerializer() { - return Renderer.templateLambda("Type/directSerializer", this::getSerializerLambdaContext); - } - - private SerializerLambdaContext getSerializerLambdaContext(Template.Fragment fragment) { - return new SerializerLambdaContext( - Type.this, - fragment.execute(), - Renderer.findContext(fragment, SerializerLambdaContext.class).map(ctx -> ctx.depth + 1).orElse(0) - ); + return new TypeSerializerLambda(this, true); } public void getRequiredImports(Set imports, String currentPkg) { @@ -202,26 +193,11 @@ public Type withGenericArgs(Type... genericArgs) { return toBuilder().genericArgs(genericArgs).build(); } - private static class SerializerLambdaContext { - public final Type type; - public final String value; - public final int depth; - - private SerializerLambdaContext(Type type, String value, int depth) { - this.type = type; - this.value = value; - this.depth = depth; - } - } - public Mustache.Lambda queryParamify() { - return templateLambda("Type/queryParamify", frag -> new Object() { - final Type type = Type.this; - final String value = frag.execute(); - }); + return new TypeQueryParamifyLambda(this); } - public static class Builder { + public static final class Builder { private String pkg; private String name; private Type[] genericArgs; diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java index cf52288365..34e39f66bf 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java @@ -5,9 +5,7 @@ import java.util.Map; public final class Types { - public static Map asMap() { - return asMap(Types.class); - } + public static final Map TYPES_MAP = asMap(Types.class); private static Map asMap(Class clazz) { var map = new HashMap(); @@ -20,7 +18,7 @@ private static Map asMap(Class clazz) { } for (var field : clazz.getDeclaredFields()) { - if ((field.getModifiers() & Modifier.STATIC) == 0) { + if ((field.getModifiers() & Modifier.STATIC) == 0 || !Type.class.isAssignableFrom(field.getType())) { continue; } try { diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java new file mode 100644 index 0000000000..35a35143c1 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer; + +import com.samskivert.mustache.Template; +import java.util.Optional; + +public final class TemplateFragmentUtils { + private TemplateFragmentUtils() {} + + @SuppressWarnings("unchecked") + public static Optional findParentContext(Template.Fragment fragment, Class clazz) { + var i = 0; + while (true) { + Object ctx; + + try { + ctx = fragment.context(i++); + } catch (NullPointerException ignored) { + return Optional.empty(); + } + + if (clazz.isAssignableFrom(ctx.getClass())) { + return Optional.of((T) ctx); + } + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java new file mode 100644 index 0000000000..69e121a2fa --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer; + +import com.samskivert.mustache.Mustache; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; +import org.opensearch.client.codegen.model.Types; +import org.opensearch.client.codegen.renderer.lambdas.TemplateStringLambda; +import org.opensearch.client.codegen.utils.Strings; + +public final class TemplateGlobalContext implements Mustache.CustomContext { + @Nonnull + private final Map values; + @Nonnull + private final TemplateRenderer renderer; + + private TemplateGlobalContext(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.values = Objects.requireNonNull(builder.values, "values must not be null"); + this.renderer = Objects.requireNonNull(builder.renderer, "renderer must not be null"); + } + + @Override + @Nullable + public Object get(@Nonnull String name) throws Exception { + return values.get(Strings.requireNonBlank(name, "name must not be blank")); + } + + @Nonnull + public TemplateRenderer getRenderer() { + return renderer; + } + + public static Builder builder() { + return new Builder().withLambda("quoted", s -> '\"' + StringEscapeUtils.escapeJava(s) + '\"') + .withLambda("camelCase", Strings::toCamelCase) + .withLambda("pascalCase", Strings::toPascalCase) + .withLambda("toLower", s -> s.toLowerCase()) + .withLambda("ERROR", s -> { + throw new RuntimeException(s); + }) + .withValue("TYPES", Types.TYPES_MAP); + } + + public static final class Builder { + private final Map values = new HashMap<>(); + private TemplateRenderer renderer; + + @Nonnull + public Builder withLambda(@Nonnull String name, @Nonnull TemplateStringLambda lambda) { + return withLambda(name, TemplateStringLambda.asMustacheLambda(lambda)); + } + + @Nonnull + public Builder withLambda(@Nonnull String name, @Nonnull Mustache.Lambda lambda) { + return withValue(name, lambda); + } + + @Nonnull + public Builder withValue(@Nonnull String name, @Nonnull Object value) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(value, "value must not be null"); + values.put(name, value); + return this; + } + + @Nonnull + public Builder withRenderer(@Nonnull TemplateRenderer renderer) { + this.renderer = Objects.requireNonNull(renderer, "renderer must not be null"); + return this; + } + + @Nonnull + public TemplateGlobalContext build() { + return new TemplateGlobalContext(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java new file mode 100644 index 0000000000..5bff6f12fd --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java @@ -0,0 +1,81 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer; + +import com.samskivert.mustache.Mustache; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; +import org.apache.commons.io.IOUtils; +import org.opensearch.client.codegen.utils.Strings; + +public final class TemplateLoader implements Mustache.TemplateLoader { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + @Nonnull + private final String templatesResourceSubPath; + + private TemplateLoader(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.templatesResourceSubPath = Strings.requireNonBlank( + builder.templatesResourceSubPath, + "templatesResourceSubPath must not be blank" + ); + } + + @Nonnull + @Override + public Reader getTemplate(@Nonnull String name) throws Exception { + Strings.requireNonBlank(name, "name must not be blank"); + var path = templatesResourceSubPath + name + ".mustache"; + + var contents = CACHE.get(path); + + if (contents == null) { + try { + contents = IOUtils.resourceToString(path, StandardCharsets.UTF_8); + CACHE.put(path, contents); + } catch (IOException e) { + throw new Exception("Unable to load template: " + path, e); + } + } + + return new StringReader(contents); + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String templatesResourceSubPath; + + @Nonnull + public Builder withTemplatesResourceSubPath(@Nonnull String templatesResourceSubPath) { + Strings.requireNonBlank(templatesResourceSubPath, "templatesResourceSubPath must not be blank"); + if (!templatesResourceSubPath.startsWith("/")) { + throw new IllegalArgumentException("templatesResourceSubPath must be absolute"); + } + if (!templatesResourceSubPath.endsWith("/")) { + templatesResourceSubPath += "/"; + } + this.templatesResourceSubPath = templatesResourceSubPath; + return this; + } + + @Nonnull + public TemplateLoader build() { + return new TemplateLoader(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java new file mode 100644 index 0000000000..bad84fa5ce --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.MustacheException; +import com.samskivert.mustache.Template; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.exceptions.JavaFormatterException; +import org.opensearch.client.codegen.exceptions.RenderException; +import org.opensearch.client.codegen.model.Shape; + +public final class TemplateRenderer { + @Nonnull + private final Mustache.Compiler compiler; + @Nonnull + private final TemplateGlobalContext context; + @Nonnull + private final JavaCodeFormatter javaCodeFormatter; + @Nonnull + private final ConcurrentHashMap templateCache = new ConcurrentHashMap<>(); + + private TemplateRenderer(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.compiler = Mustache.compiler() + .escapeHTML(false) + .withLoader(Objects.requireNonNull(builder.templateLoader, "templateLoader must not be null")) + .withFormatter(Objects.requireNonNull(builder.valueFormatter, "valueFormatter must not be null")); + this.context = TemplateGlobalContext.builder().withRenderer(this).build(); + this.javaCodeFormatter = Objects.requireNonNull(builder.javaCodeFormatter, "javaCodeFormatter must not be null"); + } + + public void render(String templateName, Object context, Writer out) throws RenderException { + try { + templateCache.computeIfAbsent(templateName, compiler::loadTemplate).execute(context, this.context, out); + } catch (MustacheException e) { + throw new RenderException("Failed to render: " + context, e); + } + } + + public String render(String templateName, Object context) throws RenderException { + var out = new StringWriter(); + render(templateName, context, out); + return out.toString(); + } + + public void renderJava(Shape shape, File outputFile) throws RenderException { + var classBody = render(shape.getClass().getSimpleName(), shape); + var classHeader = render("Partials/ClassHeader", shape); + + try (Writer fileWriter = new FileWriter(outputFile)) { + fileWriter.write(classHeader); + fileWriter.write("\n\n"); + fileWriter.write(classBody); + } catch (IOException e) { + throw new RenderException("Unable to write rendered output to: " + outputFile, e); + } + + try { + javaCodeFormatter.format(outputFile); + } catch (JavaFormatterException e) { + throw new RenderException("Unable to format rendered output: " + outputFile, e); + } + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private TemplateValueFormatter valueFormatter; + private TemplateLoader templateLoader; + private JavaCodeFormatter javaCodeFormatter; + + @Nonnull + public Builder withValueFormatter(@Nonnull Function configurator) { + this.valueFormatter = Objects.requireNonNull(configurator, "configurator must not be null").apply(TemplateValueFormatter.builder()).build(); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull TemplateLoader templateLoader) { + this.templateLoader = Objects.requireNonNull(templateLoader, "templateLoader must not be null"); + return this; + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter) { + this.javaCodeFormatter = Objects.requireNonNull(javaCodeFormatter, "javaCodeFormatter must not be null"); + return this; + } + + @Nonnull + public TemplateRenderer build() { + return new TemplateRenderer(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java new file mode 100644 index 0000000000..26e75de95a --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer; + +import com.samskivert.mustache.Mustache; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; + +public final class TemplateValueFormatter implements Mustache.Formatter { + @Nonnull + private final Map, Formatter> formatters; + + private TemplateValueFormatter(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.formatters = Objects.requireNonNull(builder.formatters, "formatters must not be null"); + } + + @Override + @Nonnull + public CharSequence format(@Nonnull Object value) { + Objects.requireNonNull(value, "value must not be null"); + return format(value, value.getClass()); + } + + @SuppressWarnings("unchecked") + @Nonnull + private CharSequence format(@Nonnull Object value, @Nonnull Class clazz) { + Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(clazz, "clazz must not be null"); + var formatter = (Formatter) formatters.get(clazz); + if (formatter != null) return formatter.format((T) value); + return String.valueOf(value); + } + + @FunctionalInterface + public interface Formatter { + @Nonnull + CharSequence format(@Nonnull T value); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map, Formatter> formatters = new HashMap<>(); + + @Nonnull + public Builder withFormatter(@Nonnull Class clazz, @Nonnull Formatter formatter) { + Objects.requireNonNull(clazz, "clazz must not be null"); + Objects.requireNonNull(formatter, "formatter must not be null"); + formatters.put(clazz, formatter); + return this; + } + + public TemplateValueFormatter build() { + return new TemplateValueFormatter(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java new file mode 100644 index 0000000000..32edd6b6db --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer.lambdas; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import java.io.IOException; +import java.io.Writer; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.exceptions.RenderException; +import org.opensearch.client.codegen.renderer.TemplateGlobalContext; +import org.opensearch.client.codegen.renderer.TemplateFragmentUtils; +import org.opensearch.client.codegen.utils.Strings; + +public abstract class TemplateRenderingLambda implements Mustache.Lambda { + @Nonnull + private final String templateName; + + protected TemplateRenderingLambda(@Nonnull String templateName) { + this.templateName = Strings.requireNonBlank(templateName, "templateName must not be blank"); + } + + @Override + public void execute(Template.Fragment fragment, Writer out) throws IOException { + var renderer = TemplateFragmentUtils.findParentContext(fragment, TemplateGlobalContext.class).orElseThrow().getRenderer(); + + try { + renderer.render(templateName, getContext(fragment), out); + } catch (RenderException e) { + throw new RuntimeException(e); + } + } + + public abstract Object getContext(Template.Fragment fragment); +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.java new file mode 100644 index 0000000000..cd8f762e2c --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer.lambdas; + +import com.samskivert.mustache.Mustache; +import java.util.Objects; +import javax.annotation.Nonnull; + +@FunctionalInterface +public interface TemplateStringLambda { + @Nonnull + String execute(@Nonnull String input); + + static Mustache.Lambda asMustacheLambda(@Nonnull TemplateStringLambda lambda) { + Objects.requireNonNull(lambda, "lambda must not be null"); + return (fragment, out) -> out.write(lambda.execute(fragment.execute())); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java new file mode 100644 index 0000000000..fe93f4987d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer.lambdas; + +import com.samskivert.mustache.Template; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.model.Type; +import org.opensearch.client.codegen.utils.Strings; + +public final class TypeQueryParamifyLambda extends TemplateRenderingLambda { + @Nonnull + private final Type type; + + public TypeQueryParamifyLambda(Type type) { + super("Type/queryParamify"); + this.type = Objects.requireNonNull(type, "type must not be null"); + } + + @Override + public Object getContext(Template.Fragment fragment) { + return new Context(type, fragment.execute()); + } + + public static final class Context { + @Nonnull + private final Type type; + @Nonnull + private final String value; + + private Context(@Nonnull Type type, @Nonnull String value) { + this.type = Objects.requireNonNull(type, "type must not be null"); + this.value = Strings.requireNonBlank(value, "value must not be blank"); + } + + @Nonnull + public Type getType() { + return type; + } + + @Nonnull + public String getValue() { + return value; + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java new file mode 100644 index 0000000000..b2559f98ba --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.renderer.lambdas; + +import com.samskivert.mustache.Template; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.model.Type; +import org.opensearch.client.codegen.renderer.TemplateFragmentUtils; +import org.opensearch.client.codegen.utils.Strings; + +public final class TypeSerializerLambda extends TemplateRenderingLambda { + @Nonnull + private final Type type; + + public TypeSerializerLambda(Type type, boolean direct) { + super("Type/" + (direct ? "directSerializer" : "serializer")); + this.type = Objects.requireNonNull(type, "type must not be null"); + } + + @Override + public Object getContext(Template.Fragment fragment) { + var depth = TemplateFragmentUtils.findParentContext(fragment, Context.class) + .map(ctx -> ctx.depth + 1) + .orElse(0); + return new Context(type, fragment.execute(), depth); + } + + public static final class Context { + @Nonnull + private final Type type; + @Nonnull + private final String value; + private final int depth; + + private Context(@Nonnull Type type, @Nonnull String value, int depth) { + this.type = Objects.requireNonNull(type, "type must not be null"); + this.value = Strings.requireNonBlank(value, "value must not be blank"); + this.depth = depth; + } + + @Nonnull + public Type getType() { + return type; + } + + @Nonnull + public String getValue() { + return value; + } + + public int getDepth() { + return depth; + } + } +}