Persisting Sling Model / OCM

Avatar

Avatar

jamiec4451712

Avatar

jamiec4451712

jamiec4451712

02-11-2018

I'm having a really hard time understanding what options there are out there for writing Java objects back to the JCR in some organized fashion. I came across Jackrabbit OCM (Object Content Mapping), but I can't for the life of me find any information on how it can be installed into AEM. I checked the depfinder and it's not already bundled, so I tried to find the JAR online, but there's no Maven repo for it (and it's not in the Adobe Public repo). I'm now trying to compile from source but once I hit that wall I thought there must be another way.

I've used Sling Models before, but I can't find a decent way to just map those models back to JCR nodes with normal operations (add, update, delete). I also ran into neba, but it appears that it works the same way as Sling Models and there is an issue that was opened and then closed asking for a persistence feature.

Overall, I've been a bit frustrated with the availability of APIs for AEM to work with custom, complex data types with the JCR. It's a waste of time and energy to write custom implementations for simple models, and not every case is a good fit for OOTB APIs like the PageManager.

I have also seen examples of people making their Sling Model adaptable to a ModifiableValueMap or something, but this doesn't work very well for nested objects.

Let's look at a real use case. I have some abstract data in a JSON object:

   "name":"John",

   "age":30,

   "cars":{

      "car1": { 

            "make":"Ford",

            "model":"Fiesta",

      },

      "car2": { 

            "make":"Honda",

            "model":"Accord",

      }

   }

}

And some POJO:

public class Person {

    public String path;

    public String name;

    public int age;

    public List<Car> cars;

}

public class Car {

    public String path;

    public String name;

    public String make;

    public String model;

}

I want to map this data to a POJO using GSON, then write that Java class to the JCR using some simple method. The JCR tree would look something like:

└── people

    └── John

        └── cars

            ├── car1

            └── car2

. I then want to be able to reverse that method, mapping the JCR nodes back into the POJO. Like Sling Models, PostConstruct methods and other features would be great. It seems like the Jackrabbit OCM package has these features, but there's no documentation on using it in AEM (and no clear path to bundling the JAR manually).

In the Jackrabbit OCM package, this would probably look something like this:

// this would come from GSON

Person person = new Person();

// Would build setters for this stuff

person.path = String.format("./people/%s", person.name);

// Create

ocm.insert(person);

ocm.save();

// Retrieve

person = (person) ocm.getObject("./people/John");

// Delete

ocm.remove(person);

ocm.save();

Accepted Solutions (1)

Accepted Solutions (1)

Avatar

Avatar

Brendan_Robert

Employee

Avatar

Brendan_Robert

Employee

Brendan_Robert
Employee

26-11-2018

You are correct, some of these jars are not found as OSGi bundles in AEM.  You need to update your bundle to include these, I'm pretty sure that the embed-dependency line from my example should do the trick.

Answers (2)

Answers (2)

Avatar

Avatar

jamiec4451712

Avatar

jamiec4451712

jamiec4451712

14-11-2018

Hey Brendan,

Thanks for the in depth reply. I’ve added the dependencies and modified the maven-bundle-plugin, but I’m still struggling to get OCM installed in AEM.

I see the pom for jackrabbit-ocm downloading when I install, yet my autocomplete is not working in IntelliJ and the ocm reference is null when I debug. The project does compile.

The OCM package also does not show up in the AEM depfinder, which leads me to believe this is why the actual reference to the OCM reference is null. Are you able to find this in the depfinder on a working instance?

Thanks!

Avatar

Avatar

Brendan_Robert

Employee

Avatar

Brendan_Robert

Employee

Brendan_Robert
Employee

02-11-2018

I've had some luck prototyping a solution using Jackrabbit OCM.  The major issue I have with it is that it has a lot of slightly dated dependencies and looks like it's not been updated in quite some time.  That being said, here's what I did to get it working.

First, in your pom add the following dependencies:

        <dependency>

            <groupId>org.apache.jackrabbit</groupId>

            <artifactId>jackrabbit-ocm</artifactId>

            <version>1.5.3</version>

        </dependency>

        <dependency>

            <groupId>commons-beanutils</groupId>

            <artifactId>commons-beanutils</artifactId>

            <version>1.7.0</version>

        </dependency>

        <dependency>

            <groupId>commons-digester</groupId>

            <artifactId>commons-digester</artifactId>

            <version>1.7</version>

        </dependency>

        <dependency>

            <groupId>cglib</groupId>

            <artifactId>cglib</artifactId>

            <version>2.2</version>

        </dependency>

        <dependency>

            <groupId>asm</groupId>

            <artifactId>asm</artifactId>

            <version>3.3.1</version>

        </dependency>

        <dependency>

            <groupId>asm</groupId>

            <artifactId>asm-util</artifactId>

            <version>3.3.1</version>

        </dependency>

        <dependency>

            <groupId>asm</groupId>

            <artifactId>asm-tree</artifactId>

            <version>3.3.1</version>

        </dependency>

        <dependency>

            <groupId>asm</groupId>

            <artifactId>asm-analysis</artifactId>

            <version>3.3.1</version>

        </dependency>

