Question

I am using Tapestry 5.3.6 for a web application and I want the user to edit an instance of a Java class (a "bean", or POJO) using a web form (which immediately suggests the use of beaneditform) - however the Java class to be edited has a fairly complex structure. I am looking for the simplest way of doing this in Tapestry 5.

Firstly, lets define some utility classes e.g.

public class ModelObject {
  private URI uri;
  private boolean modified;
  // the usual constructors, getters and setters ...
}

public class Literal<T> extends ModelObject {
  private Class<?> valueClass;
  private T value;
  public Literal(Class<?> valueClass) {
    this.valueClass = valueClass;
  }
  public Literal(Class<?> valueClass, T value) {
    this.valueClass = valueClass;
    this.value = value;
  }
  // the usual getters and setters ...
}

public class Link<T extends ModelObject> extends ModelObject {
  private Class<?> targetClass;
  private T target;
  public Link(Class<?> targetClass) {
    this.targetClass = targetClass;
  }
  public Link(Class<?> targetClass, T target) {
    this.targetClass = targetClass;
    this.target = target;
  }
  // the usual getters and setters ...
}

Now you can create some fairly complex data structures, for example:

public class HumanBeing extends ModelObject {
  private Literal<String> name;
  // ... other stuff
  public HumanBeing() {
    name = new Literal<String>(String.class);
  }
  // the usual getters and setters ...
}

public class Project extends ModelObject {
  private Literal<String> projectName;
  private Literal<Date> startDate;
  private Literal<Date> endDate;
  private Literal<Integer> someCounter;
  private Link<HumanBeing> projectLeader;
  private Link<HumanBeing> projectManager;
  // ... other stuff, including lists of things, that may be Literals or
  // Links ... e.g. (ModelObjectList is an enhanced ArrayList that remembers
  // the type(s) of the objects it contains - to get around type erasure ...
  private ModelObjectList<Link<HumanBeing>> projectMembers;
  private ModelObjectList<Link<Project>> relatedProjects;
  private ModelObjectList<Literal<String>> projectAliases;
  // the usual constructors, getters and setters for all of the above ...
  public Project() {
    projectName = new Literal<String>(String.class);
    startDate = new Literal<Date>(Date.class);
    endDate = new Literal<Date>(Date.class);
    someCounter = new Literal<Integer>(Integer.class);
    projectLeader = new Link<HumanBeing>(HumanBeing.class);
    projectManager = new Link<HumanBeing>(HumanBeing.class);
    projectMembers = new ModelObjectList<Link<HumanBeing>>(Link.class, HumanBeing.class);
    // ... more ...
  }
}

If you point beaneditform at an instance of Project.class, you will not get very far before you have to supply a lot of custom coercers, translators, valueencoders, etc - and then you still run into the problem that you can't use generics when "contributing" said coercers, translators, valueencoders, etc.

I then started writing my own components to get around these problems (e.g. ModelObjectDisplay and ModelObjectEdit) but this would require me to understand a lot more of the guts of Tapestry than I have time to learn ... it feels like I might be able to do what I want using the standard components and liberal use of "delegate" etc. Can anyone see a simple path for me to take with this?

Thanks for reading this far.

PS: if you are wondering why I have done things like this, it is because the model represents linked data from an RDF graph database (aka triple-store) - I need to remember the URI of every bit of data and how it relates (links) to other bits of data (you are welcome to suggest better ways of doing this too :-)

EDIT:

@uklance suggested using display and edit blocks - here is what I had already tried:

Firstly, I had the following in AppPropertyDisplayBlocks.tml ...

    <t:block id="literal">
        <t:delegate to="literalType" t:value="literalValue" />
    </t:block>

    <t:block id="link">
        <t:delegate to="linkType" t:value="linkValue" />
    </t:block>

and in AppPropertyDisplayBlocks.java ...

    public Block getLiteralType() {
        Literal<?> literal = (Literal<?>) context.getPropertyValue();

        Class<?> valueClass = literal.getValueClass();
        if (!AppModule.modelTypes.containsKey(valueClass))
            return null;

        String blockId = AppModule.modelTypes.get(valueClass);
        return resources.getBlock(blockId);
    }

    public Object getLiteralValue() {
        Literal<?> literal = (Literal<?>) context.getPropertyValue();
        return literal.getValue();
    }

    public Block getLinkType() {
        Link<?> link = (Link<?>) context.getPropertyValue();

        Class<?> targetClass = link.getTargetClass();
        if (!AppModule.modelTypes.containsKey(targetClass))
            return null;

        String blockId = AppModule.modelTypes.get(targetClass);
        return resources.getBlock(blockId);
    }

    public Object getLinkValue() {
        Link<?> link = (Link<?>) context.getPropertyValue();
        return link.getTarget();
    }

