| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * 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. |
| */ |
| package org.apache.cocoon.jcr.source; |
| |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import javax.jcr.LoginException; |
| import javax.jcr.Node; |
| import javax.jcr.Property; |
| import javax.jcr.Repository; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| |
| import org.apache.avalon.framework.CascadingRuntimeException; |
| import org.apache.avalon.framework.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.avalon.framework.thread.ThreadSafe; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceFactory; |
| import org.apache.excalibur.source.SourceUtil; |
| |
| /** |
| * JCRSourceFactory is an implementation of |
| * <code>ModifiableTraversableSource</code> on top of a JCR (aka <a |
| * href="http://www.jcp.org/en/jsr/detail?id=170">JSR-170</a>) repository. |
| * <p> |
| * Since JCR allows a repository to define its own node types, it is necessary |
| * to configure this source factory with a description of what node types map to |
| * "files" and "folders" and the properties used to store source-related data. |
| * <p> |
| * A typical configuration for a naked Jackrabbit repository is as follows: |
| * |
| * <pre> |
| * |
| * <source-factories> |
| * <component-instance class="org.apache.cocoon.jcr.source.JCRSourceFactory" name="jcr"> |
| * <folder-node type="rep:root" new-file="nt:file" new-folder="nt:folder"/> |
| * <folder-node type="nt:folder" new-file="nt:file"/> |
| * <file-node type="nt:file" content-path="jcr:content" content-type="nt:resource"/> |
| * <file-node type="nt:linkedFile" content-ref="jcr:content"/> |
| * <content-node type="nt:resource" |
| * content-prop="jcr:data" |
| * mimetype-prop="jcr:mimeType" |
| * lastmodified-prop="jcr:lastModified" |
| * validity-prop="jcr:lastModified"/> |
| * </component-instance> |
| * </source-factories> |
| * |
| * </pre> |
| * |
| * A <code><folder-node></code> defines a node type that is mapped to a |
| * non-terminal source (i.e. that can have children). The <code>new-file</code> |
| * and <code>new-folder</code> attributes respectively define what node types |
| * should be used to create a new terminal and non-terminal source. |
| * <p> |
| * A <code><file-node></code> defines a note type that is mapped to a |
| * terminal source (i.e. that can have some content). The |
| * <code>content-path</code> attribute defines the path to the node's child |
| * that actually holds the content, and <code>content-type</code> defines the |
| * type of this content node. |
| * <p> |
| * The <code>content-ref</code> attribute is used to comply with JCR's |
| * <code>nt:linkedFile</code> definition where the content node is not a |
| * direct child of the file node, but is referenced by a property of this file |
| * node. Such node types are read-only as there's no way to indicate where the |
| * referenced content node should be created. |
| * <p> |
| * A <code><content-node></code> defines a node type that actually holds |
| * the content of a <code>file-node</code>. The <code>content-prop</code> |
| * attribute must be present and gives the name of the node's binary property |
| * that will hold the actual content. Other attributes are optional: |
| * <ul> |
| * <li><code>mimetype-prop</code> defines a string property holding the |
| * content's MIME type, </li> |
| * <li><code>lastmodified-prop</code> defines a date property holding the |
| * node's last modification date. It is automatically updated when content is |
| * written to the <code>content-node</code>. </li> |
| * <li><code>validity-prop</code> defines a property that gives the validity |
| * of the content, used by Cocoon's cache. If not specified, |
| * <code>lastmodified-prop</code> is used, if present. Otherwise the source |
| * has no validity and won't be cacheable. </li> |
| * </ul> |
| * <p> |
| * The format of URIs for this source is a path in the repository, and it is |
| * therefore currently limited to repository traversal. Further work will add |
| * the ability to specify query strings. |
| * |
| * @version $Id$ |
| */ |
| public class JCRSourceFactory implements ThreadSafe, SourceFactory, Configurable, Serviceable { |
| |
| protected static class NodeTypeInfo { |
| // Empty base class |
| } |
| |
| protected static class FolderTypeInfo extends NodeTypeInfo { |
| public String newFileType; |
| |
| public String newFolderType; |
| } |
| |
| protected static class FileTypeInfo extends NodeTypeInfo { |
| public String contentPath; |
| |
| public String contentType; |
| |
| public String contentRef; |
| } |
| |
| protected static class ContentTypeInfo extends NodeTypeInfo { |
| public String contentProp; |
| |
| public String mimeTypeProp; |
| |
| public String lastModifiedProp; |
| |
| public String validityProp; |
| } |
| |
| /** |
| * The repository we use |
| */ |
| protected Repository repo; |
| |
| /** |
| * Scheme, lazily computed at the first call to getSource() |
| */ |
| protected String scheme; |
| |
| /** |
| * The NodeTypeInfo for each of the types described in the configuration |
| */ |
| protected Map typeInfos; |
| |
| protected ServiceManager manager; |
| |
| public void service(ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| // this.repo is lazily initialized to avoid a circular dependency between SourceResolver |
| // and JackrabbitRepository that leads to a StackOverflowError at initialization time |
| } |
| |
| public void configure(Configuration config) throws ConfigurationException { |
| this.typeInfos = new HashMap(); |
| |
| Configuration[] children = config.getChildren(); |
| |
| for (int i = 0; i < children.length; i++) { |
| Configuration child = children[i]; |
| String name = child.getName(); |
| |
| if ("folder-node".equals(name)) { |
| FolderTypeInfo info = new FolderTypeInfo(); |
| String type = child.getAttribute("type"); |
| info.newFileType = child.getAttribute("new-file"); |
| info.newFolderType = child.getAttribute("new-folder", type); |
| |
| this.typeInfos.put(type, info); |
| |
| } else if ("file-node".equals(name)) { |
| FileTypeInfo info = new FileTypeInfo(); |
| info.contentPath = child.getAttribute("content-path", null); |
| info.contentType = child.getAttribute("content-type", null); |
| info.contentRef = child.getAttribute("content-ref", null); |
| if (info.contentPath == null && info.contentRef == null) { |
| throw new ConfigurationException("One of content-path or content-ref is required at " + child.getLocation()); |
| } |
| if (info.contentPath != null && info.contentType == null) { |
| throw new ConfigurationException("content-type must be present with content-path at " + child.getLocation()); |
| } |
| this.typeInfos.put(child.getAttribute("type"), info); |
| |
| } else if ("content-node".equals(name)) { |
| ContentTypeInfo info = new ContentTypeInfo(); |
| info.contentProp = child.getAttribute("content-prop"); |
| info.lastModifiedProp = child.getAttribute("lastmodified-prop", null); |
| info.mimeTypeProp = child.getAttribute("mimetype-prop", null); |
| info.validityProp = child.getAttribute("validity-prop", info.lastModifiedProp); |
| this.typeInfos.put(child.getAttribute("type"), info); |
| |
| } else { |
| throw new ConfigurationException("Unknown configuration " + name + " at " + child.getLocation()); |
| } |
| } |
| |
| } |
| |
| protected void lazyInit() { |
| if (this.repo == null) { |
| try { |
| this.repo = (Repository)manager.lookup(Repository.class.getName()); |
| } catch (Exception e) { |
| throw new CascadingRuntimeException("Cannot lookup repository", e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.apache.excalibur.source.SourceFactory#getSource(java.lang.String, |
| * java.util.Map) |
| */ |
| public Source getSource(String uri, Map parameters) throws IOException, MalformedURLException { |
| lazyInit(); |
| |
| if (this.scheme == null) { |
| this.scheme = SourceUtil.getScheme(uri); |
| } |
| |
| Session session; |
| try { |
| // TODO: accept a different workspace? |
| session = repo.login(); |
| } catch (LoginException e) { |
| throw new SourceException("Login to repository failed", e); |
| } catch (RepositoryException e) { |
| throw new SourceException("Cannot access repository", e); |
| } |
| |
| // Compute the path |
| String path = SourceUtil.getSpecificPart(uri); |
| if (!path.startsWith("//")) { |
| throw new MalformedURLException("Expecting " + this.scheme + "://path and got " + uri); |
| } |
| // Remove first '/' |
| path = path.substring(1); |
| int pathLen = path.length(); |
| if (pathLen > 1) { |
| // Not root: ensure there's no trailing '/' |
| if (path.charAt(pathLen - 1) == '/') { |
| path = path.substring(0, pathLen - 1); |
| } |
| } |
| |
| return createSource(session, path); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.apache.excalibur.source.SourceFactory#release(org.apache.excalibur.source.Source) |
| */ |
| public void release(Source source) { |
| // nothing |
| } |
| |
| public String getScheme() { |
| return this.scheme; |
| } |
| |
| /** |
| * Get the type info for a node. |
| * |
| * @param node the node |
| * @return the type info |
| * @throws RepositoryException if node type couldn't be accessed or if no type info is found |
| */ |
| public NodeTypeInfo getTypeInfo(Node node) throws RepositoryException { |
| String typeName = node.getPrimaryNodeType().getName(); |
| NodeTypeInfo result = (NodeTypeInfo) this.typeInfos.get(typeName); |
| if (result == null) { |
| // TODO: build a NodeTypeInfo using introspection |
| throw new RepositoryException("No type info found for node type '" + typeName + "' at " + node.getPath()); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Get the type info for a given node type name. |
| * @param typeName the type name |
| * @return the type info |
| * @throws RepositoryException if no type info is found |
| */ |
| public NodeTypeInfo getTypeInfo(String typeName) throws RepositoryException { |
| NodeTypeInfo result = (NodeTypeInfo) this.typeInfos.get(typeName); |
| if (result == null) { |
| // TODO: build a NodeTypeInfo using introspection |
| throw new RepositoryException("No type info found for node type '" + typeName + "'"); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Get the content node for a given node |
| * |
| * @param node the node for which we want the content node |
| * @return the content node |
| * @throws RepositoryException if some error occurs, or if the given node isn't a file node or a content node |
| */ |
| public Node getContentNode(Node node) throws RepositoryException { |
| NodeTypeInfo info = getTypeInfo(node); |
| |
| if (info instanceof ContentTypeInfo) { |
| return node; |
| |
| } else if (info instanceof FileTypeInfo) { |
| FileTypeInfo finfo = (FileTypeInfo) info; |
| if (".".equals(finfo.contentPath)) { |
| return node; |
| } else if (finfo.contentPath != null) { |
| return node.getNode(finfo.contentPath); |
| } else { |
| Property ref = node.getProperty(finfo.contentRef); |
| return getContentNode(ref.getNode()); |
| } |
| } else { |
| // A folder |
| throw new RepositoryException("Can't get content node for folder node at " + node.getPath()); |
| } |
| } |
| |
| /** |
| * Creates a new source given its parent and its node |
| * |
| * @param parent the parent |
| * @param node the node |
| * @return a new source |
| * @throws SourceException |
| */ |
| public JCRNodeSource createSource(JCRNodeSource parent, Node node) throws SourceException { |
| return new JCRNodeSource(parent, node); |
| } |
| |
| /** |
| * Creates a new source given a session and a path |
| * |
| * @param session the session |
| * @param path the absolute path |
| * @return a new source |
| * @throws SourceException |
| */ |
| public JCRNodeSource createSource(Session session, String path) throws SourceException { |
| return new JCRNodeSource(this, session, path); |
| } |
| |
| /** |
| * Create a child file node in a folder node. |
| * |
| * @param folderNode the folder node |
| * @param name the child's name |
| * @return the newly created child node |
| * @throws RepositoryException if some error occurs |
| */ |
| public Node createFileNode(Node folderNode, String name) throws RepositoryException { |
| NodeTypeInfo info = getTypeInfo(folderNode); |
| if (!(info instanceof FolderTypeInfo)) { |
| throw new RepositoryException("Node type " + folderNode.getPrimaryNodeType().getName() + " is not a folder type"); |
| } |
| |
| FolderTypeInfo folderInfo = (FolderTypeInfo) info; |
| return folderNode.addNode(name, folderInfo.newFileType); |
| } |
| |
| /** |
| * Create the content node for a file node. |
| * |
| * @param fileNode the file node |
| * @return the content node for this file node |
| * @throws RepositoryException if some error occurs |
| */ |
| public Node createContentNode(Node fileNode) throws RepositoryException { |
| |
| NodeTypeInfo info = getTypeInfo(fileNode); |
| if (!(info instanceof FileTypeInfo)) { |
| throw new RepositoryException("Node type " + fileNode.getPrimaryNodeType().getName() + " is not a file type"); |
| } |
| |
| FileTypeInfo fileInfo = (FileTypeInfo) info; |
| Node contentNode = fileNode.addNode(fileInfo.contentPath, fileInfo.contentType); |
| |
| return contentNode; |
| } |
| |
| /** |
| * Get the content property for a given node |
| * |
| * @param node a file or content node |
| * @return the content property |
| * @throws RepositoryException if some error occurs |
| */ |
| public Property getContentProperty(Node node) throws RepositoryException { |
| Node contentNode = getContentNode(node); |
| ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode); |
| return contentNode.getProperty(info.contentProp); |
| } |
| |
| /** |
| * Get the mime-type property for a given node |
| * |
| * @param node a file or content node |
| * @return the mime-type property, or <code>null</code> if no such property exists |
| * @throws RepositoryException if some error occurs |
| */ |
| public Property getMimeTypeProperty(Node node) throws RepositoryException { |
| Node contentNode = getContentNode(node); |
| ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode); |
| |
| String propName = info.mimeTypeProp; |
| if (propName != null && contentNode.hasProperty(propName)) { |
| return contentNode.getProperty(propName); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Get the lastmodified property for a given node |
| * |
| * @param node a file or content node |
| * @return the lastmodified property, or <code>null</code> if no such property exists |
| * @throws RepositoryException if some error occurs |
| */ |
| public Property getLastModifiedDateProperty(Node node) throws RepositoryException { |
| Node contentNode = getContentNode(node); |
| ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode); |
| |
| String propName = info.lastModifiedProp; |
| if (propName != null && contentNode.hasProperty(propName)) { |
| return contentNode.getProperty(propName); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Get the validity property for a given node |
| * |
| * @param node a file or content node |
| * @return the validity property, or <code>null</code> if no such property exists |
| * @throws RepositoryException if some error occurs |
| */ |
| public Property getValidityProperty(Node node) throws RepositoryException { |
| Node contentNode = getContentNode(node); |
| ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode); |
| |
| String propName = info.validityProp; |
| if (propName != null && contentNode.hasProperty(propName)) { |
| return contentNode.getProperty(propName); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Does a node represent a collection (i.e. folder-node)? |
| * |
| * @param node the node |
| * @return <code>true</code> if it's a collection |
| * @throws RepositoryException if some error occurs |
| */ |
| public boolean isCollection(Node node) throws RepositoryException { |
| return getTypeInfo(node) instanceof FolderTypeInfo; |
| } |
| |
| /** |
| * Get the node type to create a new subfolder of a given folder node. |
| * |
| * @param folderNode |
| * @return the child folder node type |
| * @throws RepositoryException if some error occurs |
| */ |
| public String getFolderNodeType(Node folderNode) throws RepositoryException { |
| FolderTypeInfo info = (FolderTypeInfo) getTypeInfo(folderNode); |
| return info.newFolderType; |
| } |
| } |