How to access a JCR Repository from Spring Boot

Apache Oak is a scalable, high-performance hierarchical content repository designed for use as the foundation of modern world-class web sites and other demanding content applications. It is built on top of the Java Content Repository (JCR) standard, and it is an implementation of the Apache Jackrabbit project.

Using Apache Oak with Spring Boot can be a powerful combination for building content-driven applications. In this article, we will walk through the steps of integrating Apache Oak with a Spring Boot application.

Step 1: Set up the Dependencies

To use Apache Oak with Spring Boot, you will need to include the following dependencies in your project’s pom.xml file:

<dependency>
	<groupId>org.apache.jackrabbit</groupId>
	<artifactId>oak-jcr</artifactId>
	<version>${oak.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.jackrabbit</groupId>
	<artifactId>oak-store-document</artifactId>
	<version>${oak.version}</version>
</dependency>
<dependency>
	<groupId>javax.jcr</groupId>
	<artifactId>jcr</artifactId>
	<version>2.0</version>
</dependency>

The oak-jcr dependency is the Apache Oak JCR implementation, which provides the actual implementation of the JCR specification.

The oak-store-document dependecy provides base functionality and patterns for utilizing Jackrabbit Oak content management capabilities.

Finally, the jcr dependency is the Content Repository for Java Technology API.

Additionally, you may need to include extra dependencies if you want to store your Documents in a Store which is not a File System. For example, to store your Documents in Mongo DB, include the MongoDB Driver as dependency:

  <dependency>
      <groupId>org.mongodb</groupId>
      <artifactId>mongo-java-driver</artifactId>
      <version>3.12.11</version>
  </dependency>

Step 2: Configure the Repository

Next, you will need to configure the repository for your application. The following @Configuration Bean contains a reference to the javax.jcr.Repository:

@Configuration
public class JackRabbitRepositoryBuilder {
    static Repository repo = null;
    static Logger logger = LoggerFactory.getLogger(JackRabbitRepositoryBuilder.class);


    public static Repository getRepo(String host, final int port) {
        try {
            String uri = "mongodb://" + host + ":" + port;
            DocumentNodeStore ns = new MongoDocumentNodeStoreBuilder()
    .setMongoDB("mongodb://localhost:27017", "oak", 0).build();
            repo = new Jcr(new Oak(ns)).createRepository();
        } catch (Exception e) {
            logger.error("Exception caught: " + e.getLocalizedMessage());
            e.printStackTrace();
        }

        return repo;
    }
}

As you can see from the above code, we are accessing a DocumentStore with a MongoDocumentNodeStoreBuilder. This factory Class creates the DocumentNodeStore passing as argument the MongoDB Connection URL.

Step 3: Accessing the Nodes

The javax.jcr.Node interface is a part of the Java Content Repository (JCR) API. It represents a node in a JCR repository.

A node is an individual unit of content that is stored in a JCR repository. It is a structured piece of data that consists of properties and child nodes. Properties are name-value pairs that store the actual data, while child nodes are other nodes that are contained within a parent node.

The Node interface provides methods for accessing and manipulating the properties and child nodes of a node. It also provides methods for creating and deleting nodes, as well as for moving and copying them within the repository.

The following JackRabbitService Service Class contains the basic methods to create/edit/delete a Node. It also contains a createFolderNode to organize your Nodes in Folders:

@Service
public class JackRabbitService {
    Logger logger = LoggerFactory.getLogger(JackRabbitService.class);

    public Node createNode(Session session, RabbitNode input, MultipartFile uploadFile) {
        Node node = null;
        File file = new File(uploadFile.getOriginalFilename());

        try {
            Node parentNode = session.getNodeByIdentifier(input.getParentId());
                if (parentNode != null && parentNode.hasNode(file.getName())) {
                    logger.error(file.getName() + " node already exists!");
                    return editNode(session, input, uploadFile);
                } else {
                    try {
                        node = parentNode.addNode(file.getName(), "nt:file");
                        node.addMixin("mix:versionable");
                        node.addMixin("mix:referenceable");

                        Node content = node.addNode("jcr:content", "nt:resource");

                        InputStream inputStream = uploadFile.getInputStream();
                        Binary binary = session.getValueFactory().createBinary(inputStream);

                        content.setProperty("jcr:data", binary);
                        content.setProperty("jcr:mimeType", input.getMimeType());

                        Date now = new Date();
                        now.toInstant().toString();
                        content.setProperty("jcr:lastModified", now.toInstant().toString());

                        inputStream.close();
                        session.save();

                        VersionManager vm = session.getWorkspace().getVersionManager();
                        vm.checkin(node.getPath());

                        logger.error("File saved!");
                    } catch (Exception e) {
                        logger.error("Exception caught!");
                        e.printStackTrace();
                    }
            }
        } catch (Exception e) {
            logger.error("Exception caught!");
            e.printStackTrace();
        }
        return node;
    }

    public boolean deleteNode(Session session, RabbitNode input) {
        try {
            Node node = session.getNodeByIdentifier(input.getFileId());
            if (node != null) {
                node.remove();
                session.save();
                return true;
            }
        } catch (Exception e) {
            logger.error("Exception caught!");
            e.printStackTrace();
        }

        return false;
    }

    public List<String> getVersionHistory(Session session, RabbitNode input) {
        List<String> versions = new ArrayList<>();
        try {
            VersionManager vm = session.getWorkspace().getVersionManager();

            Node node = session.getNodeByIdentifier(input.getFileId());
            String filePath = node.getPath();
            if (session.itemExists(filePath)) {
                VersionHistory versionHistory = vm.getVersionHistory(filePath);
                Version currentVersion = vm.getBaseVersion(filePath);
                logger.error("Current version: " + currentVersion.getName());

                VersionIterator versionIterator = versionHistory.getAllVersions();
                while (versionIterator.hasNext()) {
                    versions.add(((Version) versionIterator.next()).getName());
                }
            }
        } catch (Exception e) {
            logger.error("Exception caught!");
            e.printStackTrace();
        }
        return versions;
    }

    public Node editNode(Session session, RabbitNode input, MultipartFile uploadFile) {
        File file = new File(uploadFile.getOriginalFilename());
        Node returnNode = null;

        try {
            Node parentNode = session.getNodeByIdentifier(input.getParentId());
            if (parentNode != null && parentNode.hasNode(file.getName())) {
                VersionManager vm = session.getWorkspace().getVersionManager();

                Node fileNode = parentNode.getNode(file.getName());
                vm.checkout(fileNode.getPath());

                Node content = fileNode.getNode("jcr:content");

                InputStream is = uploadFile.getInputStream();
                Binary binary = session.getValueFactory().createBinary(is);
                content.setProperty("jcr:data", binary);

                session.save();
                is.close();

                vm.checkin(fileNode.getPath());
                returnNode = fileNode;
            }
        } catch(Exception e) {
                logger.error("Exception caught");
                e.printStackTrace();
        }

        return returnNode;
    }

    public Node createFolderNode(Session session, RabbitNode input) {
        Node node = null;
        Node parentNode = null;

        try {
            parentNode = session.getNodeByIdentifier(input.getParentId());
            if (session.nodeExists(parentNode.getPath())) {
                if (!parentNode.hasNode(input.getFileName())) {
                    node = parentNode.addNode(input.getFileName(), "nt:folder");
                    node.addMixin("mix:referenceable");
                    session.save();
                    System.out.println("Folder created: "+input.getFileName());
                }
            } else {
                logger.error("Node already exists!");
            }
        } catch (Exception e) {
            logger.error("Exception caught!");
            e.printStackTrace();
        }

        return node;
    }

    public FileResponse getNode(Session session, String versionId, RabbitNode input) {
        FileResponse response = new FileResponse();

        try {
            Node file = session.getNodeByIdentifier(input.getFileId());
            if (file != null) {
                VersionManager vm = session.getWorkspace().getVersionManager();
                VersionHistory history = vm.getVersionHistory(file.getPath());
                for (VersionIterator it = history.getAllVersions(); it.hasNext(); ) {
                    Version version = (Version) it.next();
                    if (versionId.equals(version.getName())) {
                        file = version.getFrozenNode();
                        break;
                    }
                }

                logger.error("Node retrieved: " + file.getPath());

                Node fileContent = file.getNode("jcr:content");
                Binary bin = fileContent.getProperty("jcr:data").getBinary();
                InputStream stream = bin.getStream();
                byte[] bytes = IOUtils.toByteArray(stream);
                bin.dispose();
                stream.close();

                response.setBytes(bytes);
              //  response.setContentType(fileContent.getProperty("jcr:mimeType").getString());
                response.setContentType(input.getMimeType());
                return response;

            } else {
                logger.error("Node does not exist!");
            }

        } catch (Exception e) {
            logger.error("Exception caught!");
            e.printStackTrace();
        }

        return response;
    }
}

Part 4: Expose the Service with a REST Endpoint

To allow accessing our JackRabbitService Service we will add a Rest Endpoint which maps the Service methods:

@RestController
@RequestMapping("/services")
public class RabbitController {
    Repository repo = null;


    public RabbitController(JackRabbitService jackRabbitService) {
        this.jackRabbitService = jackRabbitService;
        repo = JackRabbitRepositoryBuilder.getRepo("localhost", 27017);
    }

    @Autowired
    JackRabbitService jackRabbitService;

    @RequestMapping(method = RequestMethod.POST, value = "/createRoot")
    public String createRoot() throws RepositoryException {
        Session session = JackRabbitUtils.getSession(repo);

        System.out.println("createRoot called!");

        RabbitNode input = new RabbitNode("/", "oak", "", "");
        Node node = jackRabbitService.createFolderNode(session,input);

        String identifier = node.getIdentifier();
        JackRabbitUtils.cleanUp(session);
        return identifier;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/createFolder")
    public String createFolderNode(@RequestBody RabbitNode input) throws RepositoryException {
        Session session = JackRabbitUtils.getSession(repo);

        System.out.println("createFolderNode called!");
        System.out.println("parentId: " + input.getParentId());
        System.out.println("filePath: " + input.getFileName());
        System.out.println("mimeType: " + input.getMimeType());
        System.out.println("fileId: " + input.getFileId());

        Node node = jackRabbitService.createFolderNode(session, input);

        String identifier = node.getIdentifier();
        JackRabbitUtils.cleanUp(session);
        return identifier;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/createFile")
    public String createNode(@RequestParam(value = "parent") String parent, @RequestParam(value = "file") MultipartFile file) throws RepositoryException {
        Session session = JackRabbitUtils.getSession(repo);
        RabbitNode input = new RabbitNode(parent, file.getOriginalFilename(), URLConnection.guessContentTypeFromName(file.getName()), "");

        System.out.println("createNode called!");
        System.out.println("parentId: " + input.getParentId());
        System.out.println("filePath: " + input.getFileName());
        System.out.println("mimeType: " + input.getMimeType());
        System.out.println("fileId: " + input.getFileId());

        Node node = jackRabbitService.createNode(session, input, file);
        String identifier = node.getIdentifier();
        session.getNodeByIdentifier(input.getParentId());

        return identifier;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/deleteFile")
    public boolean deleteNode(@RequestBody RabbitNode input) {
        Session session = JackRabbitUtils.getSession(repo);

        System.out.println("deleteNode called!");
        System.out.println("parentId: " + input.getParentId());
        System.out.println("filePath: " + input.getFileName());
        System.out.println("mimeType: " + input.getMimeType());
        System.out.println("fileId: " + input.getFileId());

        boolean result = jackRabbitService.deleteNode(session, input);
        JackRabbitUtils.cleanUp(session);
        return result;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/getVersions")
    public List<String> getVersionHistory(@RequestBody RabbitNode input) {
        Session session = JackRabbitUtils.getSession(repo);

        System.out.println("getVersionHistory called!");
        System.out.println("parentId: " + input.getParentId());
        System.out.println("filePath: " + input.getFileName());
        System.out.println("mimeType: " + input.getMimeType());
        System.out.println("fileId: " + input.getFileId());

        return jackRabbitService.getVersionHistory(session, input);
    }

    @RequestMapping(method = RequestMethod.POST, value = "/getFile/{versionId}")
    public FileResponse getNode(@PathVariable String versionId, @RequestBody RabbitNode input) {
        Session session = JackRabbitUtils.getSession(repo);
        FileResponse response = null;

        System.out.println("getNode called!");
        System.out.println("parentId: " + input.getParentId());
        System.out.println("filePath: " + input.getFileName());
        System.out.println("mimeType: " + input.getMimeType());
        System.out.println("fileId: " + input.getFileId());

        response = jackRabbitService.getNode(session, versionId, input);
        return response;
    }
}

Connecting to Mongo DB

Finally, to allow the Connection to MongoDB we will start a Docker Image of MongoDB which exposes the default Port:

docker run -ti --rm -p 27017:27017 mongo:4.0

Source code: You can find the source code for this article on GitHub: https://github.com/fmarchioni/masterspringboot/tree/master/nosql/oak-demo