AppModule.modelTypes is a map from java class to a String to be used by Tapestry e.g. Link.class -> "link" and Literal.class -> "literal" ... in AppModule I had the following code ...

    public static void contributeDefaultDataTypeAnalyzer(
            MappedConfiguration<Class<?>, String> configuration) {
        for (Class<?> type : modelTypes.keySet()) {
            String name = modelTypes.get(type);
            configuration.add(type, name);
        }
    }

    public static void contributeBeanBlockSource(
            Configuration<BeanBlockContribution> configuration) {

        // using HashSet removes duplicates ...
        for (String name : new HashSet<String>(modelTypes.values())) {
            configuration.add(new DisplayBlockContribution(name,
                    "blocks/AppPropertyDisplayBlocks", name));
            configuration.add(new EditBlockContribution(name,
                    "blocks/AppPropertyEditBlocks", name));
        }
    }

I had similar code for the edit blocks ... however none of this seemed to work - I think because the original object was passed to the "delegate" rather than the de-referenced object which was either the value stored in the literal or the object the link pointed to (hmm... should be [Ll]inkTarget in the above, not [Ll]inkValue). I also kept running into errors where Tapestry couldn't find a suitable "translator", "valueencoder" or "coercer" ... I am under some time pressure so it is difficult to follow these twisty passages through in order to get out of the maze :-)

Was it helpful?

Solution

I would suggest to build a thin wrapper around the Objects you would like to edit though the BeanEditForm and pass those into it. So something like:

public class TapestryProject {

   private Project project;

   public TapestryProject(Project proj){
      this.project = proj;
   }

   public String getName(){
      this.project.getProjectName().getValue();
   }

   public void setName(String name){
      this.project.getProjectName().setValue(name);
   }

   etc...
}

This way tapestry will deal with all the types it knows about leaving you free of having to create your own coersions (which is quite simple in itself by the way).

OTHER TIPS

You can contribute blocks to display and edit your "link" and "literal" datatypes.

The beaneditform, beaneditor and beandisplay are backed by the BeanBlockSource service. BeanBlockSource is responsible for providing display and edit blocks for various datatypes.

If you download the tapestry source code and have a look at the following files:

  • tapestry-core\src\main\java\org\apache\tapestry5\corelib\pages\PropertyEditBlocks.java
  • tapestry-core\src\main\resources\org\apache\tapestry5\corelib\pages\PropertyEditBlocks.tml
  • tapestry-core\src\main\java\org\apache\tapestry5\services\TapestryModule.java

You will see how tapestry contributes EditBlockContribution and DisplayBlockContribution to provide default blocks (eg for a "date" datatype).

If you contribute to BeanBlockSource, you could provide display and edit blocks for your custom datatypes. This will require you reference blocks by id in a page. The page can be hidden from your users by annotating it with @WhitelistAccessOnly.

Here's an example of using an interface and a proxy to hide the implementation details from your model. Note how the proxy takes care of updating the modified flag and is able to map URI's from the Literal array to properties in the HumanBeing interface.

package com.github.uklance.triplestore;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

public class TripleStoreOrmTest {
    public static class Literal<T> {
        public String uri;
        public boolean modified;
        public Class<T> type;
        public T value;

        public Literal(String uri, Class<T> type, T value) {
            super();
            this.uri = uri;
            this.type = type;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Literal [uri=" + uri + ", type=" + type + ", value=" + value + ", modified=" + modified + "]";
        }
    }

    public interface HumanBeing {
        public String getName();
        public void setName(String name);

        public int getAge();
        public void setAge();
    }

    public interface TripleStoreProxy {
        public Map<String, Literal<?>> getLiteralMap();
    }

    @Test
    public void testMockTripleStore() {
        Literal<?>[] literals = {
            new Literal<String>("http://humanBeing/1/Name", String.class, "Henry"),
            new Literal<Integer>("http://humanBeing/1/Age", Integer.class, 21)
        };

        System.out.println("Before " + Arrays.asList(literals));

        HumanBeing humanBeingProxy = createProxy(literals, HumanBeing.class);

        System.out.println("Before Name: " + humanBeingProxy.getName());
        System.out.println("Before Age: " + humanBeingProxy.getAge());

        humanBeingProxy.setName("Adam");

        System.out.println("After Name: " + humanBeingProxy.getName());
        System.out.println("After Age: " + humanBeingProxy.getAge());

        Map<String, Literal<?>> literalMap = ((TripleStoreProxy) humanBeingProxy).getLiteralMap();
        System.out.println("After " + literalMap);
    }

    protected <T> T createProxy(Literal<?>[] literals, Class<T> type) {
        Class<?>[] proxyInterfaces = { type, TripleStoreProxy.class };

        final Map<String, Literal> literalMap = new HashMap<String, Literal>();
        for (Literal<?> literal : literals) {
            String name = literal.uri.substring(literal.uri.lastIndexOf("/") + 1);
            literalMap.put(name,  literal);
        }

        InvocationHandler handler = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getDeclaringClass().equals(TripleStoreProxy.class)) {
                    return literalMap;
                }
                if (method.getName().startsWith("get")) {
                    String name = method.getName().substring(3);
                    return literalMap.get(name).value;
                } else if (method.getName().startsWith("set")) {
                    String name = method.getName().substring(3);
                    Literal<Object> literal = literalMap.get(name);
                    literal.value = args[0];
                    literal.modified = true;
                }    
                return null;
            }
        };

        return type.cast(Proxy.newProxyInstance(getClass().getClassLoader(), proxyInterfaces, handler));
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top