Server

Parameterizing and Reusing Marshallers

Updated: November 15, 2024

The Nuxeo Platform marshalling service provides a rendering context. This context allows you to parametrize your JSON marshallers. For example you can decide to add a parameter which tells your marshaller to fetch or not some parts of the rendered object.

Usages

This context is managed by the class org.nuxeo.ecm.core.io.registry.context.RenderingContext. A helper is provided to easily create a context: org.nuxeo.ecm.core.io.registry.context.RenderingContext.CtxBuilder.

When the marshalling service is used from JAX-RS, the context is filled with:

  • The Nuxeo URL
  • The client locale
  • The given HTTP headers
  • The GET parameters
  • The Java Request attributes

If you add parametrized behaviors to your marshaller, it is then customizable by the end-user application using the corresponding parameter in headers or in GET parameters.

Parametrize the description and add the product's URL

@Setup(mode = SINGLETON, priority = Priorities.REFERENCE)
public class ProductJsonWriter extends AbstractJsonWriter<Product> {
    public void write(Product product, JsonGenerator jg) throws IOException {
        jg.writeStartObject();
        jg.writeNumberField("ref", product.getReference());
        if (ctx.getBooleanParameter("loadProductDescription")) {
            jg.writeStringField("desc", product.getDescription());
        }
        String url = ctx.getBaseUrl() + "/productApp/product/ref/" + product.getReference();
        jg.writeStringField("url", url);
        jg.writeEndObject();
    }
}

You can send the parameters using HTTP GET parameter or HTTP header (useful for POST/PUT/DELETE results).

Curl calls

$ curl -X GET 'http://NUXEO_SERVER/nuxeo/site/productApp/product/ref/10001?loadProductDescription=true'
$ curl -X GET -H 'loadProductDescription:true' 'http://NUXEO_SERVER/nuxeo/site/productApp/product/ref/10001'

You can also use the rendering context from the server side using the CtxBuilder.

MarshallingRegistry - Java-to-JSON

Product product = ...;
RenderindContext ctx = CtxBuilder.param("loadProductDescription", Boolean.TRUE).get();
String json = MarshallerHelper.objectToJson(product, ctx);

Finally, you can force the value of a parameter on the server side by overriding the rendering context used to produce the response.

Use marshallers

@GET
@Produces(MediaType.APPLICATION_JSON)
public Product doGet(@Context HttpServletRequest request) {
    Product product;
    // get the product ... and configure output
    RenderindContext ctx = CtxBuilder.param("loadProductDescription", Boolean.TRUE).get();
    request.setAttribute(CTX_KEY, ctx);
    return product;
}
private static final String CTX_KEY = "_STORED_GENERATED_RENDERING_CONTEXT";

RenderingContext API

Setting/Getting the Nuxeo Base URL

Web context usage Retrieved from the HTTP call
Server side usage
ctx = CtxBuilder.base("http://server/nuxeo").get();
Use from marshallers
String url = ctx.getBaseUrl();

Setting/Getting the Current Locale

Web context usage Retrieved from the end-user's browser
Server side usage
ctx = CtxBuilder.locale(Locale.FRENCH).get();
Use from marshallers
Locale locale = ctx.getLocale();

Delivering/Using a CoreSession to/in Marshallers

Web context usage Retrieved from the WebContext, or from a document or created from a nxrepository parameter or created from a Repository header.
Server side usage
CoreSession session = ...;
ctx = CtxBuilder.session(session).get();
Use from marshallers The try-resource statement ensure the session would be closed if it's necessary
try (SessionWrapper wrapper = ctx.getSession(document)) {CoreSession session = wrapper.getSession();
    }
try (SessionWrapper wrapper = ctx.getSession(null)) {CoreSession session = wrapper.getSession();
    }

Setting/Getting Any Parameter

Web context usage URL:
  • .../some-url?paramName=paramValue
HTTP Header:
  • paramName:paramValue
Server side usage
ctx = CtxBuilder.param("paramName", "paramValue").get();
ctx.setParameterValues("paramName", "paramValue");
Use from marshallers
Object o = ctx.getParameter("paramName");
List<Object> list = ctx.getParameters("paramName");
boolean b = ctx.getBooleanParameter("paramName");

Loading Some Document Properties

Web context usage URL:
  • .../some-url?properties=*
  • .../some-url?properties=dublincore,file
  • .../some-url?properties=dublincore&properties=file
HTTP Header:
  • properties:*
  • properties:dublincore,file
Server side usage
ctx = CtxBuilder.properties("*").get();
ctx = CtxBuilder.properties("dublincore", "file").get();
ctx.setParameterValues("properties", "dublincore", "file");
Use from marshallers
Set<String> properties = ctx.getProperties();

Loading Additional Parts of an Object Whose entity-type Is 'objectType'

The corresponding behavior depends on the marshaller.

Web context usage URL:
  • .../some-url?fetch.objectType=part1,part2
  • .../some-url?fetch.objectType=part1&fetch.objectType=part2
HTTP Header:
  • X-NXfetch.objectType:part1,part2
Server side usage Only for documents
ctx = CtxBuilder.fetchInDoc("part1", "part2").get();
ctx = CtxBuilder.fetch("objectType", "part1", "part2").get();
ctx.setParameterValues("fetch.objectType", "part1", "part2");
Use from marshallers
Set<String> toLoad = ctx.getFetched("objectType");

Enabling Enricher for an objectType

The enricher will contribute to the object marshalling. The object marshaller must extend ExtensibleEntityJsonWriter. The given enricher name must match an existing enricher for this object type.

