/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. The ASF licenses this file to You
* under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
package org.apache.abdera.protocol.server.adapters.jcr;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.activation.MimeType;
import javax.jcr.Credentials;
import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.Workspace;
import org.apache.abdera.Abdera;
import org.apache.abdera.factory.Factory;
import org.apache.abdera.i18n.iri.IRI;
import org.apache.abdera.i18n.text.Sanitizer;
import org.apache.abdera.model.Content;
import org.apache.abdera.model.Element;
import org.apache.abdera.model.Person;
import org.apache.abdera.model.Text;
import org.apache.abdera.model.Content.Type;
import org.apache.abdera.protocol.server.RequestContext;
import org.apache.abdera.protocol.server.ResponseContext;
import org.apache.abdera.protocol.server.RequestContext.Scope;
import org.apache.abdera.protocol.server.context.EmptyResponseContext;
import org.apache.abdera.protocol.server.context.ResponseContextException;
import org.apache.abdera.protocol.server.impl.AbstractEntityCollectionAdapter;
import org.apache.abdera.protocol.util.PoolManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.api.JackrabbitNodeTypeManager;
/**
* Adapter implementation that uses a JCR Repository to store Atompub collection
* entries. The adapter is intended to be used with the DefaultProvider implementation.
*/
public class JcrCollectionAdapter
extends AbstractEntityCollectionAdapter<Node> {
private final static Log log = LogFactory.getLog(JcrCollectionAdapter.class);
private static final String TITLE = "title";
private static final String SUMMARY = "summary";
private static final String UPDATED = "updated";
private static final String AUTHOR = "author";
private static final String AUTHOR_EMAIL = "author.email";
private static final String AUTHOR_LANGUAGE = "author.language";
private static final String AUTHOR_NAME = "author.name";
private static final String CONTENT = "content";
private static final String SESSION_KEY = "jcrSession";
private static final String MEDIA = "media";
private static final String CONTENT_TYPE = "contentType";
private static final String NAMESPACE = "http://abdera.apache.org";
private String collectionNodePath;
private String id;
private String title;
private String author;
private String collectionNodeId;
private Repository repository;
private Credentials credentials;
private int maxActiveSessions = 100;
private PoolManager<Session> sessionPool;
public void setCollectionNodePath(String collectionNodePath) {
this.collectionNodePath = collectionNodePath;
}
public void setTitle(String title) {
this.title = title;
}
public void setAuthor(String author) {
this.author = author;
}
/**
* Logs into the repository and posts a node for the collection if one does not exist. Also,
* this will set up the session pool.
* @throws RepositoryException
*/
public void initialize() throws Exception {
Session session = repository.login(credentials);
Node collectionNode = null;
try {
collectionNode = session.getRootNode().getNode(collectionNodePath);
} catch (PathNotFoundException e) {
collectionNode = session.getRootNode().addNode(collectionNodePath);
collectionNode.addMixin("mix:referenceable");
session.save();
}
this.collectionNodeId = collectionNode.getUUID();
this.id = "urn:" + collectionNodeId;
Workspace workspace = session.getWorkspace();
// Get the NodeTypeManager from the Workspace.
// Note that it must be cast from the generic JCR NodeTypeManager to the
// Jackrabbit-specific implementation.
JackrabbitNodeTypeManager jntmgr = (JackrabbitNodeTypeManager) workspace.getNodeTypeManager();
if (!jntmgr.hasNodeType("abdera:entry")) {
InputStream in = getClass().getResourceAsStream("/org/apache/abdera/jcr/nodeTypes.xml");
try {
// register the node types and any referenced namespaces
jntmgr.registerNodeTypes(in, JackrabbitNodeTypeManager.TEXT_XML);
} finally {
in.close();
}
}
session.logout();
sessionPool = new SessionPoolManager(maxActiveSessions, repository, credentials);
}
@Override
public void start(RequestContext request) throws ResponseContextException {
try {
Session session = (Session) sessionPool.get(request);
request.setAttribute(Scope.REQUEST, SESSION_KEY, session);
} catch (Exception e) {
throw new ResponseContextException(500, e);
}
}
@Override
public void end(RequestContext request, ResponseContext response) {
// Logout of the JCR session
Session session = getSession(request);
if (session != null) {
try {
sessionPool.release(session);
} catch (Exception e) {
log.warn("Could not return Session to pool!", e);
}
}
}
@Override
public String getContentType(Node entry) {
try {
return getStringOrNull(entry, CONTENT_TYPE);
} catch (ResponseContextException e) {
throw new UnsupportedOperationException();
}
}
@Override
public boolean isMediaEntry(Node entry) throws ResponseContextException {
try {
return entry.hasProperty(MEDIA);
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
@Override
public Node postMedia(MimeType mimeType, String slug,
InputStream inputStream, RequestContext request)
throws ResponseContextException {
if (slug == null) {
throw new ResponseContextException("A slug header must be supplied.", 500);
}
Node n = postEntry(slug, null, null, new Date(), null, null, request);
try {
n.setProperty(MEDIA, inputStream);
n.setProperty(CONTENT_TYPE, mimeType.toString());
String summary = postSummaryForEntry(n);
if (summary != null) {
n.setProperty(SUMMARY, summary);
}
getSession(request).save();
return n;
} catch (RepositoryException e) {
try {
getSession(request).refresh(false);
} catch (Throwable t) {
log.warn(t);
}
throw new ResponseContextException(500, e);
}
}
/**
* post a summary for an entry. Used when a media entry is postd
* so you have the chance to post a meaningful summary for consumers
* of the feed.
*
* @param n
* @return
*/
protected String postSummaryForEntry(Node n) {
return null;
}
@Override
public Node postEntry(String title, IRI id, String summary, Date updated, List<Person> authors,
Content content, RequestContext request) throws ResponseContextException {
Session session = getSession(request);
try {
Node collectionNode = session.getNodeByUUID(collectionNodeId);
String resourceName = Sanitizer.sanitize(title, "-");
return postEntry(title, summary, updated, authors,
content, session, collectionNode,
resourceName, 0);
} catch (RepositoryException e) {
try {
session.refresh(false);
} catch (Throwable t) {
log.warn(t);
}
throw new ResponseContextException(500, e);
}
}
protected Node postEntry(String title, String summary,
Date updated, List<Person> authors,
Content content, Session session,
Node collectionNode, String resourceName, int num)
throws ResponseContextException, RepositoryException {
try {
String name = resourceName;
if (num > 0) {
name = name + "_" + num;
}
Node entry = collectionNode.addNode(name, "abdera:entry");
entry.addMixin("mix:referenceable");
mapEntryToNode(entry, title, summary, updated, authors, content, session);
session.save();
return entry;
}
catch (ItemExistsException e)
{
return postEntry(title, summary, updated, authors, content, session, collectionNode, resourceName, ++num);
}
}
protected Node mapEntryToNode(Node entry, String title, String summary, Date updated, List<Person> authors,
Content content, Session session)
throws ResponseContextException, RepositoryException {
if (title == null) {
EmptyResponseContext ctx = new EmptyResponseContext(500);
ctx.setStatusText("Entry title cannot be empty.");
throw new ResponseContextException(ctx);
}
entry.setProperty(TITLE, title);
if (summary != null) {
entry.setProperty(SUMMARY, summary);
}
Calendar upCal = Calendar.getInstance();
upCal.setTime(updated);
entry.setProperty(UPDATED, upCal);
if (authors != null) {
for (Person p : authors) {
Node addNode = entry.addNode(AUTHOR);
addNode.setProperty(AUTHOR_EMAIL, p.getEmail());
addNode.setProperty(AUTHOR_LANGUAGE, p.getLanguage());
addNode.setProperty(AUTHOR_NAME, p.getName());
}
}
if (content != null) {
switch(content.getContentType()) {
case TEXT:
entry.setProperty(CONTENT, content.getText());
entry.setProperty(CONTENT_TYPE, Type.TEXT.toString());
break;
case XHTML:
entry.setProperty(CONTENT, asString(content));
entry.setProperty(CONTENT_TYPE, Type.XHTML.toString());
break;
default:
throw new ResponseContextException("Invalid content element type.", 500);
}
}
if (summary != null) {
entry.setProperty(SUMMARY, summary);
}
return entry;
}
private String asString(Content content2) throws ResponseContextException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
content2.<Element>getFirstChild().writeTo(bos);
} catch (IOException e) {
throw new ResponseContextException(500, e);
}
return new String(bos.toByteArray());
}
protected Session getSession(RequestContext request) {
return (Session)request.getAttribute(Scope.REQUEST, SESSION_KEY);
}
@Override
public void deleteEntry(String resourceName, RequestContext request) throws ResponseContextException {
Session session = getSession(request);
try {
getNode(session, resourceName).remove();
session.save();
} catch (RepositoryException e) {
try {
session.refresh(false);
} catch (Throwable t) {
log.warn(t);
}
throw new ResponseContextException(500, e);
}
}
private Node getNode(Session session, String resourceName) throws ResponseContextException,
RepositoryException {
try {
return session.getNodeByUUID(collectionNodeId).getNode(resourceName);
} catch (PathNotFoundException e) {
throw new ResponseContextException(404);
}
}
/** Recursively outputs the contents of the given node. */
public static void dump(Node node) throws RepositoryException {
// First output the node path
System.out.println(node.getPath());
// Skip the virtual (and large!) jcr:system subtree
if (node.getName().equals("jcr:system")) {
return;
}
// Then output the properties
PropertyIterator properties = node.getProperties();
while (properties.hasNext()) {
Property property = properties.nextProperty();
if (property.getDefinition().isMultiple()) {
// A multi-valued property, print all values
Value[] values = property.getValues();
for (int i = 0; i < values.length; i++) {
System.out.println(
property.getPath() + " = " + values[i].getString());
}
} else {
// A single-valued property
System.out.println(
property.getPath() + " = " + property.getString());
}
}
// Finally output all the child nodes recursively
NodeIterator nodes = node.getNodes();
while (nodes.hasNext()) {
dump(nodes.nextNode());
}
}
@Override
public String getAuthor(RequestContext request) throws ResponseContextException {
return author;
}
@Override
public List<Person> getAuthors(Node entry, RequestContext request) throws ResponseContextException {
try {
ArrayList<Person> authors = new ArrayList<Person>();
for (NodeIterator nodes = entry.getNodes(); nodes.hasNext();) {
Node n = nodes.nextNode();
if (n.getName().equals(AUTHOR)) {
Person author = request.getAbdera().getFactory().newAuthor();
author.setName(getStringOrNull(entry, AUTHOR_NAME));
author.setEmail(getStringOrNull(entry, AUTHOR_EMAIL));
author.setLanguage(getStringOrNull(entry, AUTHOR_LANGUAGE));
authors.add(author);
}
}
return authors;
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
@Override
public Object getContent(Node entry, RequestContext request) throws ResponseContextException {
String typeStr = getStringOrNull(entry, CONTENT_TYPE);
Factory factory = Abdera.getInstance().getFactory();
String textContent = getStringOrNull(entry, CONTENT);
Type type = Type.valueOf(typeStr);
Content content = factory.newContent(type);
switch (type) {
case TEXT:
content.setValue(textContent);
return content;
case XHTML:
content.setWrappedValue(textContent);
return content;
default:
}
return null;
}
@Override
public Iterable<Node> getEntries(RequestContext request) throws ResponseContextException {
ArrayList<Node> entries = new ArrayList<Node>();
Session session = getSession(request);
try {
Node n = session.getNodeByUUID(collectionNodeId);
for (NodeIterator nodes = n.getNodes(); nodes.hasNext();) {
entries.add(nodes.nextNode());
}
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
return entries;
}
@Override
public Node getEntry(String resourceName, RequestContext request) throws ResponseContextException {
try {
return getNode(getSession(request), resourceName);
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
@Override
public String getId(RequestContext request) {
return id;
}
@Override
public String getId(Node entry) throws ResponseContextException {
try {
return "urn:" + entry.getUUID();
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
public String getMediaName(Node entry) throws ResponseContextException {
return getName(entry);
}
public InputStream getMediaStream(Node entry) throws ResponseContextException {
try {
Value value = getValueOrNull(entry, MEDIA);
return (value != null) ? value.getStream() : null;
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
@Override
public String getName(Node entry) throws ResponseContextException {
try {
return entry.getName();
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
@Override
public Text getSummary(Node entry, RequestContext request) throws ResponseContextException {
Text summary = request.getAbdera().getFactory().newSummary();
summary.setText(getStringOrNull(entry, SUMMARY));
return summary;
}
public String getTitle(RequestContext request) {
return title;
}
@Override
public String getTitle(Node entry) throws ResponseContextException {
return getStringOrNull(entry, TITLE);
}
@Override
public Date getUpdated(Node entry) throws ResponseContextException {
Calendar updated = getDateOrNull(entry, UPDATED);
return (updated != null) ? updated.getTime() : null;
}
@Override
public void putEntry(Node entry, String title, Date updated,
List<Person> authors, String summary,
Content content, RequestContext request) throws ResponseContextException {
Session session = getSession(request);
try {
mapEntryToNode(entry, title, summary, updated, authors, content, session);
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
public static String getStringOrNull(Node node, String propName) throws ResponseContextException {
try {
Value v = getValueOrNull(node, propName);
return (v != null) ? v.getString() : null;
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
public ResponseContext getCategories(RequestContext request) {
return null;
}
public static Calendar getDateOrNull(Node node, String propName) throws ResponseContextException {
try {
Value v = getValueOrNull(node, propName);
return (v != null) ? v.getDate() : null;
} catch (RepositoryException e) {
throw new ResponseContextException(500, e);
}
}
public static Value getValueOrNull(Node node, String propName) throws RepositoryException {
return node.hasProperty(propName)
? node.getProperty(propName).getValue() : null;
}
public void setRepository(Repository repository) {
this.repository = repository;
}
public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}
public void setMaxActiveSessions(int maxActiveSessions) {
this.maxActiveSessions = maxActiveSessions;
}
}