handlebars.java icon indicating copy to clipboard operation
handlebars.java copied to clipboard

FieldValueResolver leads to IllegalAccessException in 4.3.0

Open mdesharnais opened this issue 2 years ago • 21 comments

Hi,

the following code, wich uses the FieldValueResolver, leads to an InaccessibleObjectException.

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.context.FieldValueResolver;
import com.github.jknack.handlebars.io.FileTemplateLoader;

public final class Main {
  public static void main(final String[] args) throws Throwable {
    final var loader = new FileTemplateLoader(".", ".hbs");
    final var handlebars = new Handlebars(loader);

    final String str = handlebars.compile("foo").apply(
      Context.newBuilder(null).resolver(FieldValueResolver.INSTANCE).build());

    System.out.println(str);
  }
}

Assuming that this code is in the file Main.java and that, sitting next to it, the file foo.hbs contains the following.

AAA{{> bar}}CCC

And that the file bar.hbs contains the following.

BBB

Then the following command produces the exception.

$ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Debian-1deb11u2)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Debian-1deb11u2, mixed mode, sharing)
$ java -cp "./handlebars-4.3.0.jar:./slf4j-api-1.7.32.jar" Main.java                                   
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".                                                                                              
SLF4J: Defaulting to no-operation (NOP) logger implementation                                                                                                 
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.                                                                            
Exception in thread "main" com.github.jknack.handlebars.HandlebarsException: ./foo.hbs:1:7: java.lang.IllegalStateException: Shouldn't be illegal to access field 'size'                                                                                                                                                    
    ./foo.hbs:1:7                                                                                                                                             
        at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:217)                                                  
        at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:39)                                                   
        at com.github.jknack.handlebars.context.MemberValueResolver.resolve(MemberValueResolver.java:59)                                                      
        at com.github.jknack.handlebars.Context$CompositeValueResolver.resolve(Context.java:200)                                                              
        at com.github.jknack.handlebars.internal.path.PropertyPath.eval(PropertyPath.java:52)                                                                 
        at com.github.jknack.handlebars.Context$PathExpressionChain.next(Context.java:361)                                                                    
        at com.github.jknack.handlebars.Context$PathExpressionChain.eval(Context.java:381)                                                                    
        at com.github.jknack.handlebars.Context.get(Context.java:621)                                                                                         
        at com.github.jknack.handlebars.internal.Partial.override(Partial.java:253)                                                                           
        at com.github.jknack.handlebars.internal.Partial.merge(Partial.java:226)                                                                              
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)                                                                    
        at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)                                                                     
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)                                                                    
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)                                                                    
        at com.github.jknack.handlebars.internal.ForwardingTemplate.apply(ForwardingTemplate.java:100)                                                        
        at Main.main(Main.java:11)
Caused by: java.lang.IllegalStateException: Shouldn't be illegal to access field 'size'                                                                       
        at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:217)                                                  
        at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:39)                                                   
        at com.github.jknack.handlebars.context.MemberValueResolver.resolve(MemberValueResolver.java:59)                                                      
        at com.github.jknack.handlebars.Context$CompositeValueResolver.resolve(Context.java:200)                                                              
        at com.github.jknack.handlebars.internal.path.PropertyPath.eval(PropertyPath.java:52)                                                                 
        at com.github.jknack.handlebars.Context$PathExpressionChain.next(Context.java:361)                                                                    
        at com.github.jknack.handlebars.Context$PathExpressionChain.eval(Context.java:381)                                                                    
        at com.github.jknack.handlebars.Context.get(Context.java:621)          
        at com.github.jknack.handlebars.internal.Partial.override(Partial.java:253)                                                                           
        at com.github.jknack.handlebars.internal.Partial.merge(Partial.java:226)                                                                              
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)                                                                    
        at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)                                  
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
        at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)
        at com.github.jknack.handlebars.internal.ForwardingTemplate.apply(ForwardingTemplate.java:100)
        at Main.main(Main.java:11)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:419)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
Caused by: java.lang.IllegalAccessException: class com.github.jknack.handlebars.context.FieldValueResolver$FieldMembercannot access a member of class java.util.HashMap (in module java.base) with modifiers "transient"
        at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
        at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
        at java.base/java.lang.reflect.Field.checkAccess(Field.java:1102)
        at java.base/java.lang.reflect.Field.get(Field.java:423)
        at com.github.jknack.handlebars.context.FieldValueResolver$FieldMember.get(FieldValueResolver.java:135)
        at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:214)
        ... 22 more

mdesharnais avatar Jan 22 '22 16:01 mdesharnais