Web context usage URL:
  • .../some-url?enrichers.objectType=children,acl
  • .../some-url?enrichers.objectType=children&enrichers.objectType=acl
HTTP Header:
  • X-NXenrichers.objectType:children,acl
Server side usage Only for documents
ctx = CtxBuilder.enrichDoc("children", "acl").get();
ctx = CtxBuilder.enrich("objectType", "children", "acl").get();
ctx.setParameterValues("enrichers.objectType", "children", "acl");
Use from marshallers
Set<String> enricherToActivate = ctx.getEnrichers("objectType");

Translating Some Part of an Object Which May Contains Some l10n Key

Web context usage URL:
  • .../some-url?translate.objectType=elementToTranslate
HTTP Header:
  • X-NXtranslate.objectType:elementToTranslate
Server side usage
ctx = CtxBuilder.translate("objectType", "elementToTranslate").get();
ctx.setParameterValues("translate.objectType", "elementToTranslate");
Use from marshallers
Set<String> toTranslate = ctx.getTranslated("objectType");

Controlling the Aggregation Depth

Available values are: root for no aggregation, children for 1 level, max for 2 levels. Default value is 'children'. See next chapter for more details

Web context usage URL:
  • .../some-url?depth=children
HTTP Header:
  • depth:children
Server side usage
ctx = CtxBuilder.depth(DepthValues.children).get();
ctx.setParameterValues("depth", "children");
Use from marshallers
try (Closeable resource = ctx.wrap().controlDepth().open()) {
 // call another marshaller
} catch (MaxDepthReachedException e) {
 // do not load properties
}

Aggregating Marshallers and Avoiding Infinite Loops

Let's take an example. We created two classes, Product and Category. And we'd like to marshall them as JSON.

When loading a category, we'd like to be able to get the corresponding products.

Custom JSON

@Setup(mode = Instantiations.SINGLETON, priority = Priorities.REFERENCE)
public class CategoryJsonWriter extends AbstractJsonWriter<Category> {
    public void write(Category category, JsonGenerator jg) throws IOException {
        jg.writeStartObject();
        jg.writeNumberField("ref", category.getReference());
        jg.writeStringField("desc", category.getDescription());
        if (ctx.getFetched("category").contains("products")) {
            jg.writeArrayFieldStart("products");
            for (Product product: product.getProducts()) {
                // delegates the marshalling to Nuxeo
                writeEntity(product);
            }
            jg.writeEndArray();
        }
        jg.writeEndObject();
    }
}

Curl calls

$ curl -X GET 'http://NUXEO_SERVER/nuxeo/site/productApp/category/ref/12?fetch.category=products'

When loading a product, we'd like to be able to get the corresponding categories.

Custom JSON

@Setup(mode = Instantiations.SINGLETON, priority = Priorities.REFERENCE)
public class ProductJsonWriter extends AbstractJsonWriter<Product> {
    public void write(Product product, JsonGenerator jg) throws IOException {
        jg.writeStartObject();
        jg.writeNumberField("ref", product.getReference());
        jg.writeStringField("desc", product.getDescription());
        if (ctx.getFetched("product").contains("categories")) {
            jg.writeArrayFieldStart("categories");
            for (Category category: product.getCategories()) {
                // delegates the marshalling to Nuxeo
                writeEntity(category);
            }
            jg.writeEndArray();
        }
        jg.writeEndObject();
    }
}

Curl calls

$ curl -X GET 'http://NUXEO_SERVER/nuxeo/site/productApp/category/ref/12?fetch.product=categories'

This works fine. But if we want to get both product's categories and category's products, there's a real risk to get an infinite loop. Or at least to get a very huge amount of JSON (if the category-product graph has no cycle).

Curl calls

$ curl -X GET 'http://NUXEO_SERVER/nuxeo/site/productApp/category/ref/12?fetch.product=categories&fetch.category=products'

To manage this case, we could use a counter put in the context but this will not work in most case because the counter will manage the first "line" of marshaller and disable the others: that's due to the processing which follows a "Depth First Search".

The Nuxeo Platform provides a specific object to wrap the depth control in each "crossing line". Each time you call another marshaller, you should use it.

Custom JSON

@Setup(mode = Instantiations.SINGLETON, priority = Priorities.REFERENCE)
public class CategoryJsonWriter extends AbstractJsonWriter<Category> {
    public void write(Category category, JsonGenerator jg) throws IOException {
        jg.writeStartObject();
        jg.writeNumberField("ref", category.getReference());
        jg.writeStringField("desc", category.getDescription());
        // control the graph depth
        try (Closeable resource = ctx.wrap().controlDepth().open()) {
            if (ctx.getFetched("category").contains("products")) {
                jg.writeArrayFieldStart("products");
                for (Product product: product.getProducts()) {
                    // delegates the marshalling to Nuxeo
                    writeEntity(product);
                }
                jg.writeEndArray();
            }
        } catch (MaxDepthReachedException e) {
            // do not load products
        }
        jg.writeEndObject();
    }
}

The max depth is controlled by the depth parameter in the rendering context. The default value for depth is children.

Depth values: If we get a product -> its categories -> their products ...

  • root: No category will be loaded, just the first product.
  • children: The product is loaded with its categories.
  • max: The product is loaded, its categories also, and their products.

The only way to load more data is to avoid the use of depth control (which is bad!).

Each existing Nuxeo writer/enricher, which may use another marshaller, uses the depth's control.