package org.springmodules.jcr; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.jcr.Item; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFactory; import javax.jcr.query.Query; import javax.jcr.query.QueryManager; import javax.jcr.query.QueryResult; import org.springframework.core.CollectionFactory; import org.springframework.dao.DataAccessException; import org.xml.sax.ContentHandler; /** * Helper class that simplifies JCR data access code. * * Typically used to implement data access or business logic services that use * JCR within their implementation but are JCR-agnostic in their interface. * * Requires a {@link JcrSessionFactory} to provide access to a JCR repository. A * workspace name is optional, as the repository will choose the default * workspace if a name is not provided. * * @author Costin Leau * */ public class JcrTemplate extends JcrAccessor implements JcrOperations { private boolean allowCreate = false; private boolean exposeNativeSession = false; /** */ public JcrTemplate() { } /** */ public JcrTemplate(SessionFactory sessionFactory) { setSessionFactory(sessionFactory); afterPropertiesSet(); } // InitializingBean methods /** * @see org.springmodules.jcr.JcrOperations#execute(org.springmodules.jcr.JcrCallback, * boolean) */ public Object execute(JcrCallback action, boolean exposeNativeSession) throws DataAccessException { Session session = getSession(); boolean existingTransaction = SessionFactoryUtils.isSessionThreadBound(session, getSessionFactory()); if (existingTransaction) { logger.debug("Found thread-bound Session for JcrTemplate"); } try { Session sessionToExpose = (exposeNativeSession ? session : createSessionProxy(session)); Object result = action.doInJcr(sessionToExpose); // TODO: does flushing (session.refresh) should work here? // flushIfNecessary(session, existingTransaction); return result; } catch (RepositoryException ex) { throw convertJcrAccessException(ex); // IOException are not converted here } catch (IOException ex) { // use method to decouple the static call throw convertJcrAccessException(ex); } catch (RuntimeException ex) { // Callback code threw application exception... throw convertJcrAccessException(ex); } finally { if (existingTransaction) { logger.debug("Not closing pre-bound Jcr Session after JcrTemplate"); } else { SessionFactoryUtils.releaseSession(session, getSessionFactory()); } } } /** * @see org.springmodules.jcr.JcrOperations#execute(org.springmodules.jcr.JcrCallback) */ public Object execute(JcrCallback callback) throws DataAccessException { return execute(callback, isExposeNativeSession()); } /** * Return a Session for use by this template. A pre-bound Session in case of * "allowCreate" turned off and a pre-bound or new Session else (new only if * no transactional or otherwise pre-bound Session exists). * * @see SessionFactoryUtils#getSession * @see SessionFactoryUtils#getNewSession * @see #setAllowCreate */ protected Session getSession() { return SessionFactoryUtils.getSession(getSessionFactory(), allowCreate); } // ------------------------------------------------------------------------- // Convenience methods for loading individual objects // ------------------------------------------------------------------------- /** * @see org.springmodules.jcr.JcrOperations#addLockToken(java.lang.String) */ public void addLockToken(final String lock) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.addLockToken(lock); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getAttribute(java.lang.String) */ public Object getAttribute(final String name) { return execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getAttribute(name); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getAttributeNames() */ public String[] getAttributeNames() { return (String[]) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getAttributeNames(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getImportContentHandler(java.lang.String, * int) */ public ContentHandler getImportContentHandler(final String parentAbsPath, final int uuidBehavior) { return (ContentHandler) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getImportContentHandler(parentAbsPath, uuidBehavior); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getItem(java.lang.String) */ public Item getItem(final String absPath) { return (Item) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getItem(absPath); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getLockTokens() */ public String[] getLockTokens() { return (String[]) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getLockTokens(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getNamespacePrefix(java.lang.String) */ public String getNamespacePrefix(final String uri) { return (String) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getNamespacePrefix(uri); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getNamespacePrefixes() */ public String[] getNamespacePrefixes() { return (String[]) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getNamespacePrefixes(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getNamespaceURI(java.lang.String) */ public String getNamespaceURI(final String prefix) { return (String) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getNamespaceURI(prefix); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getNodeByUUID(java.lang.String) */ public Node getNodeByUUID(final String uuid) { return (Node) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getNodeByUUID(uuid); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getRootNode() */ public Node getRootNode() { return (Node) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getRootNode(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getUserID() */ public String getUserID() { return (String) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getUserID(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#getValueFactory() */ public ValueFactory getValueFactory() { return (ValueFactory) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return session.getValueFactory(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#hasPendingChanges() */ public boolean hasPendingChanges() { return ((Boolean) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return new Boolean(session.hasPendingChanges()); } }, true)).booleanValue(); } /** * @see org.springmodules.jcr.JcrOperations#importXML(java.lang.String, * java.io.InputStream, int) */ public void importXML(final String parentAbsPath, final InputStream in, final int uuidBehavior) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { try { session.importXML(parentAbsPath, in, uuidBehavior); } catch (IOException e) { throw new JcrSystemException(e); } return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#refresh(boolean) */ public void refresh(final boolean keepChanges) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.refresh(keepChanges); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#removeLockToken(java.lang.String) */ public void removeLockToken(final String lt) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.removeLockToken(lt); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#rename(javax.jcr.Node, java.lang.String) */ public void rename(final Node node, final String newName) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.move(node.getPath(), node.getParent().getPath() + "/" + newName); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#setNamespacePrefix(java.lang.String, * java.lang.String) */ public void setNamespacePrefix(final String prefix, final String uri) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.setNamespacePrefix(prefix, uri); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#isLive() */ public boolean isLive() { return ((Boolean) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return new Boolean(session.isLive()); } }, true)).booleanValue(); } /** * @see org.springmodules.jcr.JcrOperations#itemExists(java.lang.String) */ public boolean itemExists(final String absPath) { return ((Boolean) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { return new Boolean(session.itemExists(absPath)); } }, true)).booleanValue(); } /** * @see org.springmodules.jcr.JcrOperations#move(java.lang.String, * java.lang.String) */ public void move(final String srcAbsPath, final String destAbsPath) { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.move(srcAbsPath, destAbsPath); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#save() */ public void save() { execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { session.save(); return null; } }, true); } /** * @see org.springmodules.jcr.JcrOperations#dump(javax.jcr.Node) */ public String dump(final Node node) { return (String) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { Node nd = node; if (nd == null) nd = session.getRootNode(); return dumpNode(nd); } }, true); } /** * Recursive method for dumping a node. This method is separate to avoid the * overhead of searching and opening/closing JCR sessions. * * @param node * @return * @throws RepositoryException */ protected String dumpNode(Node node) throws RepositoryException { StringBuffer buffer = new StringBuffer(); buffer.append(node.getPath()); PropertyIterator properties = node.getProperties(); while (properties.hasNext()) { Property property = properties.nextProperty(); buffer.append(property.getPath() + "="); if (property.getDefinition().isMultiple()) { Value[] values = property.getValues(); for (int i = 0; i < values.length; i++) { if (i > 0) { buffer.append(","); } buffer.append(values[i].getString()); } } else { buffer.append(property.getString()); } buffer.append("\n"); } NodeIterator nodes = node.getNodes(); while (nodes.hasNext()) { Node child = nodes.nextNode(); buffer.append(dumpNode(child)); } return buffer.toString(); } /** * @see org.springmodules.jcr.JcrOperations#query(javax.jcr.Node) */ public QueryResult query(final Node node) { if (node == null) throw new IllegalArgumentException("node can't be null"); return (QueryResult) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { boolean debug = logger.isDebugEnabled(); // get query manager QueryManager manager = session.getWorkspace().getQueryManager(); if (debug) logger.debug("retrieved manager " + manager); Query query = manager.getQuery(node); if (debug) logger.debug("created query " + query); return query.execute(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#query(java.lang.String) */ public QueryResult query(final String statement) { return query(statement, null); } /** * @see org.springmodules.jcr.JcrOperations#query(java.lang.String, * java.lang.String) */ public QueryResult query(final String statement, final String language) { if (statement == null) throw new IllegalArgumentException("statement can't be null"); return (QueryResult) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { // check language String lang = language; if (lang == null) lang = Query.XPATH; boolean debug = logger.isDebugEnabled(); // get query manager QueryManager manager = session.getWorkspace().getQueryManager(); if (debug) logger.debug("retrieved manager " + manager); Query query = manager.createQuery(statement, lang); if (debug) logger.debug("created query " + query); return query.execute(); } }, true); } /** * @see org.springmodules.jcr.JcrOperations#query(java.util.List) */ public Map query(final List list) { return query(list, null, false); } /** * @see org.springmodules.jcr.JcrOperations#query(java.util.List, java.lang.String, boolean) */ public Map query(final List list, final String language, final boolean ignoreErrors) { if (list == null) throw new IllegalArgumentException("list can't be null"); return (Map) execute(new JcrCallback() { /** * @see org.springmodules.jcr.JcrCallback#doInJcr(javax.jcr.Session) */ public Object doInJcr(Session session) throws RepositoryException { // check language String lang = language; if (lang == null) lang = Query.XPATH; boolean debug = logger.isDebugEnabled(); Map map = CollectionFactory.createLinkedMapIfPossible(list.size()); // get query manager QueryManager manager = session.getWorkspace().getQueryManager(); if (debug) logger.debug("retrieved manager " + manager); for (Iterator iter = list.iterator(); iter.hasNext();) { String statement = (String) iter.next(); Query query = manager.createQuery(statement, lang); if (debug) logger.debug("created query " + query); QueryResult result; try { result = query.execute(); map.put(statement, result); } catch (RepositoryException e) { if (ignoreErrors) map.put(statement, null); else throw convertJcrAccessException(e); } } return map; } }, true); } /** * @return Returns the allowCreate. */ public boolean isAllowCreate() { return allowCreate; } /** * @param allowCreate The allowCreate to set. */ public void setAllowCreate(boolean allowCreate) { this.allowCreate = allowCreate; } /** * Create a close-suppressing proxy for the given Jcr Session. * * @param session the Jcr Session to create a proxy for * @return the Session proxy * @see javax.jcr.Session#logout() */ protected Session createSessionProxy(Session session) { return (Session) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { Session.class }, new LogoutSuppressingInvocationHandler(session)); } /** * Invocation handler that suppresses logout calls on JCR Session. * * @see javax.jcr.Sesion#logout */ private class LogoutSuppressingInvocationHandler implements InvocationHandler { private final Session target; public LogoutSuppressingInvocationHandler(Session target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on Session interface (or vendor-specific // extension) coming in... if (method.getName().equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); } else if (method.getName().equals("hashCode")) { // Use hashCode of session proxy. return new Integer(hashCode()); } else if (method.getName().equals("logout")) { // Handle close method: suppress, not valid. return null; } // Invoke method on target Session. try { Object retVal = method.invoke(this.target, args); // TODO: watch out for Query returned /* * if (retVal instanceof Query) { prepareQuery(((Query) * retVal)); } */ return retVal; } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } protected boolean isVersionable(Node node) throws RepositoryException { return node.isNodeType("mix:versionable"); } /** * @return Returns the exposeNativeSession. */ public boolean isExposeNativeSession() { return exposeNativeSession; } /** * @param exposeNativeSession The exposeNativeSession to set. */ public void setExposeNativeSession(boolean exposeNativeSession) { this.exposeNativeSession = exposeNativeSession; } }