@mdesharnais I get this as well. The underlying problem seems to be that the access restrictions are stronger in Java 17. I resolved it by overriding FieldResolver like so:

    @Override
    public boolean matches(FieldWrapper field, String name) {
        return !isStatic(field) && field.getName().equals(name) && isPublic(field);
    }

Presumably, this will be a problem for any templates that have assumed access to private fields.

jhannes avatar Feb 22 '22 10:02 jhannes

Guys,

You need to stop accessing private fields in Java 17. Field must be public or you need a getter method.

jknack avatar Feb 22 '22 11:02 jknack

Hey @jknack: This happens when you pass a map into handlebars. We're not the ones using private fields, you are ;-)

I tried the following, where example.txt.hbs just is a simple import so it copies the context:

        Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader());
        Template tmpl = handlebars.compile("example.txt");

        System.out.println(tmpl.apply(Context
                .newBuilder(new HashMap<String, String>(Map.of()))
                .resolver(FieldValueResolver.INSTANCE)
                .build()));

The result is the same as @mdesharnais 's stack trace above.

jhannes avatar Feb 22 '22 12:02 jhannes

@jhannes Look here. It checks what Java version are you running and setup the value resolvers properly.

This work:

Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader());
        Template tmpl = handlebars.compile("example.txt");

        System.out.println(tmpl.apply(Context
                .newBuilder(new HashMap<String, String>(Map.of()))
                .build()));

Because it uses the default value resolvers. Now if you override that and just add FieldValueResolver, then it won't work. If yo still need to access to public field then add it to the end:

Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader());
        Template tmpl = handlebars.compile("example.txt");

        System.out.println(tmpl.apply(Context
                .newBuilder(new HashMap<String, String>(Map.of()))
                .resolver(MapValueResolver.INSTANCE,
          JavaBeanValueResolver.INSTANCE, MethodValueResolver.INSTANCE, FieldValueResolver.INSTANCE)
                .build()));

jknack avatar Feb 22 '22 12:02 jknack

@jknack Thanks for the clarification. This looks good. However, I can't get it to work properly in my context, which is OpenAPI Generator

I've tried overriding OpenAPI's adaptor and remove the call to .resolver(), but then it breaks majorly (lots of data is omitted). Overriding FieldResolver with the isPublic check works.

jhannes avatar Feb 22 '22 13:02 jhannes

Odd. We can probably add the public check on Java 14 or higher. But again, people need to understand this issue is part of upgrading to new JVM.

jknack avatar Feb 22 '22 14:02 jknack

I totally agree. Private fields are going away. But at the moment, there is no migration path for e.g. openapi-generator. I'll examine the behavior a bit more so I can create a better reproduction

jhannes avatar Feb 22 '22 14:02 jhannes

@jknack I have made some more research into the OpenAPI with Java 17 issue and have found an example that mimics the issue.

The following test fails to generate output:

class FixedHandlebarsEngineAdapterTest {

    public static class MyFieldValueResolver extends FieldValueResolver {

        @Override
        public boolean matches(FieldWrapper field, String name) {
            return super.matches(field, name) && !isTransient(field);
        }

        protected boolean isTransient(FieldWrapper member) {
            return Modifier.isTransient(member.getModifiers());
        }
    }

    @Test
    void java17failure() throws IOException {
        HashMap<Object, Object> bundle = exampleModel();

        Context context = Context
                .newBuilder(bundle)
                // Commenting in the following will make it work
                /*
                .resolver(
                        MapValueResolver.INSTANCE,
                        JavaBeanValueResolver.INSTANCE,
                        new MyFieldValueResolver())

                 */
                .build();

        Handlebars handlebars = new Handlebars();
        // Commenting out this will make it work again
        /*
         */
        handlebars.registerHelperMissing((obj, options) -> {
            System.err.printf(Locale.ROOT, "Unregistered helper name '%s', processing template:%n%s%n", options.helperName, options.fn.text());
            return "";
        });
        handlebars.getLoader().setPrefix("/typescript-fetch-api/");
        handlebars.getLoader().setSuffix(".handlebars");
        handlebars.registerHelper("json", Jackson2Helper.INSTANCE);
        StringHelpers.register(handlebars);
        handlebars.registerHelpers(ConditionalHelpers.class);
        handlebars.registerHelpers(org.openapitools.codegen.templating.handlebars.StringHelpers.class);
        Template tmpl = handlebars.compile("model");

        String output = tmpl.apply(context);
        System.out.println(output);
        assert !output.isBlank();
    }

