Question

Consider this case.

You have a class which you cannot change or extend in any way.

public class Foo {
    ...
    private Boolean bar;
    ...
}

You need to edit the fields of that class via BeanEditor, but the logic behind that class allows and uses the fact that Boolean can have, so to say, 3 states: null, true and false.

Tapestry will, however, give you a checkbox with only 2 options, true or false.

So, people online suggest that you convert your Boolean type property to BooleanExtendedEnum type property which could represent three way logic.

public enum BooleanExtendedEnum {
    UNDEFINED(null),
    TRUE(Boolean.TRUE),
    FALSE(Boolean.FALSE);

    private Boolean booleanValue;

    private static Map<Boolean, BooleanExtendedEnum> booleanToExtendedMap = new HashMap<Boolean, BooleanExtendedEnum>();


    static {
        for (BooleanExtendedEnum be : BooleanExtendedEnum.values()) {
            booleanToExtendedMap.put(be.booleanValue, be);
        }
    }

    private BooleanExtendedEnum(Boolean booleanValue) {
        this.booleanValue = booleanValue;
    }

    public Boolean getBooleanValue() {
        return booleanValue;
    }

    public static BooleanExtendedEnum getBooleanExtendedValue(Boolean booleanInput) {
        return booleanToExtendedMap.get(booleanInput);
    }

}

Since you cannot change your Foo class, you'll need to create a coercer for Boolean <=> BooleanExtendedEnum.

Coercion<Boolean, BooleanExtendedEnum> threeWayBooleanToExtended = new Coercion<Boolean, BooleanExtendedEnum>() {
    @Override
    public BooleanExtendedEnum coerce(Boolean input) {
        if (input == null) {
            return BooleanExtendedEnum.UNDEFINED;
        } else {
            return BooleanExtendedEnum.getBooleanExtendedEnumValue(input);
        }
    }
};

Coercion<BooleanExtendedEnum, Boolean> threeWayExtendedToBoolean = new Coercion<BooleanExtendedEnum, Boolean>() {
    @Override
    public Boolean coerce(BooleanExtendedEnum input) {
        if (input == null) {
            return null;
        } else {
            return input.getBooleanValue();
        }
    }
};

configuration.add(new CoercionTuple<Boolean, BooleanExtendedEnum>(Boolean.class, BooleanExtendedEnum.class, threeWayBooleanToExtended));
configuration.add(new CoercionTuple<BooleanExtendedEnum, Boolean>(BooleanExtendedEnum.class, Boolean.class, threeWayExtendedToBoolean));

Let's assume you have done something as simple as this in your BeanEditor in your tml:

<p:bar>
    <div class="t-beaneditor-row">
        <label>Bar Value</label>
        <t:select t:id="fooBar" t:value="foo.bar" t:model="booleanExtendedSelectModel" t:blankOption="NEVER"/>
    </div>
</p:bar>

... and provided the SelectModel like this:

public SelectModel getBooleanExtendedSelectModel() {
    return new EnumSelectModel(BooleanExtendedEnum.class, messages);
}

Tapestry will create a drop-down list with three options

  • Undefined
  • True
  • False

However, the real Boolean values it will coerce those displayed values to will be

  • Undefined -> true
  • True -> true
  • False -> false

How can one achieve the desired effect (Undefined -> null), with limitations of not changing the class or wrapping it in another class which has Boolean type fields replaced with BooleanExtendedEnum type ones or using any other "hacky" solution?

Was it helpful?

Solution

The "glue" between the BeanEditor and the backing bean is the BeanModel. BeanModels are created by the BeanModelSource which in turn uses PropertyConduitSource.

It's quite simple to decorate the PropertyConduitSource to use Ternary instead of Boolean.

eg

public class MyAppModule {
    public PropertyConduitSource decoratePropertyConduitSource(final PropertyConduitSource old) {
       return new PropertyConduitSource() {
          public PropertyConduit create(Class rootType, String expression) { 
             PropertyConduit conduit = old.create(rootType, expression);

             // you cound also check for conduit.getAnnotation(AllowNull.class) 
             // and then annotate your bean properties for a more granular approach
             if (Boolean.class.equals(conduit.getPropertyType()) {
                return new TernaryPropertyConduit(conduit);
             }
             return conduit;
          }
       }
    }
}

public class TernaryPropertyConduit implements PropertyConduit {
   private PropertyConduit delegate;

   public getPropertyType() { return Ternary.class };

   public set(Object instance, Object value) {
      delegate.set(instance, ((Ternary) value).asBoolean());
   }

   public get(Object) {
      Boolean bValue = (Boolean) delegate.get(instance);
      return Ternary.valueOf(instance);
   }
}

OTHER TIPS

You could add a property to your page and use a custom block.

public enum Ternary {
    TRUE(Boolean.TRUE), FALSE(Boolean.FALSE), UNDEFINED(null);

    public static Ternary valueOf(Boolean value) { ... }
    public Boolean asBoolean() { ... }
}

public class MyPage {
    @Property
    private Foo foo;

    public Ternary getTernaryBar() {
       return Ternary.valueOf(foo.getBar());
    }

    public void setTernaryBar(Ternary tBar) {
       foo.setBar(tBar.asBoolean());
    }
}

<t:beaneditor t:id="foo" exclude="bar" add="ternaryBar">
    <p:ternaryBar>
       <t:label for="ternaryBar"/>
       <t:select t:id="ternaryBar" />
    </p:ternaryBar>
</t:beaneditor>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top