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

JavaBeanValueResolver does not work for all uppercase JavaBean accessors

Open bertramn opened this issue 5 years ago • 5 comments

We have some JAXB generated classes that have accessors that are all uppercase. Unfortunately when we try to use these accessors in handlbars we have to do this weird thing of lowercasing the first character and then continue all uppercase

e.g. to call Person.getSURNAME() we have to write {{ person.sURNAME }}.

This is neither intuitive nor working as one would expect.I tripled checked and all uppercase naming is compliant with the JavaBean Specification

Specifically section 8.8 states:

However to support the occasional use of all upper-case names, we check if the
first two characters of the name are both upper case and if so leave it alone.

So for example,
“FooBah” becomes “fooBah”
“Z” becomes “z”
“URL” becomes “URL”

To comply with the spec looks like this lowercase/uppercase conversion stuff would also need to consider if the property name (without is/get) is all upper case then the property name remains all uppercase.

Or even maybe it is sufficient enough to do a equalsIgnoreCase() at line 51-52 ??

Here a simple test to demonstrate this issue:

package com.virginaustralia.wiremock.sabre.scr.v1_0_0;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Template;
import org.junit.Test;

import java.io.IOException;

import static com.github.jknack.handlebars.Context.newContext;
import static java.util.Collections.singletonMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

public class HandleBarTest {

  private final Handlebars handlebars = new Handlebars();

  private final Context context = newContext(singletonMap("person", new Person("Napoleon", "Bonaparte")));

  @Test
  public void isShouldProduceSurnameUsingJavaBeanAccessor() throws IOException {
    String accessor = "person.SURNAME";
    assertEquals("expected '" + accessor + "' to work according to the JavaBean Spec", "Bonaparte", process("{{ " + accessor + " }}"));
  }

  @Test
  public void itShouldNotProduceSurname() throws Exception {
    String accessor = "person.sURNAME";
    assertNotEquals("not expected '" + accessor + "' to produce the surname according to the JavaBean Spec", "Bonaparte", process("{{ " + accessor + " }}"));
  }

  @Test
  public void itShouldNotProduceSurnameAllLowerCase() throws IOException {
    String accessor = "person.surname";
    assertNotEquals("not expected '" + accessor + "' to produce the surname according to the JavaBean Spec", "Bonaparte", process("{{ " + accessor + " }}"));
  }

  private String process(String template) throws IOException {
    Template tmpl = handlebars.compileInline(template);
    return tmpl.apply(context);
  }

  private static class Person {

    private String firstname;

    private String surname;

    public Person(String firstname, String surname) {
      this.firstname = firstname;
      this.surname = surname;
    }

    public String getFirstname() {
      return firstname;
    }

    public void setFirstname(String firstname) {
      this.firstname = firstname;
    }

    public String getSURNAME() {
      return surname;
    }

    public void setSURNAME(String surname) {
      this.surname = surname;
    }

  }

}

bertramn avatar Jan 16 '19 01:01 bertramn

install/add the MethodValueResolver

that will give you access to those names, but you have to use complete method name:

{{person.getSURNAME}}

jknack avatar Jan 16 '19 13:01 jknack

The {{person.sURNAME}} also works once one understands that there is a defect in the JavaBeanValueResolver. I wanted to point out that the JavaBeanValueResolver does not properly function when using a valid and documented JavaBean property name that is all upper case.

bertramn avatar Jan 19 '19 11:01 bertramn

Whats even worse is this at one point worked. We have some older code using a really old handlebars and SURNAME would work.

agentgt avatar Oct 01 '19 00:10 agentgt

Anyway the canonical approach is to use Instrospector.decapitalize but that only goes from method -> property name (after get or is removed).

agentgt avatar Oct 01 '19 00:10 agentgt

@bertramn and @jknack Here is a trivial, and Java Bean Compliant Value Resolver:

import java.beans.Introspector;
import java.lang.reflect.Method;

import com.github.jknack.handlebars.context.JavaBeanValueResolver;

public class CustomHandlebarsBeanValueResolver extends JavaBeanValueResolver {

	public static final CustomHandlebarsBeanValueResolver INSTANCE = new CustomHandlebarsBeanValueResolver();
	
	/**
	 * The 'is' prefix.
	 */
	private static final String IS_PREFIX = "is";

	/**
	 * The 'get' prefix.
	 */
	private static final String GET_PREFIX = "get";

	@Override
	public boolean matches(
			final Method method,
			final String name) {
		boolean isStatic = isStatic(method);
		boolean isPublic = isPublic(method);

		boolean isProperty = name.equals(propertyName(method));

		return !isStatic && isPublic && isProperty;
	}

	//TODO cache?
	private static String propertyName(
			final Method member) {
		if (member.getParameterTypes().length != 0) {
			return null;
		}
		String name = member.getName();
		int length = name.length();
		if (length > 3 && name.startsWith(GET_PREFIX)) {
			name = Introspector.decapitalize(name.substring(3));
		}
		else if ( length > 2 && name.startsWith(IS_PREFIX)) {
			name = Introspector.decapitalize(name.substring(2));
		}
		else {
			name = null;
		}
		return name;
	}
	
	@Override
	protected String memberName(
			final Method member) {
		String propertyName = propertyName(member);
		if (propertyName != null) {
			return propertyName;
		}
		return member.getName();
	}

}

agentgt avatar Oct 01 '19 01:10 agentgt