    private HashMap<Object, Object> exampleModel() {
        HashMap<Object, Object> bundle = new HashMap<>();
        ArrayList<Object> models = new ArrayList<>();
        bundle.put("models", models);
        Map<String, Object> petDto = new HashMap<>();
        petDto.put("importPath", "PetDto");
        CodegenModel petModel = new CodegenModel();
        petModel.classname = "PetDto";
        petModel.parent = "AnimalDto";
        petModel.isEnum = false;
        CodegenProperty property = new CodegenProperty();
        property.name = "petName";
        property.dataType = "string";
        petModel.vars.add(property);
        petDto.put("model", petModel);
        models.add(petDto);
        return bundle;
    }
}

Given the following templates:

{{#models}}{{#model}}{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{>modelGeneric}}{{/isEnum}}{{/model}}{{/models}}
export interface {{classname}} {{#if parent}}extends {{{parent}}} {{/if}}{

{{~#vars}}
    {{~#if description}}
    /**
     * {{{description}}}
     */
    {{~/if}}
    {{#isReadOnly}}readonly {{/isReadOnly}}{{name}}{{^required}}?{{/required}}{{#required}}{{#isReadOnly}}?{{/isReadOnly}}{{/required}}: {{#if isEnum}}{{{datatypeWithEnum}}}{{else if isFile}}Blob{{else}}{{{dataType}}}{{/if}}{{#isNullable}} | null{{/isNullable}};
{{~/vars}}
}

The issue seems to be composed of several problems and I don't know why it doesn't work.

I'm wondering if Block.java is checking for it == null when it should be checking for helper == null.

As Handlebars use in OpenAPI Generator is an important use case for the project, I think this is could be an important issue

jhannes avatar Feb 23 '22 16:02 jhannes

Hi @jknack @jhannes, I tried to add the FieldValueResolver at the end but then encountered a new problematic situation when accessing an element from the outer context that happens to have the same name as an unexposed field of the class behind the current context.

import java.util.Map;
import java.util.List;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.context.FieldValueResolver;
import com.github.jknack.handlebars.context.JavaBeanValueResolver;
import com.github.jknack.handlebars.context.MapValueResolver;
import com.github.jknack.handlebars.context.MethodValueResolver;

public final class Main {
  public static void main(final String[] args) throws Throwable {
    final var handlebars = new Handlebars();

    final String str = handlebars
      .compileInline("{{value}}{{#strings}} {{value}}.{{.}}{{/strings}}")
      .apply(
        Context.newBuilder(
          Map.of(
            "value", "aaa",
            "strings", List.of("bbb", "ccc")))
          .resolver(
            MapValueResolver.INSTANCE,
            JavaBeanValueResolver.INSTANCE,
            MethodValueResolver.INSTANCE,
            FieldValueResolver.INSTANCE)
          .build());

    System.out.println(str);
  }
}

I would expect the output to be "aaa aaa.bbb aaa.ccc", but get the same IllegalAccessException again. Apparently, the String class has an internal field with the same name as an object of my Handlebars context.

$ java -version
openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Debian-1deb11u1)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Debian-1deb11u1, mixed mode, sharing)
$ java -cp "./handlebars-4.3.0.jar:./slf4j-api-1.7.32.jar" Main.java
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" com.github.jknack.handlebars.HandlebarsException: inline@7fe0618c:1:24: java.lang.IllegalStateException: Shouldn't be illegal to access field 'value'
    inline@7fe0618c:1:24
	at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:217)
	at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:39)
	at com.github.jknack.handlebars.context.MemberValueResolver.resolve(MemberValueResolver.java:59)
	at com.github.jknack.handlebars.Context$CompositeValueResolver.resolve(Context.java:200)
	at com.github.jknack.handlebars.internal.path.PropertyPath.eval(PropertyPath.java:52)
	at com.github.jknack.handlebars.Context$PathExpressionChain.next(Context.java:361)
	at com.github.jknack.handlebars.Context$PathExpressionChain.eval(Context.java:381)
	at com.github.jknack.handlebars.Context.get(Context.java:618)
	at com.github.jknack.handlebars.internal.Variable.value(Variable.java:169)
	at com.github.jknack.handlebars.internal.Variable.merge(Variable.java:146)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)
	at com.github.jknack.handlebars.Options.apply(Options.java:538)
	at com.github.jknack.handlebars.helper.EachHelper.apply(EachHelper.java:73)
	at com.github.jknack.handlebars.internal.Block.merge(Block.java:211)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)
	at com.github.jknack.handlebars.internal.ForwardingTemplate.apply(ForwardingTemplate.java:100)
	at Main.main(Main.java:17)
Caused by: java.lang.IllegalStateException: Shouldn't be illegal to access field 'value'
	at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:217)
	at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:39)
	at com.github.jknack.handlebars.context.MemberValueResolver.resolve(MemberValueResolver.java:59)
	at com.github.jknack.handlebars.Context$CompositeValueResolver.resolve(Context.java:200)
	at com.github.jknack.handlebars.internal.path.PropertyPath.eval(PropertyPath.java:52)
	at com.github.jknack.handlebars.Context$PathExpressionChain.next(Context.java:361)
	at com.github.jknack.handlebars.Context$PathExpressionChain.eval(Context.java:381)
	at com.github.jknack.handlebars.Context.get(Context.java:618)
	at com.github.jknack.handlebars.internal.Variable.value(Variable.java:169)
	at com.github.jknack.handlebars.internal.Variable.merge(Variable.java:146)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)
	at com.github.jknack.handlebars.Options.apply(Options.java:538)
	at com.github.jknack.handlebars.helper.EachHelper.apply(EachHelper.java:73)
	at com.github.jknack.handlebars.internal.Block.merge(Block.java:211)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.TemplateList.merge(TemplateList.java:95)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:126)
	at com.github.jknack.handlebars.internal.BaseTemplate.apply(BaseTemplate.java:114)
	at com.github.jknack.handlebars.internal.ForwardingTemplate.apply(ForwardingTemplate.java:100)
	at Main.main(Main.java:17)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:419)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
