4

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 :-)

Murray Jensen
  • 296
  • 2
  • 11

3 Answers3

2

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).

joostschouten
  • 3,863
  • 1
  • 18
  • 31
  • Sometimes its hard to see the forest for the trees :-) ... thank you for the suggestion, this sounds like the way to go. – Murray Jensen Mar 04 '13 at 13:21
2

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.

lance-java
  • 25,497
  • 4
  • 59
  • 101
  • Thanks for your comment - I tried this but ran into a maze of twisty passages, all the same :-) Link does nothing really except reference another complex object, and Literal is the same except for simple objects. I had a display and edit block for each of these, both just had in them, then I had a method which examined the type the Link or Literal referenced and returned an appropriate block, but I couldn't work out how to provide the dereferenced object as the value for the "delegated" block ... will re-edit the main question and explain what I tried in more detail ... – Murray Jensen Mar 05 '13 at 23:34
0

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));
    }
}
lance-java
  • 25,497
  • 4
  • 59
  • 101
  • nice idea - but I actually have to store information with each element e.g. the "projectName" field has a unique URI associated with it - or rather the real object is the URI itself, and it "has a literal value" which is a String. My base class ModelObject contains a URI at a minimum, and anything else I might want to keep (such as a modified flag). – Murray Jensen Mar 06 '13 at 23:16
  • Unfortunately I'm not familiar with RDF graph databases so I don't fully understand what you're doing under the hood. Have you considered using a java.lang.reflect.Proxy? You could declare your value objects as interfaces instead of concrete objects and return a proxy that is connected to the underlying data store. The proxy holds references to the URI's and proxies get and set operations through to the underlying datastore. – lance-java Mar 07 '13 at 00:06
  • Here's a list of tools which might do the job http://semanticweb.org/wiki/Tripresso – lance-java Mar 07 '13 at 11:09
  • Thanks for the pointers ... but my problem isn't getting the data from the RDF database into the "model" classes - I can do that fine. The problem is getting Tapestry to understand the linked data structures. I'm beginning to think that it just can't do it - what I really need to be able to do is to "push" the current bean edit or display "context" (which stores information about the "property" being displayed or edited) and start a new one with a different object (i.e. a "dereference"). I don't think Tapestry can do this. – Murray Jensen Mar 07 '13 at 15:15
  • It looks like you have let the underlying implementation drive your model when it should be the other way round. If you weren't using a triplestore, I'm sure your model would be much prettier. I think that you should be aiming for a simple model and hide all that ugliness from your model. Then you wouldn't need any tapestry customisations. Also, if you want to expose a public API, you can. At the moment, there's no way you'd expose an API with all that clutter in it. – lance-java Mar 07 '13 at 15:19
  • The necessity to store a URI with every data item drives the model. It is as simple as it can be. But you are correct, the user is not interested in the complexity, it should be hidden from them - I just didn't realise you had to hide it from Tapestry too :-) See comment from @joostschouten above - I have used his approach, which is obvious in hindsight. – Murray Jensen Mar 31 '13 at 02:31
  • Have you read through my solution above? The complexity is hidden behind a simple bean interface but you can still access the complex model by casting the object to TripleStoreProxy. – lance-java Mar 31 '13 at 13:37
  • Like I said - its a nice idea, I like it and if I had the time to pursue it I would. However, I am getting what I need by using the simple wrapper layer suggested by @joostschouten above so I am going with that. Thank you for your explanations. – Murray Jensen Apr 01 '13 at 02:55