I present my sites as domain objects. I would like to open my nodes via the REST interface, using GET to query by id, POST to save the node with the connected nodes and PUT to update the node and its associated nodes. GET is working fine, but I have a question implementing the question of POST and PUT. Let me illustrate the problem, as code often speaks a lot more than words.
In this example, there are two related node types that are represented as domain objects. A collaboration can have multiple tags, and tags can belong to multiple collaborative activities. So we have a lot of relationships. They both have the same base class, NodeBacked, which basically serves as a wrapper for the base node.
Nodebacked
abstract class NodeBacked { private Node node; public NodeBacked(final Node node) { this.node = node; } public Long getId() { return this.node.getId(); } @Override public int hashCode() { return this.node.hashCode(); } @JsonIgnore public Node getNode() { return this.node; } }
Cooperation
public class Collaboration extends NodeBacked { public Collaboration(final Node node) { super(node); } // Leaving out some properties for clearness @JsonProperty(NAME_JSON) public String getName() { return (String) getNode().getProperty(NAME); } @JsonProperty(TAGS_JSON) public Iterable<Tag> getTags() { return new IterableWrapper<Tag, Path>(Traversal.description().breadthFirst() .relationships(Relationships.HAS, Direction.OUTGOING).uniqueness(Uniqueness.NODE_GLOBAL) .evaluator(Evaluators.atDepth(1)).evaluator(Evaluators.excludeStartPosition()).traverse(getNode())) { @Override protected Tag underlyingObjectToObject(final Path path) { return new Tag(path.endNode()); } }; } public void setName(final String name) { final Index<Node> index = getNode().getGraphDatabase().index().forNodes(Indexes.NAMES); getNode().setProperty(NAME, name); if (StringUtils.isNotEmpty(getName())) { index.remove(getNode(), NAME, name); } index.add(getNode(), NAME, name); } public void addTag(final Tag tag) { if (!Traversal.description().breadthFirst().relationships(Relationships.HAS, Direction.OUTGOING) .uniqueness(Uniqueness.NODE_GLOBAL).evaluator(Evaluators.atDepth(1)) .evaluator(Evaluators.excludeStartPosition()) .evaluator(Evaluators.includeWhereEndNodeIs(tag.getNode())).traverse(getNode()).iterator().hasNext ()) { getNode().createRelationshipTo(tag.getNode(), Relationships.HAS); } } @Override public boolean equals(final Object o) { return o instanceof Collaboration && getNode().equals(((Collaboration) o).getNode()); } }
Tag
public class Tag extends NodeBacked { public Tag(final Node node) { super(node); } @JsonProperty(NAME_JSON) public String getName() { return (String) getNode().getProperty(NAME); } public void setName(final String name) { final Index<Node> index = getNode().getGraphDatabase().index().forNodes(Indexes.NAMES); getNode().setProperty(NAME, name); if (StringUtils.isNotEmpty(getName())) { index.remove(getNode(), NAME, name); } index.add(getNode(), NAME, name); } @JsonProperty(COLLABORATIONS_JSON) @JsonSerialize(using = SimpleCollaborationSerializer.class) private Iterable<Collaboration> getCollaborations(int depth) { return new IterableWrapper<Collaboration, Path>(Traversal.description().breadthFirst() .relationships(Relationships.HAS, Direction.INCOMING).uniqueness(Uniqueness.NODE_GLOBAL) .evaluator(Evaluators.atDepth(1)).evaluator(Evaluators.excludeStartPosition()).traverse(getNode())) { @Override protected Collaboration underlyingObjectToObject(final Path path) { return new Collaboration(path.endNode()); } }; } @Override public boolean equals(final Object o) { return o instanceof Tag && getNode().equals(((Tag) o).getNode()); } }
I am looking at collaboration through REST (Spring 3.2) as follows. MappingJackson2HttpMessageConverter is used to convert POJO to JSON and vice versa.
@Controller @RequestMapping(value = CollaborationController.CONTEXT_PATH) public class CollaborationController { public static final String CONTEXT_PATH = "/collaborations"; @Autowired private GraphDatabaseService db; @Transactional @RequestMapping(value = "/{id}", method = RequestMethod.GET) public @ResponseBody Collaboration getCollaboration(final @PathVariable Long id) {
This works great. Recipients request a node for its properties and return a JSON string. Example JSON string for GET /collaborations/1 :
{ "name" : "Dummy Collaboration", "tags" : [ { "name" : "test", "id" : 3 }, { "name" : "dummy", "id" : 2 } ], "id" : 1 }
So what then is the problem? Imagine a POST request with a JSON body that looks like this:
{ "name" : "Second collaboration", "tags" : [ { "name" : "tagged" } ] }
CollaborationController has the following POST request processing method:
@Transactional @RequestMapping(method = RequestMethod.POST, headers = JSON_CONTENT_TYPE) public @ResponseBody ResponseEntity<Collaboration> persist(final @RequestBody Collaboration collaboration, final UriComponentsBuilder builder) { final Collaboration collab = new Collaboration(db.createNode(Labels.COLLAB));
String collab.setName(collaboration.getName()); will not work because the collaboration class does not contain its own properties and uses getters that directly query the base node. In this case, there is no node available, because Collaboration needs to be converted from JSON to POJO from Jackson2 using Spring MappingJackson2HttpMessageConverter. There are no properties, so nothing needs to be set ...
I am studying a pure solution to this problem, but have not yet found it. I could use POJO (or VO or ...) as an input parameter to the persist method, but this is not supported. Changing a property requires updating the Collaboration class, as well as the CollaborationVO (POJO) class.
Suggestions are more than welcome! Spring Data Neo4j solves mainly all of this, but I'm not happy with its performance. This is the exact reason I'm trying to find a different approach.
I hope this explanation is clear enough. Thank you for agreeing with me!
Used framework:
- Spring 3.2.3.RELEASE
- Neo4j 2.0.0-M3 ( built-in )
- Jackson 2.2.2