Caused by: java.lang.IllegalAccessException: class com.github.jknack.handlebars.context.FieldValueResolver$FieldMember cannot access a member of class java.lang.String (in module java.base) with modifiers "private final"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
	at java.base/java.lang.reflect.Field.checkAccess(Field.java:1102)
	at java.base/java.lang.reflect.Field.get(Field.java:423)
	at com.github.jknack.handlebars.context.FieldValueResolver$FieldMember.get(FieldValueResolver.java:135)
	at com.github.jknack.handlebars.context.FieldValueResolver.invokeMember(FieldValueResolver.java:214)
	... 29 more

Considering that there is no way to know or control the fields unexposed by third party code, I argue that FieldValueResolver should treat illegal access as if the field did not exist.

mdesharnais avatar Mar 13 '22 13:03 mdesharnais

Considering that there is no way to know or control the fields unexposed by third party code, I argue that FieldValueResolver should treat illegal access as if the field did not exist.

I second @mdesharnais 's proposal here

jhannes avatar Mar 15 '22 08:03 jhannes

Field must be public in modern JVM. You can see the root cause here:

Caused by: java.lang.IllegalAccessException: class com.github.jknack.handlebars.context.FieldValueResolver$FieldMember cannot access a member of class java.lang.String (in module java.base) with modifiers "private final"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)

That is the JVM throwing the error, it isn't something the handlebars.java does. If the field doesn't exist, handlebars.java skip the resolver and execute next on the chain/pipeline.

Are you asking to ignore existing non-public fields on modern JVM?

jknack avatar Mar 15 '22 09:03 jknack

Sorry for the delayed answer. Yes, I am asking for the FieldValueResolver to ignore non-public fields.

mdesharnais avatar Mar 24 '22 19:03 mdesharnais

@jknack we probably could do something like the same trick we did here: https://github.com/jknack/handlebars.java/commit/a7adb1030d9dadc653f4ae89c13a28e62f8237b8#diff-9fd22b23d4adba304bdb22b65fe8094ba8b0f45943cb330a672b1dbaf52e5d97

Basically ignore field access to any class that is in the "java." or "sun." package including possibly even public ones (because in a modular world even if they are public they may not be open).

Otherwise we would have to use the modules API which would not work for JDK 8 compile.

agentgt avatar Mar 30 '22 13:03 agentgt

The other option is to include modern value resolvers that are module aware in a separate maven module (not java module but maven) and compile them with JDK 11/17 (E.g. handlebars-jdk17-value-resolvers).

Then in the JDK 8 code base you could use the Service Loader to load them up (to avoid using weird reflection hacks or check java version). This would require a new SPI.

agentgt avatar Mar 30 '22 14:03 agentgt

Sorry for the delayed answer. Yes, I am asking for the FieldValueResolver to ignore non-public fields.

Just so we are clear... that will not always work. I field can be public but still not accessible. You need to see if the class is public as well as the module is open for reflection.

