Details

    • Type: Improvement
    • Status: Closed
    • Priority: Minor
    • Resolution: Fixed
    • Affects Version/s: 2.3.24-incubating
    • Fix Version/s: 2.3.26-incubating
    • Component/s: engine
    • Labels:
      None

      Description

      The default object method model currently does not support default methods:
      https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

      Here is the current error:

      FTL stack trace ("~" means nesting-related):
      	- Failed at: #if task.switchable  [in template "...../home.ftl" at line 43, column 25]
      ----
      	at freemarker.core.InvalidReferenceException.getInstance(InvalidReferenceException.java:131)
      	at freemarker.core.UnexpectedTypeException.newDesciptionBuilder(UnexpectedTypeException.java:77)
      	at freemarker.core.UnexpectedTypeException.<init>(UnexpectedTypeException.java:40)
      	at freemarker.core.NonBooleanException.<init>(NonBooleanException.java:44)
      	at freemarker.core.Expression.modelToBoolean(Expression.java:142)
      	at freemarker.core.Expression.evalToBoolean(Expression.java:125)
      	at freemarker.core.Expression.evalToBoolean(Expression.java:110)
      	at freemarker.core.ConditionalBlock.accept(ConditionalBlock.java:46)
      	at freemarker.core.Environment.visit(Environment.java:324)
      	at freemarker.core.MixedContent.accept(MixedContent.java:54)
      	at freemarker.core.Environment.visitByHiddingParent(Environment.java:345)
      	at freemarker.core.IteratorBlock$IterationContext.executeNestedBlockInner(IteratorBlock.java:240)
      	at freemarker.core.IteratorBlock$IterationContext.executeNestedBlock(IteratorBlock.java:220)
      	at freemarker.core.IteratorBlock$IterationContext.accept(IteratorBlock.java:194)
      	at freemarker.core.Environment.visitIteratorBlock(Environment.java:572)
      	at freemarker.core.IteratorBlock.acceptWithResult(IteratorBlock.java:78)
      	at freemarker.core.IteratorBlock.accept(IteratorBlock.java:64)
      	at freemarker.core.Environment.visit(Environment.java:324)
      	at freemarker.core.MixedContent.accept(MixedContent.java:54)
      	at freemarker.core.Environment.visit(Environment.java:324)
      	at freemarker.core.Environment.process(Environment.java:302)
      	at freemarker.template.Template.process(Template.java:325)
      	at com.qwazr.tools.FreeMarkerTool.template(FreeMarkerTool.java:86)
      	at com.qwazr.tools.FreeMarkerTool.template(FreeMarkerTool.java:101)
      

      To reproduce this.

      1. Use an interface

      public interface UpdatableTask extends Runnable {
      
          default boolean isSwitchable() {
              return false;
          }
      }
      

      2. Use an object that implements this interface with the following template:

      <#if task.switchable>
      ...
      </#if>
      

        Activity

        Hide
        ddekany Daniel Dekany added a comment -

        Note that that's the duplicate of https://sourceforge.net/p/freemarker/feature-requests/92/ (see some details there). That's the old tracker, so no problem, leave this open.

        Show
        ddekany Daniel Dekany added a comment - Note that that's the duplicate of https://sourceforge.net/p/freemarker/feature-requests/92/ (see some details there). That's the old tracker, so no problem, leave this open.
        Hide
        quaff zhouyanming added a comment -

        Daniel Dekany Can you make a workaround ASAP?

        Show
        quaff zhouyanming added a comment - Daniel Dekany Can you make a workaround ASAP?
        Hide
        ddekany Daniel Dekany added a comment -

        Not nowadays, sorry. I'm quite preoccupied elsewhere. But I realize that it's a high priority issue.

        Show
        ddekany Daniel Dekany added a comment - Not nowadays, sorry. I'm quite preoccupied elsewhere. But I realize that it's a high priority issue.
        Hide
        robotdan Daniel DeGroff added a comment -

        In the original issue linked above on SourceForge Daniel Dekany indicates FreeMarker is using java.bean.Introspector.

        There is an open JDK bug for the java.bean.Introspector ignoring default methods. JDK-8071693 If this is the root cause then this issue affects all FreeMarker versions and is not a FreeMarker bug. I have observed it on version 2.3.23.

        Show
        robotdan Daniel DeGroff added a comment - In the original issue linked above on SourceForge Daniel Dekany indicates FreeMarker is using java.bean.Introspector . There is an open JDK bug for the java.bean.Introspector ignoring default methods. JDK-8071693 If this is the root cause then this issue affects all FreeMarker versions and is not a FreeMarker bug. I have observed it on version 2.3.23 .
        Hide
        ddekany Daniel Dekany added a comment -

        Yes, it's caused by that JDK bug (it's also pointed out in the comments of the SourceForge page you have linked).

        It's likely that FM would benefit from an own introspector (as a configuration option). Users often ran into problems because the Java Beans rules aren't relaxed enough for the purposes of FM. Like, Boolean isFoo() is not a bean property getter, getXCode() is not accessibly as bean.xCode, only as bean.XCode, and such. Also java.bean.Introspector is not accessible on Android. So an own introspector could solve those, and the default method issue too.

        Show
        ddekany Daniel Dekany added a comment - Yes, it's caused by that JDK bug (it's also pointed out in the comments of the SourceForge page you have linked). It's likely that FM would benefit from an own introspector (as a configuration option). Users often ran into problems because the Java Beans rules aren't relaxed enough for the purposes of FM. Like, Boolean isFoo() is not a bean property getter, getXCode() is not accessibly as bean.xCode , only as bean.XCode , and such. Also java.bean.Introspector is not accessible on Android. So an own introspector could solve those, and the default method issue too.
        Hide
        abrin adam brin added a comment -

        FYI – if the issue is a JDK bug, it's also in the Oracle JDK too.

        Show
        abrin adam brin added a comment - FYI – if the issue is a JDK bug, it's also in the Oracle JDK too.
        Hide
        eunice_ Aron Wieck added a comment - - edited

        Currently, ClassIntrospector discovers methods using beanInfo.getPropertyDescriptors();

                Map<String, PropertyDescriptor> propertyDescriptors = new LinkedHashMap<>();
        
                PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors();
                if (pda != null)
                    for (PropertyDescriptor pd : pda)
                        propertyDescriptors.put(pd.getName(), pd);
        

        A quick and dirty fix would be to manually add default Methods:

        
                Map<String, Method> getters = new HashMap<>();
                Map<String, Method> setters = new HashMap<>();
                Set<String> props = new LinkedHashSet<>();
                for (Method method : clazz.getMethods())
                    if (method.isDefault()) {
                        final String methodName = method.getName();
        
                        boolean getter = true;
                        String name = null;
        
                        if (methodName.startsWith("is") && methodName.length() > 2) {
                            name = Introspector.decapitalize(methodName.substring(2));
                            getter = true;
                        } else if (methodName.startsWith("get") && methodName.length() > 3) {
                            name = Introspector.decapitalize(methodName.substring(3));
                            getter = true;
                        } else if (methodName.startsWith("set") && methodName.length() > 3) {
                            name = Introspector.decapitalize(methodName.substring(3));
                            getter = false;
                        }
        
                        if (name != null) {
                            props.add(name);
                            if (getter)
                                getters.put(name, method);
                            else
                                setters.put(name, method);
                        }
                    }
                for (String prop : props) {
                    PropertyDescriptor orig = propertyDescriptors.get(prop);
                    Method readMethod = orig != null && orig.getReadMethod() != null ? orig.getReadMethod() : getters.get(prop);
                    Method writeMethod = orig != null && orig.getWriteMethod() != null ? orig.getWriteMethod() : setters.get(prop);
                    propertyDescriptors.put(prop, new PropertyDescriptor(prop, readMethod, writeMethod));
        
                }
        
        Show
        eunice_ Aron Wieck added a comment - - edited Currently, ClassIntrospector discovers methods using beanInfo.getPropertyDescriptors(); Map< String , PropertyDescriptor> propertyDescriptors = new LinkedHashMap<>(); PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors(); if (pda != null ) for (PropertyDescriptor pd : pda) propertyDescriptors.put(pd.getName(), pd); A quick and dirty fix would be to manually add default Methods: Map< String , Method> getters = new HashMap<>(); Map< String , Method> setters = new HashMap<>(); Set< String > props = new LinkedHashSet<>(); for (Method method : clazz.getMethods()) if (method.isDefault()) { final String methodName = method.getName(); boolean getter = true ; String name = null ; if (methodName.startsWith( "is" ) && methodName.length() > 2) { name = Introspector.decapitalize(methodName.substring(2)); getter = true ; } else if (methodName.startsWith( "get" ) && methodName.length() > 3) { name = Introspector.decapitalize(methodName.substring(3)); getter = true ; } else if (methodName.startsWith( "set" ) && methodName.length() > 3) { name = Introspector.decapitalize(methodName.substring(3)); getter = false ; } if (name != null ) { props.add(name); if (getter) getters.put(name, method); else setters.put(name, method); } } for ( String prop : props) { PropertyDescriptor orig = propertyDescriptors.get(prop); Method readMethod = orig != null && orig.getReadMethod() != null ? orig.getReadMethod() : getters.get(prop); Method writeMethod = orig != null && orig.getWriteMethod() != null ? orig.getWriteMethod() : setters.get(prop); propertyDescriptors.put(prop, new PropertyDescriptor(prop, readMethod, writeMethod)); }
        Hide
        ddekany Daniel Dekany added a comment -

        Quoting from the version history: Added workaround (not enabled by default) to expose Java 8 default methods (and the bean properties they define) to templates, despite that java.beans.Introspector (the official JavaBeans introspector) ignores them, at least as of JRE 1.8.0_66. To enable this workaround, either increase the value of the incompatibleImprovements constructor argument of DefaultObjectWrapper or BeansWrapper the used to 2.3.26, or set its treatDefaultMethodsAsBeanMembers setting to true. Note that if you leave the object_wrapper setting of the Configuration on its default, it's enough to increase the incompatibleImprovements setting of the Configuration to 2.3.26, as that's inherited by the default object_wrapper.

        Show
        ddekany Daniel Dekany added a comment - Quoting from the version history: Added workaround (not enabled by default) to expose Java 8 default methods (and the bean properties they define) to templates, despite that java.beans.Introspector (the official JavaBeans introspector) ignores them, at least as of JRE 1.8.0_66. To enable this workaround, either increase the value of the incompatibleImprovements constructor argument of DefaultObjectWrapper or BeansWrapper the used to 2.3.26, or set its treatDefaultMethodsAsBeanMembers setting to true. Note that if you leave the object_wrapper setting of the Configuration on its default, it's enough to increase the incompatibleImprovements setting of the Configuration to 2.3.26, as that's inherited by the default object_wrapper.
        Hide
        ddekany Daniel Dekany added a comment -

        And of course, as this was a non-trivial change (rarely used things like indexed properties complicate it) please test this on your own systems!

        Show
        ddekany Daniel Dekany added a comment - And of course, as this was a non-trivial change (rarely used things like indexed properties complicate it) please test this on your own systems!
        Hide
        ddekany Daniel Dekany added a comment -

        The planned release binaries (under voting ATM, so it's not too late to stop the process):
        https://dist.apache.org/repos/dist/dev/incubator/freemarker/engine/2.3.26-incubating/binaries/

        Show
        ddekany Daniel Dekany added a comment - The planned release binaries (under voting ATM, so it's not too late to stop the process): https://dist.apache.org/repos/dist/dev/incubator/freemarker/engine/2.3.26-incubating/binaries/

          People

          • Assignee:
            ddekany Daniel Dekany
            Reporter:
            ekeller Emmanuel Keller
          • Votes:
            4 Vote for this issue
            Watchers:
            6 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development