Next in the maven-bundle-plugin, change the import-package section to make org.apache.tools.ant.* optional and embed the dependencies used by OCM:

            <plugin>

                <groupId>org.apache.felix</groupId>

                <artifactId>maven-bundle-plugin</artifactId>

                <extensions>true</extensions>

                <configuration>

                    <instructions>

                        <Bundle-SymbolicName>com.mybundle.name</Bundle-SymbolicName>

                        <!-- Import any version of javax.inject, to allow running on multiple versions of AEM

                            We don't want Apache ANT in the build because it's not needed, making it optional allows the bundle to start without it.

                        -->

                        <Import-Package>

                            javax.inject;version=0.0.0,

                            sun.misc.*;resolution:=optional,

                            org.apache.tools.ant.*;resolution:=optional,

                            *

                        </Import-Package>

                        <!-- Embed OCM and all its dependencies except for Ant -->

                        <Embed-Dependency>jackrabbit-ocm,commons-beanutils,commons-digester,cglib,asm,asm-util,asm-tree,asm-analysis</Embed-Dependency>

                        <Sling-Model-Packages>

                              com.mypackage.model.yaddayadda

                        </Sling-Model-Packages>

                    </instructions>

                </configuration>

            </plugin>

In order to use OCM you have to put a @Node annotation on the class itself and then use the @Field annotation on each field you want to persist.  I really don't know how well it deals with structures like arrays/lists but I got it to work with basic stuff easily enough once I worked out the dependency issues.  Here's an example base class you can use as a starting point.  Have your sling models extend this base class (but don't forget: Use the @Node annotation on each model class!) and then you will have persistence methods which hopefully do the trick:

package com.mypackage.model;

import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.ArrayList;

import java.util.List;

import javax.annotation.PostConstruct;

import javax.jcr.Session;

import org.apache.jackrabbit.ocm.manager.ObjectContentManager;

import org.apache.jackrabbit.ocm.manager.impl.ObjectContentManagerImpl;

import org.apache.jackrabbit.ocm.mapper.Mapper;

import org.apache.jackrabbit.ocm.mapper.impl.annotation.AnnotationMapperImpl;

import org.apache.jackrabbit.ocm.mapper.impl.annotation.Field;

import org.apache.jackrabbit.ocm.mapper.impl.annotation.Node;

import org.apache.sling.api.SlingHttpServletRequest;

import org.apache.sling.api.resource.Resource;

import org.apache.sling.api.resource.ResourceResolver;

import org.apache.sling.models.annotations.injectorspecific.Self;

import org.apache.sling.models.annotations.injectorspecific.SlingObject;

import org.osgi.service.component.annotations.Reference;

/**

* Describes a model class which can be persisted

*/

@Node

public class PersistentModel {

    @Reference

    ObjectContentManager ocm;

    @Self

    private SlingHttpServletRequest request;

    @SlingObject

    @Self

    private Resource resource;

    @Field(path = true)

    private String path;

    @JsonIgnore

    public SlingHttpServletRequest getRequest() {

        return request;

    }

    @JsonIgnore

    public Resource getResource() {

        return resource;

    }

    public void setPath(String path) {

        this.path = path;

    }

    @JsonIgnore

    public String getPath() {

        return path;

    }

    @PostConstruct

    public void init() throws Exception {

        if (resource != null && request != null) {

            resource = request.getResource();

        }

        path = resource.getPath();

        if (ocm == null) {

            ResourceResolver rr = resource.getResourceResolver();

            List<Class> classes = new ArrayList<>();

            classes.add(this.getClass());

            classes.add(PersistentModel.class);

            Mapper mapper = new AnnotationMapperImpl(classes);

            ocm = new ObjectContentManagerImpl(rr.adaptTo(Session.class), mapper);

        }

    }

    public void update() {

        ocm.update(this);

        ocm.save();

    }

    public void insert(String path) {

        if (path.equals(this.path) && ocm.objectExists(path)) {

            update();

        } else {

            this.path = path;

            ocm.insert(this);

            ocm.save();

        }

    }

}

This is about as far as I was able to take it.  I would hope that sling models will eventually evolve to provide this functionality as having OCM annotations feels rather redundant, and I'm almost sure that OCM cannot handle complex structures as well as Sling models can.  Still, with this solution in-hand it might still work for more basic solutions and give you a head-start.  Good luck!

-Brendan