Anyway jknack chose the correct behavior on not including the FieldValueResolver as a default precisely because of these problems. With records now ideally one should not use field value resolver for struct objects anyway (albeit I get backwards compatibility).

agentgt avatar Mar 30 '22 15:03 agentgt

Hi @agentgt, thanks for looking into that.

Sorry for the delayed answer. Yes, I am asking for the FieldValueResolver to ignore non-public fields.

Just so we are clear... that will not always work. I field can be public but still not accessible. You need to see if the class is public as well as the module is open for reflection.

You are correct, I should have written, that I am asking for the FieldValueResolver to ignore fields not part of the object's published API, which should take public/non-public view as well as module view in consideration.

mdesharnais avatar Mar 30 '22 15:03 mdesharnais

OK I see the issue. The cache still has members that are not accessible.

  private Map<String, M> cache(final Class<?> clazz) {
    Map<String, M> mcache = this.cache.get(clazz);
    if (mcache == null) {
      mcache = new HashMap<>();
      Set<M> members = members(clazz);
      for (M m : members) {
        // Mark as accessible.
        if (isUseSetAccessible(m) && m instanceof AccessibleObject) {
          ((AccessibleObject) m).setAccessible(true);
        }
////// WE are still adding the member
        mcache.put(memberName(m), m);
      }
      this.cache.put(clazz, mcache);
    }
    return mcache;
  }

One of the reasons I was looking into this is I made some of the reflection changes and the above is sort of a bug.

Anyway @jknack It looks we first need to check if its a setAccessibleObject. If it is not then we add it to the cache. If it is then check if isUseSetAccessible and if that is true setAccessible and then add it to the cache.

  private Map<String, M> cache(final Class<?> clazz) {
    Map<String, M> mcache = this.cache.get(clazz);
    if (mcache == null) {
      mcache = new HashMap<>();
      Set<M> members = members(clazz);
      for (M m : members) {
        // Mark as accessible.
        if (m instanceof AccessibleObject) {
          if (isUseSetAccessible(m)) {
            ((AccessibleObject) m).setAccessible(true);
            mcache.put(memberName(m), m);
          }
        }
        else {
          mcache.put(memberName(m), m);
        }
      }
      this.cache.put(clazz, mcache);
    }
    return mcache;
  }

agentgt avatar Mar 30 '22 23:03 agentgt

Anyway a work around is this Field Value Resolver:

         var MY_FIELD_VALUE_RESOLVER = new FieldValueResolver() {

            @Override
            protected Set<FieldWrapper> 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;
            }
        };

You will need to java 8 if thats your target.

agentgt avatar Mar 31 '22 00:03 agentgt

As I mentioned here https://github.com/jknack/handlebars.java/pull/948#issuecomment-1090529948

We really cannot change FieldValueResolver otherwise folks will get very bizarre behavior on upgrades to newer JDKs without any errors (ie parent contexts now taking over illegal access fields).

And they will file bugs like #951 that are very hard to reproduce without their complete object graph.

The real solution is too deprecate FieldValueResolver in a minor version release. Then in a major version we rename FieldValueResolver to LegacyFieldValueResolver forcing folks code to break which is easier to fix and understand than templates breaking.

We then add another module to this project compiling FieldValueResolver with Java 9 or whatever using trySetAccessible. That module is an optional module (by module I mean maven module) that folks can add as a dependency.

agentgt avatar Apr 06 '22 17:04 agentgt

@jhannes Look here. It checks what Java version are you running and setup the value resolvers properly.

This work:

Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader());
        Template tmpl = handlebars.compile("example.txt");

        System.out.println(tmpl.apply(Context
                .newBuilder(new HashMap<String, String>(Map.of()))
                .build()));

Because it uses the default value resolvers. Now if you override that and just add FieldValueResolver, then it won't work. If yo still need to access to public field then add it to the end:

Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader());
        Template tmpl = handlebars.compile("example.txt");

        System.out.println(tmpl.apply(Context
                .newBuilder(new HashMap<String, String>(Map.of()))
                .resolver(MapValueResolver.INSTANCE,
          JavaBeanValueResolver.INSTANCE, MethodValueResolver.INSTANCE, FieldValueResolver.INSTANCE)
                .build()));

Thanks for your help. It's working now.

sribalajivelan-icanio avatar Dec 27 '22 08:12 sribalajivelan-icanio

Is this included in 4.3.1? I still get "com.github.jknack.handlebars.HandlebarsException: baseApi.handlebars:4:3: java.lang.IllegalStateException: Shouldn't be illegal to access field 'size'"

jhannes avatar Jan 15 '23 18:01 jhannes