JSON, REST, Magnolia and the creation of end points without writing a single line of Java

Published on January 1, 2016 by Jan Haderka



Here's the craziest of the crazy Christmas ideas (as usual, it's not my original idea, so thanks, Tomas, for mentioning that in a conversation we had :)).

 

There’s a bunch of tools out there that already convert any given Java object into JSON and back, so don’t expect anything from me there. However, many (read: “all") of the tools have issues with the JCR API and, for example, the getParent() method that will throw an exception when you try to get the parent of the root node. Similarly (at least with an Apache JackRabbit implementation) you will run into an infinite loop as soon as you start going through various getters on the node types (to which you get from a JCR Node class simply by calling getPrimaryNodeType()). So all things considered, a JCR Node object is not the best choice for a JSON serialisation.

 

Here's more: the way Magnolia makes you use JCR nodes to keep your content in different pools based on the content's type (and then makes them easy to manipulate via apps) means that you end up with a lot of properties in your node that look like categoryId=501114fc-f951-4f08-a23f-47c28e00128e or image=jcr:501114fc-f951-4f08-a23f-47c28e00128e. Not so handy when all you want in your template is to produce a JSON array that you can feed to your AngularJS (or your favourite js framework), is it? What you end up doing is either making REST calls from AngularJS to resolve those IDs into JSON arrays of category, image or whatever you are dealing with, or you come up with some more or less complicated way of resolving those IDs to properties you want when building a JSON array in the first place.

 

Now assume you have the following structure:

/crossaint

     name = Crossaint

     categoryId = 501114fc-f951-4f08-a23f-47c28e00128e  (points to Category = Pastry)

     image = jcr:501114fc-f951-4f08-a23f-47c28e00128e (points to DAM image of croissant)

     description = "Our signature flaky croissant with fine dark chocolate chunks on the inside. Warm them just before serving to experience pure melted chocolate goodness."

 

Not many props here and not much work to turn it to JSON, right? Except those two IDs in the way there. Now consider you could do this:

[#assign pastry = cmsfn.contentByPath(“/crossaint”, “pastry”)]

${jsonfn.from(pastry).addAll().expand(“categoryId”, “category”).expand(“image”, “dam”).print()}

 

...and you end up with a JSON array looking like this:

{

  "@nodeType" : “pastry",

  "@name" : “croissant",

  "@depth" : 1,

  “category" : {

    “name" : “Pastry”,

    "@depth" : 1,

    "@nodeType" : "category",

    "@name" : “pastry",

    "@id" : "501114fc-f951-4f08-a23f-47c28e00128e",

    "@path" : “/pastry"

    “@link" : "null"

  },

  “image" : {

    “caption" : “Croissant”,

    “name" : “croissant”,

    "@depth" : 2,

    "@nodeType" : “mgnl:asset",

    "@name" : “croissant.JPG",

    "@id" : “403214fc-f951-4f08-a23f-47c28e00234a",

    "@path" : “/pastry/croissant.JPG”,

    “@link" : “/myContextPath/mysite/dam/jcr:403214fc-f951-4f08-a23f-47c28e00234a/crossaint.JPG"

  },

  "@id" : "a017061d-b57b-4477-910a-b71b652bf2cd",

  “name" : “Croissant",

  "@path" : “/croissant”,

  “@link" : “/myContextPath/mysite/pastry/Crossaint"

}

 

Looks cool, right? Let’s stop here for a second and reevaluate the syntax again:

${jsonfn.from(pastry).addAll().expand(“categoryId”, “category”).expand(“image”, “dam”).print()}

 

So the .from(pastry) part tells our functions which node we will want to print out.

 

By default, no properties will be outputted. Why? Anything can be stored under the node. We have no way of knowing. It might even be some sensitive info. And even if it's not, we want to generate lean JSON that contains only the necessary data to minimize the transfer and the further processing of it. So by default, we don’t want to output anything. In this case, however, since we wanted to show all properties, we will use the .addAll() method to output all properties anyway, but it is our conscious decision knowing we do not expose any sensitive data or are not sending an encoded image or video file over for nothing.

 

Furthermore, to make those functions useful, we want to process and expand, or translate if you will, both categoryId and imageId properties, so we tell our function which property points to which node in which other workspace by using .expand(“categoryId”, “category”).expand(“image”, “dam”)

And finally we use .print() to tell the function class to proceed with our data.

 

Don’t care about the path and the node type? Ok, let’s get rid of those:

[#assign pastry = cmsfn.contentByPath(“/crossaint”, “pastry”)]

${jsonfn.from(pastry).addAll().expand(“categoryId”, “category”).expand(“image”, “dam”).exclude(“@path”,”@nodeType”).print()}

...and you end up with a JSON array looking like this:

{

  "@name" : “croissant",

  "@depth" : 1,

  “category" : {

    “name" : “Pastry”,

    "@depth" : 1,

    "@name" : “pastry",

    "@id" : "501114fc-f951-4f08-a23f-47c28e00128e",

    “@link" : "null"

  },

  “image" : {

    “caption" : “Croissant”,

    “name" : “croissant”,

    "@depth" : 2,

    "@name" : “croissant.JPG",

    "@id" : “403214fc-f951-4f08-a23f-47c28e00234a",

    “@link" : “/myContextPath/mysite/dam/jcr:403214fc-f951-4f08-a23f-47c28e00234a/crossaint.JPG"

  },

  "@id" : "a017061d-b57b-4477-910a-b71b652bf2cd",

  “name" : “Croissant",

  “@link" : “/myContextPath/mysite/pastry/Crossaint"

}

 

