diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java index 95feb4d6510b..6ffe155ba823 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java @@ -33,6 +33,7 @@ import lombok.Setter; import org.openapitools.codegen.api.AbstractTemplatingEngineAdapter; import org.openapitools.codegen.api.TemplatingExecutor; +import org.openapitools.codegen.templating.handlebars.AccessAwareFieldValueResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +46,7 @@ import java.util.stream.Collectors; public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter { - final Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class); + final Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class); private final String[] extensions = {"handlebars", "hbs"}; // We use this as a simple lookup for valid file name extensions. This adapter will inspect .mustache (built-in) and infer the relevant handlebars filename @@ -73,36 +74,13 @@ public TemplateSource sourceAt(String location) { } }; - // $ref: https://github.com/jknack/handlebars.java/issues/917 - var MY_FIELD_VALUE_RESOLVER = new FieldValueResolver() { - @Override - protected Set members( - Class clazz) { - var members = super.members(clazz); - return members.stream() - .filter(fw -> isValidField(fw)) - .collect(Collectors.toSet()); - } - - boolean isValidField( - FieldWrapper fw) { - if (fw instanceof AccessibleObject) { - if (isUseSetAccessible(fw)) { - return true; - } - return false; - } - return true; - } - }; - Context context = Context .newBuilder(bundle) .resolver( MapValueResolver.INSTANCE, JavaBeanValueResolver.INSTANCE, - MY_FIELD_VALUE_RESOLVER.INSTANCE, - MethodValueResolver.INSTANCE) + MethodValueResolver.INSTANCE, + AccessAwareFieldValueResolver.INSTANCE) .build(); Handlebars handlebars = new Handlebars(loader); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/handlebars/AccessAwareFieldValueResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/handlebars/AccessAwareFieldValueResolver.java new file mode 100644 index 000000000000..ebf279af2052 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/handlebars/AccessAwareFieldValueResolver.java @@ -0,0 +1,28 @@ +package org.openapitools.codegen.templating.handlebars; + +import com.github.jknack.handlebars.context.FieldValueResolver; + +import java.lang.reflect.AccessibleObject; +import java.util.Set; +import java.util.stream.Collectors; + +// $ref: https://github.com/jknack/handlebars.java/issues/917 +public class AccessAwareFieldValueResolver extends FieldValueResolver { + + public static final AccessAwareFieldValueResolver INSTANCE = new AccessAwareFieldValueResolver(); + + @Override + protected Set members(Class clazz) { + var members = super.members(clazz); + return members.stream() + .filter(this::isValidField) + .collect(Collectors.toSet()); + } + + boolean isValidField(FieldWrapper fw) { + if (fw instanceof AccessibleObject) { + return isUseSetAccessible(fw); + } + return true; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/HandlebarsEngineAdapterTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/HandlebarsEngineAdapterTest.java index a1ee38796b11..f43b92c313ac 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/HandlebarsEngineAdapterTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/HandlebarsEngineAdapterTest.java @@ -1,9 +1,14 @@ package org.openapitools.codegen.templating; +import org.mockito.Mockito; +import org.openapitools.codegen.api.TemplatingExecutor; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static org.testng.Assert.*; +import java.io.IOException; +import java.util.Map; + +import static org.testng.Assert.assertEquals; public class HandlebarsEngineAdapterTest { @Test(dataProvider = "handlesFileExpectations") @@ -33,4 +38,73 @@ public Object[][] handlesFileExpectations() { {"README.md", false, "Should not attempt to handle non-handlebars extensions (other than mustache)"} }; } + + @Test(description = "verify https://github.com/jknack/handlebars.java/issues/940#issue-1111612043 is fixed") + public void testHandlePartialTemplate() throws IOException { + // Given + HandlebarsEngineAdapter adapter = new HandlebarsEngineAdapter(); + TemplatingExecutor executorMock = Mockito.mock(TemplatingExecutor.class); + Mockito.when(executorMock.getFullTemplateContents("outerTemplate.hbs")).thenReturn("Contents: {{>innerTemplate}}"); + Mockito.when(executorMock.getFullTemplateContents("innerTemplate.hbs")).thenReturn("'Specific contents'"); + + // When + String generatedFile = adapter.compileTemplate(executorMock, Map.of(), "outerTemplate.hbs"); + + // Then + assertEquals(generatedFile, "Contents: 'Specific contents'"); + } + + @Test(description = "should prioritize public getters over breaking encapsulation") + public void testResolverPriority() throws IOException { + // Given + HandlebarsEngineAdapter adapter = new HandlebarsEngineAdapter(); + TemplatingExecutor executorMock = Mockito.mock(TemplatingExecutor.class); + Mockito.when(executorMock.getFullTemplateContents("outerTemplate.hbs")).thenReturn( + "Contents: {{#propertyObj}}\n" + + " public getter: {{valueMethodAndBean}}\n" + + " public method: {{valueAndMethod}}\n" + + " private property: {{valueOnly}}{{/propertyObj}}"); + + Map bundle = Map.of("propertyObj", new PropertyObject()); + + // When + String generatedFile = adapter.compileTemplate(executorMock, bundle, "outerTemplate.hbs"); + + // Then + assertEquals(generatedFile, "Contents: \n" + + " public getter: get_raw_data1_formatted\n" + + " public method: raw_data2_formatted\n" + + " private property: raw_data3"); + } + + static class PropertyObject { + /** + * getter-exposed + */ + private final String valueMethodAndBean = "raw_data1"; + + public String valueMethodAndBean() { + return valueMethodAndBean + "_formatted"; + } + + public String getValueMethodAndBean() { + return "get_" + valueMethodAndBean(); + } + + /** + * method-exposed + */ + private final String valueAndMethod = "raw_data2"; + + public String valueAndMethod() { + return valueAndMethod + "_formatted"; + } + + /** + * private + * note: ideally we long-term move towards respecting encapsulation where possible + */ + @SuppressWarnings({"unused", "java:S1068"}) // this private value is still read by our HandleBars engine + private final String valueOnly = "raw_data3"; + } } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/handlebars/StringHelpersTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/handlebars/StringHelpersTest.java index d3594029159f..6faba5c5fc5b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/handlebars/StringHelpersTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/handlebars/StringHelpersTest.java @@ -21,7 +21,7 @@ private void evaluate(HashMap data, String template, String expe Context context = Context .newBuilder(data) .resolver( - FieldValueResolver.INSTANCE) + AccessAwareFieldValueResolver.INSTANCE) .build(); Template tmpl = handlebars.compileInline(template);