JCR is practically a sub-culture within the Java world. Most Java developers have never heard about it. For the Java developers that actually work with CMS it is a super important technology in their everyday work.
I believe it is relevant to learn about what it is and what it can be used for.
Disclaimer: I think JCR is a very interesting technology, but I have never worked with it professionally hands on, so I am just a beginner at JCR myself. All the code below is tested to work, but some of it may not be considede best practice among JCR professionals.
First question is obviously: what is JCR?
JCR is Java Content Repository - a standard Java API for content repositories.
Second question: what is a content repository?
A content repository is an abstract tree structure providing a core CMS engine.
A normal CMS application architecture looks like:
and it contains:
JCR provide a standard API to the CMS engine decoupling the applications from the CMS engine:
and it contains:
Now we see the benefits of the standard API:
That is pretty good.
Third question: what is the point in the tree structure?
In general a tree structure is much more convenient than a traditional relational database structure for a CMS.
To illustrate that let us look at a classic CMS usage: a forum.
Everybody knows how a forum works:
Object oriented it look like:
This is actually pretty easy to implement using a traditional relational database:
Topic |
---|
id(INTEGER),PK |
name(VARCHAR),IX |
description(VARCHAR) |
Post |
---|
id(INTEGER),PK |
title(VARCHAR),IX |
body(VARCHAR) |
author(VARCHAR),IX |
time(DATETIME),IX |
topic(INTEGER),FK->Topic,IX |
Comment |
---|
id(INTEGER),PK |
number(VARCHAR),IX |
text(VARCHAR) |
author(VARCHAR),IX |
time(DATETIME),IX |
post(INTEGER),FK->Post,IX |
But now comes some additional requirements:
Implementing #1 and #2 will cause the SQL to become absolute horrible. And implementing #3 will either be significant work or have to rely on database specific features.
Implementing #1 with a tree structure is much easier:
And after considering it a bit then one realize that #2 (versions) can also easily be solved by adding version nodes below the node itself in the tree.
So a tree structure is definitely what we want to work with - from API perspective.
JCR provide the standard API and the abstract tree structure.
JCR 1.0 was defined in JSR 170 in 2005.
JCR 2.0 was defined in JSR 283 in 2009.
Several implementations of JCR exist:
JCR repository | Status | Used by CMS products | Storage support |
---|---|---|---|
Apache Jackrabbit | Open source | AEM (Adobe ExperienceManager) Jahia Hippo/BloomReach Magnolia |
Relational databases (Oracle DB, MS SQLServer, MySQL/MariaDB, PostgreSQL) File system |
JBoss ModeShape | Open source | JBoss/WildFly | Relational databases (Oracle DB, MySQL/MariaDB, PostgreSQL) NoSQL databases (MongoDB, Cassandra) File system |
Alfresco | Open source &commercial | Alfresco | Relational databases (Oracle DB, IBM DB2, MS SQLServer, MySQL/MariaDB, PostgreSQL) |
eXo | Open source &commercial | eXo | Relational databases (Oracle DB, IBM DB2, MS SQLServer, MySQL/MariaDB, PostgreSQL, Sybase ASE) File system |
Apache Jackrabbit Oak | Open source | Relational databases (Oracle DB, IBM DB2, MS SQLServer, MySQL/MariaDB, PostgreSQL) NoSQL databases (MongoDB) |
Note that even when a JCR repository is stored in a relational database, then the data is not easily accessible outside the JCR API. The storage format in the database is not documented and usually very convoluted.
Let us try and look at how one actually use the JCR API.
The way to get a connection to the repository via JCR API in a repository independent way is to:
private static Repository getRepository(Map<String, String> parameters) throws RepositoryException {
for (RepositoryFactory factory : ServiceLoader.load(RepositoryFactory.class)) {
Repository repo = factory.getRepository(parameters);
if(repo != null) {
return repo;
}
}
return null;
}
...
Repository repo = getRepository(parameters);
Session session = repo.login(new SimpleCredentials(usr, pwd.toCharArray()));
In JCR nodes are identified with a path of node names. Example:
For the forum example we can use:
Node type | Node path |
---|---|
Forum | /F |
Topic | /F/topicname |
Post | /F/topicname/posttitle |
Comment to post | /F/topicname/posttitle/commentnumber |
Comment to comment to post | /F/topicname/posttitle/commentnumber/commentnumber |
Comment to comment to comment to post | /F/topicname/posttitle/commentnumber/commentnumber/commentnumber |
Get root node (top node):
Node rootnode = session.getRootNode();
Add node below existing node:
Node child = parent.addNode(name);
...
session.save();
Get node below existing node:
Node node = abovenode.getNode(relativepath);
(relative path is just name if only 1 level below)
Nodes in JCR comes in two flavors:
For unstructured nodes the developer just define whatever properties desired on a per node basis.
For typed nodes the properties are defined.
All the discission and code below assume unstructured nodes.
For the forum example it may have been possible to define node types for forum, topic, post and comment. But my feeling is that unstructued is the most JCR'ish (even though I in general like strongly typed programming languages).
Set properties:
node.setProperty("iprop", 123);
node.setProperty("sprop", "ABC");
...
session.save();
Get properties:
long iv = node.getProperty("iprop").getLong();
String sv = node.getProperty("sprop").getString();
JCR provides several options for querying (searching) the node tree:
QOM (Query Object Model) is a low level programmtic API.
SQL2 is as the name indicate a database SQL inspired language and pretty high level.
All example below will use SQL2.
The SQL syntax are like:
SELECT * FROM [nt:unstructured] WHERE ...
WHERE conditions can be:
Syntax | Semantics |
---|---|
p = 123 p = 'ABC' |
select nodes where the property has the specified value |
p IS NOT NULL | select nodes that do have property |
p IS NULL | select nodes that do not have property |
CONTAINS(p, 'ABC') | select nodes where the property contains the specified value |
The CONTAINS search could be a very slow opertation if the JCR implementation implement it by iterating over every instance of the node property and do a simple string search in each.
Any good JCR implementation use a full text search engine to make such queries fast.
Both Apache Jackrabbit and JBoss ModeShape can use Lucene as full text search engine. Lucene is what Wikipedia is using, so it can handle really large data sizes very efficiently.
To execute a query:
Query q = session.getWorkspace().getQueryManager().createQuery(sqlstr, Query.JCR_SQL2);
QueryResult qr = q.execute();
NodeIterator nit = qr.getNodes();
while(nit.hasNext()) {
Node n = nit.nextNode();
...
}
JCR uses a version manager and a check-in/check-out model to manager versions.
Code to make a node versionable (it is not per default):
node.addMixin(NodeType.MIX_VERSIONABLE);
...
session.save();
Code to add new versions::
VersionManager vm = session.getWorkspace().getVersionManager();
vm.checkout(absolutepath);
ses.save();
...
node.setProperty("prop", newval);
ses.save();
...
vm.checkin(path);
ses.save();
Code to get all versions:
VersionManager vm = session.getWorkspace().getVersionManager();
VersionHistory vhist = vm.getVersionHistory(n.getPath());
VersionIterator vit = vhist.getAllVersions();
while(vit.hasNext()) {
Version v = vit.nextVersion();
NodeIterator vnit = v.getNodes();
while(vnit.hasNext()) {
Node vn = vnit.nextNode();
...
}
}
This is a complete example showing an implementation of previuously described forum using JCR.
ForumManager.java:
package dk.vajhoej.forum;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.version.VersionManager;
public class ForumManager {
// use dash instead of space and special characters in path parts
static String nameFixUp(String name) {
return name.replaceAll("[ /]", "-").replaceAll("[^A-Za-z0-9#-]", "");
}
// number formats:
// #n
// #n.m
private String getAutoNumber(Node parent) throws ValueFormatException, PathNotFoundException, RepositoryException, ForumException {
String num = Long.toString(parent.getNodes("*").getSize() + 1);
if(isPost(parent)) {
return "#" + num;
} else if(isComment(parent)) {
return parent.getProperty("number").getString() + "." + num;
} else {
throw new ForumException("Comment parent is neither post nor comment");
}
}
// check that can be used to avoid duplicates
private boolean alreadyHasChild(Node parent, String nodename) throws RepositoryException {
return parent.getNodes(nodename).getSize() > 0;
}
// a permanent open session per ForumManager instance
private Session ses;
public ForumManager(Repository repo, String usr, String pwd) throws RepositoryException {
// login
this.ses = repo.login(new SimpleCredentials(usr, pwd.toCharArray()));
// ensure forum root exists
if(!ses.getRootNode().hasNode("F")) {
ses.getRootNode().addNode("F");
}
}
Session getSession() {
return ses;
}
// test nodes
boolean isTopic(Node n) throws RepositoryException {
return n.hasProperty("name") && n.hasProperty("description");
}
boolean isPost(Node n) throws RepositoryException {
return n.hasProperty("title") && n.hasProperty("body") && n.hasProperty("author");
}
boolean isComment(Node n) throws RepositoryException {
return n.hasProperty("number") && n.hasProperty("text") && n.hasProperty("author");
}
// find nodes
Node getAnyNode(String path) throws PathNotFoundException, RepositoryException {
return ses.getRootNode().getNode(path.substring(1));
}
// add nodes
public String addTopic(String topicName, String topicDescription) throws RepositoryException {
Node parent = getAnyNode("/F");
String nodename = nameFixUp(topicName);
if(!alreadyHasChild(parent, nodename)) {
Node child = parent.addNode(nodename);
child.setProperty("name", topicName);
child.setProperty("description", topicDescription);
ses.save();
return "/F/" + nodename;
} else {
return null;
}
}
public String addPost(String path, String postTitle, String postBody, String postAuthor) throws RepositoryException, ForumException {
Node parent = getAnyNode(path);
if(!isTopic(parent)) {
throw new ForumException("Trying to add post to non-topic");
}
String nodename = nameFixUp(postTitle);
if(!alreadyHasChild(parent, nodename)) {
Node child = parent.addNode(nodename);
child.addMixin(NodeType.MIX_VERSIONABLE);
child.setProperty("title", postTitle);
child.setProperty("body", postBody);
child.setProperty("author", postAuthor);
child.setProperty("time", System.currentTimeMillis());
ses.save();
return path + "/" + nodename;
} else {
return null;
}
}
public String addComment(String path, String commentNumber, String commentText, String commentAuthor) throws RepositoryException, ForumException {
Node parent = getAnyNode(path);
if(!isPost(parent) && !isComment(parent)) {
throw new ForumException("Trying to add comment to non-post and non-comment");
}
String nodename = nameFixUp(commentNumber);
if(!alreadyHasChild(parent, nodename)) {
Node child = parent.addNode(nodename);
child.addMixin(NodeType.MIX_VERSIONABLE);
child.setProperty("number", commentNumber);
child.setProperty("text", commentText);
child.setProperty("author", commentAuthor);
child.setProperty("time", System.currentTimeMillis());
ses.save();
return path + "/" + nodename;
} else {
return null;
}
}
public String addCommentAutoNumber(String path, String commentText, String commentAuthor) throws RepositoryException, ForumException {
String cn = getAutoNumber(getAnyNode(path));
return addComment(path, cn, commentText, commentAuthor);
}
// update nodes
public void updatePost(String path, String newPostBody) throws RepositoryException, ForumException {
VersionManager vm = ses.getWorkspace().getVersionManager();
// if first update save original version
if(vm.getVersionHistory(path).getAllVersions().getSize() <= 1) {
vm.checkout(path);
ses.save();
vm.checkin(path);
ses.save();
}
// checkout
vm.checkout(path);
ses.save();
// update
Node node = getAnyNode(path);
if(!isPost(node)) {
throw new ForumException("Updating non-post as post");
}
node.setProperty("body", newPostBody);
node.setProperty("time", System.currentTimeMillis());
ses.save();
// checkin
vm.checkin(path);
ses.save();
}
public void updateComment(String path, String newCommentText) throws RepositoryException, ForumException {
VersionManager vm = ses.getWorkspace().getVersionManager();
// if first update save original version
if(vm.getVersionHistory(path).getAllVersions().getSize() <= 1) {
vm.checkout(path);
ses.save();
vm.checkin(path);
ses.save();
}
// checkout
vm.checkout(path);
ses.save();
// update
Node node = getAnyNode(path);
if(!isComment(node)) {
throw new ForumException("Updating non-comment as comment");
}
node.setProperty("text", newCommentText);
node.setProperty("time", System.currentTimeMillis());
ses.save();
// checkin
vm.checkin(path);
ses.save();
}
}
ForumException.java:
package dk.vajhoej.forum;
public class ForumException extends Exception {
private static final long serialVersionUID = 1L;
public ForumException(String msg) {
super(msg);
}
}
Forum.java:
package dk.vajhoej.forum.model;
import java.util.ArrayList;
import java.util.List;
public class Forum {
private List<Topic> topics;
public Forum() {
this.topics = new ArrayList<Topic>();
}
public List<Topic> getTopics() {
return topics;
}
}
Topic.java:
package dk.vajhoej.forum.model;
import java.util.ArrayList;
import java.util.List;
public class Topic {
private String name;
private String description;
private List<Post> posts;
public Topic(String name, String description) {
this.name = name;
this.description = description;
this.posts = new ArrayList<>();
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public List<Post> getPosts() {
return posts;
}
}
Post.java:
package dk.vajhoej.forum.model;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class Post {
private String author;
private Date time;
private String title;
private String body;
private List<Comment> comments;
private List<Post> versions;
public Post(String author, String title, String body) {
this(author, new Date(), title, body);
}
public Post(String author, Date time, String title, String body) {
this.author = author;
this.time = time;
this.title = title;
this.body = body;
this.comments = new ArrayList<Comment>();
this.versions = new ArrayList<Post>();
}
public String getAuthor() {
return author;
}
public Date getTime() {
return time;
}
public String getTitle() {
return title;
}
public String getBody() {
return body;
}
public List<Comment> getComments() {
return comments;
}
public List<Post> getVersions() {
return versions;
}
}
Comment.java:
package dk.vajhoej.forum.model;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class Comment {
private String author;
private Date time;
private String number;
private String text;
private List<Comment> comments;
private List<Comment> versions;
public Comment(String author, String text) {
this(author, null, text);
}
public Comment(String author, String number, String text) {
this(author, new Date(), number, text);
}
public Comment(String author, Date time, String number, String text) {
this.author = author;
this.time = time;
this.number = number;
this.text = text;
this.comments = new ArrayList<Comment>();
this.versions = new ArrayList<Comment>();
}
public String getAuthor() {
return author;
}
public Date getTime() {
return time;
}
public String getNumber() {
return number;
}
public String getText() {
return text;
}
public List<Comment> getComments() {
return comments;
}
public List<Comment> getVersions() {
return versions;
}
}
ModelMapper.java:
package dk.vajhoej.forum;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.VersionManager;
import dk.vajhoej.forum.model.Comment;
import dk.vajhoej.forum.model.Forum;
import dk.vajhoej.forum.model.Post;
import dk.vajhoej.forum.model.Topic;
public class ModelMapper {
private Topic nodeToTopic(Node n) throws ValueFormatException, PathNotFoundException, RepositoryException {
return new Topic(n.getProperty("name").getString(),
n.getProperty("description").getString());
}
private Post nodeToPost(Node n) throws ValueFormatException, PathNotFoundException, RepositoryException {
return new Post(n.getProperty("author").getString(),
new Date(n.getProperty("time").getLong()),
n.getProperty("title").getString(),
n.getProperty("body").getString());
}
private Comment nodeToComment(Node n) throws ValueFormatException, PathNotFoundException, RepositoryException {
return new Comment(n.getProperty("author").getString(),
new Date(n.getProperty("time").getLong()),
n.getProperty("number").getString(),
n.getProperty("text").getString());
}
private void loadTopics(List<Topic> topics, boolean recurse) throws RepositoryException, ForumException {
NodeIterator it = fmgr.getSession().getRootNode().getNode("F").getNodes("*");
while(it.hasNext()) {
Node n = it.nextNode();
if(fmgr.isTopic(n)) {
Topic t = nodeToTopic(n);
topics.add(t);
if(recurse) {
loadPosts(t.getPosts(), n, true);
}
} else {
throw new ForumException("Forum has non-topic child");
}
}
}
private void loadPosts(List<Post> posts, Node parent, boolean recurse) throws RepositoryException, ForumException {
if(!fmgr.isTopic(parent)) {
throw new ForumException("Trying to get posts of non-topic");
}
NodeIterator it = parent.getNodes("*");
while(it.hasNext()) {
Node n = it.nextNode();
if(fmgr.isPost(n)) {
Post p = nodeToPost(n);
posts.add(p);
VersionManager vm = fmgr.getSession().getWorkspace().getVersionManager();
VersionHistory vhist = vm.getVersionHistory(n.getPath());
VersionIterator vit = vhist.getAllVersions();
while(vit.hasNext()) {
Version v = vit.nextVersion();
NodeIterator vnit = v.getNodes();
while(vnit.hasNext()) {
Node vn = vnit.nextNode();
if(fmgr.isPost(vn)) {
p.getVersions().add(nodeToPost(vn));
}
}
}
if(recurse) {
loadComments(p.getComments(), n, true);
}
} else {
throw new ForumException("Topic has non-post child");
}
}
}
private void loadComments(List<Comment> comments, Node parent, boolean recurse) throws RepositoryException, ForumException {
if(!fmgr.isPost(parent) && !fmgr.isComment(parent)) {
throw new ForumException("Trying to get comemnts of non-topic and non-comment");
}
NodeIterator it = parent.getNodes("*");
while(it.hasNext()) {
Node n = it.nextNode();
if(fmgr.isComment(n)) {
Comment c = nodeToComment(n);
comments.add(c);
VersionManager vm = fmgr.getSession().getWorkspace().getVersionManager();
VersionHistory vhist = vm.getVersionHistory(n.getPath());
VersionIterator vit = vhist.getAllVersions();
while(vit.hasNext()) {
Version v = vit.nextVersion();
NodeIterator vnit = v.getNodes();
while(vnit.hasNext()) {
Node vn = vnit.nextNode();
if(fmgr.isComment(vn)) {
c.getVersions().add(nodeToComment(vn));
}
}
}
if(recurse) {
loadComments(c.getComments(), n, true);
}
} else {
throw new ForumException("Post/comment has non-comment child");
}
}
}
// mapping between model and JCR
private ForumManager fmgr;
public ModelMapper(ForumManager fmgr) {
this.fmgr = fmgr;
}
// load
public Forum loadForum() throws RepositoryException, ForumException {
Forum f = new Forum();
loadTopics(f.getTopics(), true);
return f;
}
public Topic loadTopic(String path, boolean recurse) throws PathNotFoundException, RepositoryException, ForumException {
Node n = fmgr.getAnyNode(path);
if(fmgr.isTopic(n)) {
Topic t = nodeToTopic(n);
if(recurse) {
loadPosts(t.getPosts(), n, true);
}
return t;
} else {
throw new ForumException("Path is not a topic path: " + path);
}
}
public Topic loadTopic(String path) throws PathNotFoundException, RepositoryException, ForumException {
return loadTopic(path, false);
}
public Post loadPost(String path, boolean recurse) throws PathNotFoundException, RepositoryException, ForumException {
Node n = fmgr.getAnyNode(path);
if(fmgr.isPost(n)) {
Post p = nodeToPost(n);
if(recurse) {
loadComments(p.getComments(), n, true);
}
return p;
} else {
throw new ForumException("Path is not a post path: " + path);
}
}
public Post loadPost(String path) throws PathNotFoundException, RepositoryException, ForumException {
return loadPost(path, false);
}
public Comment loadComment(String path, boolean recurse) throws PathNotFoundException, RepositoryException, ForumException {
Node n = fmgr.getAnyNode(path);
if(fmgr.isComment(n)) {
Comment c = nodeToComment(n);
if(recurse) {
loadComments(c.getComments(), n, true);
}
return c;
} else {
throw new ForumException("Path is not a comment path: " + path);
}
}
public Comment loadComment(String path) throws PathNotFoundException, RepositoryException, ForumException {
return loadComment(path, false);
}
// search
public List<Post> findPostByAuthor(String author, boolean recurse) throws RepositoryException, ForumException {
List<Post> res = new ArrayList<>();
Query q = fmgr.getSession().getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:unstructured] WHERE author = $author AND title IS NOT NULL AND number IS NULL", Query.JCR_SQL2);
q.bindValue("author", fmgr.getSession().getValueFactory().createValue(author));
QueryResult qr = q.execute();
NodeIterator nit = qr.getNodes();
while(nit.hasNext()) {
Node n = nit.nextNode();
if(fmgr.isPost(n)) {
Post p = nodeToPost(n);
res.add(p);
if(recurse) {
loadComments(p.getComments(), n, true);
}
}
}
return res;
}
public List<Post> findPostByAuthor(String author) throws RepositoryException, ForumException {
return findPostByAuthor(author, false);
}
public List<Post> findPostByContent(String content, boolean recurse) throws RepositoryException, ForumException {
List<Post> res = new ArrayList<>();
Query q = fmgr.getSession().getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:unstructured] WHERE CONTAINS(body,$content) AND title IS NOT NULL AND number IS NULL", Query.JCR_SQL2);
q.bindValue("content", fmgr.getSession().getValueFactory().createValue(content));
QueryResult qr = q.execute();
NodeIterator nit = qr.getNodes();
while(nit.hasNext()) {
Node n = nit.nextNode();
if(fmgr.isPost(n)) {
Post p = nodeToPost(n);
res.add(p);
if(recurse) {
loadComments(p.getComments(), n, true);
}
}
}
return res;
}
public List<Post> findPostByContent(String content) throws RepositoryException, ForumException {
return findPostByContent(content, false);
}
public List<Comment> findCommentByAuthor(String author, boolean recurse) throws RepositoryException, ForumException {
List<Comment> res = new ArrayList<>();
Query q = fmgr.getSession().getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:unstructured] WHERE author = $author AND title IS NULL AND number IS NOT NULL", Query.JCR_SQL2);
q.bindValue("author", fmgr.getSession().getValueFactory().createValue(author));
QueryResult qr = q.execute();
NodeIterator nit = qr.getNodes();
while(nit.hasNext()) {
Node n = nit.nextNode();
if(fmgr.isComment(n)) {
Comment c = nodeToComment(n);
res.add(c);
if(recurse) {
loadComments(c.getComments(), n, true);
}
}
}
return res;
}
public List<Comment> findCommentByAuthor(String author) throws RepositoryException, ForumException {
return findCommentByAuthor(author, false);
}
public List<Comment> findCommentByContent(String content, boolean recurse) throws RepositoryException, ForumException {
List<Comment> res = new ArrayList<>();
Query q = fmgr.getSession().getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:unstructured] WHERE CONTAINS(text,$content) AND title IS NULL AND number IS NOT NULL", Query.JCR_SQL2);
q.bindValue("content", fmgr.getSession().getValueFactory().createValue(content));
QueryResult qr = q.execute();
NodeIterator nit = qr.getNodes();
while(nit.hasNext()) {
Node n = nit.nextNode();
if(fmgr.isComment(n)) {
Comment c = nodeToComment(n);
res.add(c);
if(recurse) {
loadComments(c.getComments(), n, true);
}
}
}
return res;
}
public List<Comment> findCommentByContent(String content) throws RepositoryException, ForumException {
return findCommentByContent(content, false);
}
}
Test.java:
package dk.vajhoej.forum.test;
import java.util.Map;
import java.util.ServiceLoader;
import javax.jcr.PathNotFoundException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.RepositoryFactory;
import dk.vajhoej.forum.ForumException;
import dk.vajhoej.forum.ForumManager;
import dk.vajhoej.forum.ModelMapper;
import dk.vajhoej.forum.model.Comment;
import dk.vajhoej.forum.model.Forum;
import dk.vajhoej.forum.model.Post;
import dk.vajhoej.forum.model.Topic;
public class Test {
private static Repository getRepository(Map<String, String> parameters) throws RepositoryException {
for (RepositoryFactory factory : ServiceLoader.load(RepositoryFactory.class)) {
Repository repo = factory.getRepository(parameters);
if(repo != null) {
return repo;
}
}
return null;
}
private static void printComment(String indent, Comment comment) {
System.out.println(indent + "Comment:");
System.out.println(indent + "Author = " + comment.getAuthor());
System.out.println(indent + "Time = " + comment.getTime());
System.out.println(indent + "Number = " + comment.getNumber());
System.out.println(indent + "Text = " + comment.getText());
for(Comment v : comment.getVersions()) {
System.out.println(indent + "Version = (" + v.getTime() + "," + v.getText() + ")");
}
for(Comment c : comment.getComments()) {
printComment(indent + " ", c);
}
}
private static void printPost(String indent, Post post) {
System.out.println(indent + "Post:");
System.out.println(indent + "Author = " + post.getAuthor());
System.out.println(indent + "Time = " + post.getTime());
System.out.println(indent + "Title = " + post.getTitle());
System.out.println(indent + "Body = " + post.getBody());
for(Post v : post.getVersions()) {
System.out.println(indent + "Version = (" + v.getTime() + "," + v.getBody() + ")");
}
for(Comment c : post.getComments()) {
printComment(indent + " ", c);
}
}
private static void printTopic(String indent, Topic topic) {
System.out.println(indent + "Topic:");
System.out.println(indent + "Name = " + topic.getName());
System.out.println(indent + "Description = " + topic.getDescription());
for(Post p : topic.getPosts()) {
printPost(indent + " ", p);
}
}
private static void printForum(Forum forum) {
for(Topic t : forum.getTopics()) {
printTopic("", t);
}
}
private static void testForumLoad(ForumManager fmgr) throws RepositoryException, ForumException {
ModelMapper mm = new ModelMapper(fmgr);
printForum(mm.loadForum());
}
private static void testTopicLoad(ForumManager fmgr, String path) throws PathNotFoundException, RepositoryException, ForumException {
System.out.println("Load: " + path);
ModelMapper mm = new ModelMapper(fmgr);
printTopic("", mm.loadTopic(path, true));
}
private static void testPostLoad(ForumManager fmgr, String path) throws PathNotFoundException, RepositoryException, ForumException {
System.out.println("Load: " + path);
ModelMapper mm = new ModelMapper(fmgr);
printPost("", mm.loadPost(path, true));
}
private static void testCommentLoad(ForumManager fmgr, String path) throws PathNotFoundException, RepositoryException, ForumException {
System.out.println("Load: " + path);
ModelMapper mm = new ModelMapper(fmgr);
printComment("", mm.loadComment(path, true));
}
private static void testPostAuthorSearch(ForumManager fmgr, String author) throws RepositoryException, ForumException {
System.out.println("Post author search for " + author + ":");
ModelMapper mm = new ModelMapper(fmgr);
for(Post p : mm.findPostByAuthor(author, true)) {
printPost("", p);
}
}
private static void testPostContentSearch(ForumManager fmgr, String content) throws RepositoryException, ForumException {
System.out.println("Post content search for " + content + ":");
ModelMapper mm = new ModelMapper(fmgr);
for(Post p : mm.findPostByContent(content, true)) {
printPost("", p);
}
}
private static void testCommentAuthorSearch(ForumManager fmgr, String author) throws RepositoryException, ForumException {
System.out.println("Comment author search for " + author + ":");
ModelMapper mm = new ModelMapper(fmgr);
for(Comment c : mm.findCommentByAuthor(author, true)) {
printComment("", c);
}
}
private static void testCommentContentSearch(ForumManager fmgr, String content) throws RepositoryException, ForumException {
System.out.println("Comment content search for " + content + ":");
ModelMapper mm = new ModelMapper(fmgr);
for(Comment c :mm.findCommentByContent(content, true)) {
printComment("", c);
}
}
public static void test(Map<String, String> parameters, String usr, String pwd) throws RepositoryException, ForumException, InterruptedException {
Repository repo = getRepository(parameters);
ForumManager fmgr = new ForumManager(repo, usr, pwd);
// add 2 topics
String smart = fmgr.addTopic("Smart questions", "This is for smart questions");
String dumb = fmgr.addTopic("Dumb questions", "This is for dumb questions");
// add 3 posts
String plus1 = fmgr.addPost(dumb, "What is 1+1?", "I have a math question: what is 1+1?","arne");
String plus2 = fmgr.addPost(dumb, "What is 2+2?", "I have another math question: what is 2+2?", "arne");
String life = fmgr.addPost(smart, "What is the purpose of life?", "I am in the philosophical mood. What is the purpose of life?", "arne");
// add 4 comments
String plus1a = fmgr.addCommentAutoNumber(plus1, "2", "arne");
String plus2a = fmgr.addCommentAutoNumber(plus2, "4", "arne");
String lifea1 = fmgr.addCommentAutoNumber(life, "Good question.", "arne");
String lifea2 = fmgr.addCommentAutoNumber(life, "Please tell me if you find out.", "arne");
// add 2 comments to comments to comments
fmgr.addCommentAutoNumber(plus1a, "Hmmm.", "Arne");
fmgr.addCommentAutoNumber(plus2a, "Hmmm.", "Arne");
// update 1 comment twice
Thread.sleep(1000);
fmgr.updateComment(plus1a, "Forget it.");
fmgr.updateComment(plus2a, "Forget it.");
Thread.sleep(1000);
fmgr.updateComment(plus1a, "Sorry.");
fmgr.updateComment(plus2a, "Sorry.");
// test load
testForumLoad(fmgr);
testTopicLoad(fmgr, smart);
testPostLoad(fmgr, life);
testCommentLoad(fmgr, lifea1);
testCommentLoad(fmgr, lifea2);
// test search
testPostAuthorSearch(fmgr, "xxx");
testPostAuthorSearch(fmgr, "arne");
testPostContentSearch(fmgr, "math");
testPostContentSearch(fmgr, "foobar");
testCommentAuthorSearch(fmgr, "xxx");
testCommentAuthorSearch(fmgr, "arne");
testCommentContentSearch(fmgr, "good");
testCommentContentSearch(fmgr, "foobar");
}
}
The examples use MySQL for storage and Lucene as search engine.
The exampels also have a custom login module.
Main.java:
package dk.vajhoej.forum.test;
import java.util.HashMap;
import java.util.Map;
public class Main extends Test {
public static void main(String[] args) throws Exception {
Map<String, String> local = new HashMap<>();
local.put("org.apache.jackrabbit.repository.home", "/work/jcr/jackrabbit/repo");
local.put("org.apache.jackrabbit.repository.conf", "/work/jcr/jackrabbit/repoconfig.xml");
test(local, "jcradmin", "secret");
}
}
repoconfig.xml:
<?xml version="1.0"?>
<!DOCTYPE Repository
PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 2.0//EN"
"http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
<Repository>
<FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
<param name="url" value="jdbc:mysql://localhost/jrb_repo"/>
<param name="user" value="root"/>
<param name="password" value=""/>
<param name="schema" value="mysql"/>
<param name="schemaObjectPrefix" value="repository_"/>
</FileSystem>
<DataStore class="org.apache.jackrabbit.core.data.FileDataStore"/>
<Security appName="Jackrabbit">
<SecurityManager class="org.apache.jackrabbit.core.DefaultSecurityManager" workspaceName="security">
</SecurityManager>
<AccessManager class="org.apache.jackrabbit.core.security.DefaultAccessManager">
</AccessManager>
<LoginModule class="custom.JRBLoginModule">
<param name="anonymousId" value="anonymous"/>
<param name="adminId" value="jcradmin"/>
</LoginModule>
</Security>
<Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default"/>
<Workspace name="${wsp.name}">
<FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
<param name="url" value="jdbc:mysql://localhost/jrb_repo"/>
<param name="user" value="root"/>
<param name="password" value=""/>
<param name="schema" value="mysql"/>
<param name="schemaObjectPrefix" value="${wsp.name}_"/>
</FileSystem>
<PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.BundleDbPersistenceManager">
<param name="url" value="jdbc:mysql://localhost/jrb_repo"/>
<param name="user" value="root"/>
<param name="password" value=""/>
<param name="databaseType" value="mysql"/>
<param name="schemaObjectPrefix" value="${wsp.name}_"/>
</PersistenceManager>
<SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
<param name="path" value="${wsp.home}/index"/>
</SearchIndex>
</Workspace>
<Versioning rootPath="${rep.home}/version">
<FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
<param name="url" value="jdbc:mysql://localhost/jrb_repo"/>
<param name="user" value="root"/>
<param name="password" value=""/>
<param name="schema" value="mysql"/>
<param name="schemaObjectPrefix" value="version_"/>
</FileSystem>
<PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.BundleDbPersistenceManager">
<param name="url" value="jdbc:mysql://localhost/jrb_repo"/>
<param name="user" value="root"/>
<param name="password" value=""/>
<param name="databaseType" value="mysql"/>
<param name="schemaObjectPrefix" value="version_"/>
</PersistenceManager>
</Versioning>
</Repository>
Classpath:
JRBLoginModule.java:
package custom;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.jackrabbit.core.security.authentication.CredentialsCallback;
import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
public class JRBLoginModule implements LoginModule {
private Subject subject;
private CallbackHandler callbackHandler;
private Principal principal;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
}
@Override
public boolean login() throws LoginException {
try {
CredentialsCallback ccb = new CredentialsCallback();
callbackHandler.handle(new Callback[] { ccb });
Credentials cred = ccb.getCredentials();
if(cred instanceof SimpleCredentials) {
SimpleCredentials scred = (SimpleCredentials)cred;
String usr = scred.getUserID();
String pwd = new String(scred.getPassword());
if(usr.equals("jcradmin") && pwd.equals("secret")) {
principal = new AdminPrincipal(usr);
return true;
} else {
throw new LoginException();
}
} else {
return false;
}
} catch (IOException e) {
return false;
} catch (UnsupportedCallbackException e) {
return false;
}
}
@Override
public boolean commit() throws LoginException {
subject.getPrincipals().add(principal);
return true;
}
@Override
public boolean abort() throws LoginException {
return true;
}
@Override
public boolean logout() throws LoginException {
return true;
}
}
Main.java:
package dk.vajhoej.forum.test;
import java.util.HashMap;
import java.util.Map;
public class Main extends Test {
public static void main(String[] args) throws Exception {
Map<String, String> local = new HashMap<>();
local.put("org.modeshape.jcr.URL", "file:///C:/work/jcr/modeshape/repoconfig.json");
test(local, "jcradmin", "secret");
}
}
repoconfig.json:
{
"name" : "Demo",
"storage" : {
"persistence" : {
"type" : "db",
"driver" : "com.mysql.jdbc.Driver",
"connectionUrl": "jdbc:mysql://localhost/ms_repo",
"username" : "root",
"password" : ""
}
},
"security" : {
"providers" : [
{
"classname" : "custom.MSLoginModule"
}
]
},
"indexProviders" : {
"lucene" : {
"classname" : "lucene",
"directory" : "/work/jcr/modeshape/target/indexes"
}
}
}
Classpath:
MSLoginModule.java:
package custom;
import java.util.Map;
import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.security.AuthenticationProvider;
import org.modeshape.jcr.security.SecurityContext;
public class MSLoginModule implements AuthenticationProvider {
public static class MSSecurityContext implements SecurityContext {
private String usr;
public MSSecurityContext(String usr) {
this.usr = usr;
}
@Override
public String getUserName() {
return usr;
}
@Override
public boolean hasRole(String roleName) {
return true;
}
@Override
public boolean isAnonymous() {
return false;
}
@Override
public void logout() {
}
}
@Override
public ExecutionContext authenticate(Credentials credentials, String repositoryName, String workspaceName, ExecutionContext repositoryContext, Map<String, Object> sessionAttributes) {
if(credentials instanceof SimpleCredentials) {
SimpleCredentials simpcred = (SimpleCredentials)credentials;
String usr = simpcred.getUserID();
String pwd = new String(simpcred.getPassword());
if(usr.equals("jcradmin") && pwd.equals("secret")) {
return repositoryContext.with(new MSSecurityContext(usr));
} else {
return null;
}
} else {
return null;
}
}
}
JCR is pretty widely adopted among Java CMS'es.
Similar concepts has not had the same success for other platforms.
Only major equivalent is PHP that has created the PHPCR standard with some implementations:
A sligthgly different approach is CMIS (Content Management Interoperability Services). CMIS is an OASIS standard that defines technology neutral web service API for CMS'es. The standard defines bindings: SOAP, AtomPub and a RESTFul. CMIS has gained some acceptance in both Java, .NET and PHP world.
Read more about CMIS here.
Version | Date | Description |
---|---|---|
1.0 | November 17th 2019 | Initial version |
See list of all articles here
Please send comments to Arne Vajhøj