Still tmi? Yeah, that's usually the issue for me too, which is also why by default we are not including anything. Now, let’s try it the other way around:

[#assign pastry = cmsfn.contentByPath(“/crossaint”, “pastry”)]

${jsonfn.from(pastry).expand(“categoryId”, “category”).expand(“image”, “dam”).add(“name”,”caption”).print()}

 

...and our JSON array is now minuscule:

{

  “category" : {

    “name" : “Pastry”,

  },

  “image" : {

    “caption" : “Croissant”,

    “name" : “croissant”,

  },

  “name" : “Croissant",

}

 

Was that too much (or too little)? Let’s try again...

[#assign pastry = cmsfn.contentByPath(“/crossaint”, “pastry”)]

${jsonfn.from(pastry).expand(“categoryId”, “category”).expand(“image”, “dam”).add(“name”, “@id”).print()}

 

...and you end up with a JSON array looking like this (notice that all expanded properties are included automatically):

{

  “category" : {

    “name" : “Pastry”,

    "@id" : "501114fc-f951-4f08-a23f-47c28e00128e",

  },

  “image" : {

    “name" : “croissant”,

    "@id" : “403214fc-f951-4f08-a23f-47c28e00234a",

  },

  "@id" : "a017061d-b57b-4477-910a-b71b652bf2cd",

  “name" : “Croissant",

}

 

Here we go … I hope you are happy now, as I've got nothing more to show 😀

BTW, if you haven't noticed, there's difference between @name and name. The former is the name of the JCR node, the latter is the value of the property name, if you have defined one.

 

To recap:

 

You tell functions what node you want to work with:

jsonfn.from(myNode)

 

Or maybe you want to work with all child nodes of a given node instead:

jsonfn.fromChildNodesOf(myNode)

 

Both of the above functions work with Node or ContentMap.

 

You can include all properties by calling

.addAll()

 

Or you can explicitly name those you want in:

.add(“myProp1”, “myProp2”, ...)

 

Or you use regex to do the same:

.add(“my.*”, ...)

 

If you added all or used regex to add them and ended up adding too much, you can just exclude some properties:

.exclude(“myProp1”, “myProp2”, …)

 

You can also use regex when excluding props:

.excludeWithRegex(“my.*”, “some.*more”, …)

 

And most importantly, you can also expand any property you want, by providing the name of the expanded property and workspace name in which to look for the target:

.expand(“categoryId”, “category”)

 

You can also control how many levels down you want to expand your JSON (by default it operates on a single level, the current level only):

.down(int)

 

And you can also have links for different renditions of binaries, just say which renditions you want.

.binaryLinkRendition(“rendition1”, “myRendition2”, ...)

 

In this case, for all assets (the expanded imageId in the example above), you end up with extra properties in your JSON in the form of 

“@rendition_rendition1”:”/link/to/rendition1”,

“@rendition_myRendition2”:”/link/to/myRendition2”

 

And last but not least, when you built your chain of operations, you execute on them by calling:

.print()

 

All right, so far so good. We now have another library/API to turn a java object into JSON. And not even any java object, just JCR nodes. So instead of thousands of such libraries, we have now 1000 and 1 … Why bother? Surprisingly, this is not aimed at java developers. Instead, with all the changes in Magnolia, the goal here is to make it more friendly for all web developers. With this, we enable all of them to create REST end points in Magnolia without having to write a single line of Java code.

 

How? Simply, in these 5 steps:

 

1) Define a restEndpoint template and point it to /restEndpoint.ftl

2) Create a /rest/myEndPoint page in the website and assign it a restEndpoint template.

3) Edit restEndpoint.ftl and use jsonfn to output whatever you want. For example:

[#assign foo = cmsfn.contentById(state.selectors[1], state.selectors[0])]

${jsonfn.from(foo).addAll().print()}

4) Create a VirtualURIMapping

class=info.magnolia.cms.beans.config.RegexpVirtualURIMapping

fromURI=/rest/myEndPoint/([a-zA-Z0-9,-]*)/(.*)

toURI=forward:/rest/myEndPoint~$1~$2~.html

5) Point your browser to http://yourmagnolia.com/rest/myEndPoint/contacts/3b8a2169-977e-4bcc-bed5-c1477966f912

 

Voilà … I’m sure you can think of more useful variations of the above to use in your web applications powered by data coming from Magnolia.

 

The API is still very experimental. It adapted it to what I needed it to do, but I’m sure it can be greatly improved. Your suggestions and contributions are welcome.

 

Oh, I nearly forgot: The code is on github!

https://github.com/rah003/neat-jsonfn

 

And you can get it in your Magnolia as another module, just drop the jar (and its dependencies - neat-tweaks-common & jackson-*) in your WEB-INF/lib and restart, the rest should be a breeze.

It should work on any 5.x version, and maybe even on the older ones, but it does require Java 8.

 

Happy New Year & all the best in 2016,

Jan

 



Comments



{{item.userId}}   {{item.timestamp | timestampToDate}}

About the author Jan Haderka

Jan is Magnolia's Chief Technology Officer. He has been developing software since 1995. On this blog, he'll write about Magnolia's connectors, integrations and coding ...with the odd Magnolia usability and development tip thrown in for good measure. He lives in the Czech Republic with his wife and three children. Follow him on Twitter @rah003.


See all posts on Jan Haderka

Demo site Contact us Free trial