001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.jcr.source;
018:
019: import java.io.IOException;
020: import java.net.MalformedURLException;
021: import java.util.HashMap;
022: import java.util.Map;
023:
024: import javax.jcr.LoginException;
025: import javax.jcr.Node;
026: import javax.jcr.Property;
027: import javax.jcr.Repository;
028: import javax.jcr.RepositoryException;
029: import javax.jcr.Session;
030:
031: import org.apache.avalon.framework.CascadingRuntimeException;
032: import org.apache.avalon.framework.configuration.Configurable;
033: import org.apache.avalon.framework.configuration.Configuration;
034: import org.apache.avalon.framework.configuration.ConfigurationException;
035: import org.apache.avalon.framework.service.ServiceException;
036: import org.apache.avalon.framework.service.ServiceManager;
037: import org.apache.avalon.framework.service.Serviceable;
038: import org.apache.avalon.framework.thread.ThreadSafe;
039: import org.apache.excalibur.source.Source;
040: import org.apache.excalibur.source.SourceException;
041: import org.apache.excalibur.source.SourceFactory;
042: import org.apache.excalibur.source.SourceUtil;
043:
044: /**
045: * JCRSourceFactory is an implementation of
046: * <code>ModifiableTraversableSource</code> on top of a JCR (aka <a
047: * href="http://www.jcp.org/en/jsr/detail?id=170">JSR-170</a>) repository.
048: * <p>
049: * Since JCR allows a repository to define its own node types, it is necessary
050: * to configure this source factory with a description of what node types map to
051: * "files" and "folders" and the properties used to store source-related data.
052: * <p>
053: * A typical configuration for a naked Jackrabbit repository is as follows:
054: *
055: * <pre>
056: *
057: * <source-factories>
058: * <component-instance class="org.apache.cocoon.jcr.source.JCRSourceFactory" name="jcr">
059: * <folder-node type="rep:root" new-file="nt:file" new-folder="nt:folder"/>
060: * <folder-node type="nt:folder" new-file="nt:file"/>
061: * <file-node type="nt:file" content-path="jcr:content" content-type="nt:resource"/>
062: * <file-node type="nt:linkedFile" content-ref="jcr:content"/>
063: * <content-node type="nt:resource"
064: * content-prop="jcr:data"
065: * mimetype-prop="jcr:mimeType"
066: * lastmodified-prop="jcr:lastModified"
067: * validity-prop="jcr:lastModified"/>
068: * </component-instance>
069: * </source-factories>
070: *
071: * </pre>
072: *
073: * A <code><folder-node></code> defines a node type that is mapped to a
074: * non-terminal source (i.e. that can have children). The <code>new-file</code>
075: * and <code>new-folder</code> attributes respectively define what node types
076: * should be used to create a new terminal and non-terminal source.
077: * <p>
078: * A <code><file-node></code> defines a note type that is mapped to a
079: * terminal source (i.e. that can have some content). The
080: * <code>content-path</code> attribute defines the path to the node's child
081: * that actually holds the content, and <code>content-type</code> defines the
082: * type of this content node.
083: * <p>
084: * The <code>content-ref</code> attribute is used to comply with JCR's
085: * <code>nt:linkedFile</code> definition where the content node is not a
086: * direct child of the file node, but is referenced by a property of this file
087: * node. Such node types are read-only as there's no way to indicate where the
088: * referenced content node should be created.
089: * <p>
090: * A <code><content-node></code> defines a node type that actually holds
091: * the content of a <code>file-node</code>. The <code>content-prop</code>
092: * attribute must be present and gives the name of the node's binary property
093: * that will hold the actual content. Other attributes are optional:
094: * <ul>
095: * <li><code>mimetype-prop</code> defines a string property holding the
096: * content's MIME type, </li>
097: * <li><code>lastmodified-prop</code> defines a date property holding the
098: * node's last modification date. It is automatically updated when content is
099: * written to the <code>content-node</code>. </li>
100: * <li><code>validity-prop</code> defines a property that gives the validity
101: * of the content, used by Cocoon's cache. If not specified,
102: * <code>lastmodified-prop</code> is used, if present. Otherwise the source
103: * has no validity and won't be cacheable. </li>
104: * </ul>
105: * <p>
106: * The format of URIs for this source is a path in the repository, and it is
107: * therefore currently limited to repository traversal. Further work will add
108: * the ability to specify query strings.
109: *
110: * @version $Id: JCRSourceFactory.java 449153 2006-09-23 04:27:50Z crossley $
111: */
112: public class JCRSourceFactory implements ThreadSafe, SourceFactory,
113: Configurable, Serviceable {
114:
115: protected static class NodeTypeInfo {
116: // Empty base class
117: }
118:
119: protected static class FolderTypeInfo extends NodeTypeInfo {
120: public String newFileType;
121:
122: public String newFolderType;
123: }
124:
125: protected static class FileTypeInfo extends NodeTypeInfo {
126: public String contentPath;
127:
128: public String contentType;
129:
130: public String contentRef;
131: }
132:
133: protected static class ContentTypeInfo extends NodeTypeInfo {
134: public String contentProp;
135:
136: public String mimeTypeProp;
137:
138: public String lastModifiedProp;
139:
140: public String validityProp;
141: }
142:
143: /**
144: * The repository we use
145: */
146: protected Repository repo;
147:
148: /**
149: * Scheme, lazily computed at the first call to getSource()
150: */
151: protected String scheme;
152:
153: /**
154: * The NodeTypeInfo for each of the types described in the configuration
155: */
156: protected Map typeInfos;
157:
158: protected ServiceManager manager;
159:
160: public void service(ServiceManager manager) throws ServiceException {
161: this .manager = manager;
162: // this.repo is lazily initialized to avoid a circular dependency between SourceResolver
163: // and JackrabbitRepository that leads to a StackOverflowError at initialization time
164: }
165:
166: public void configure(Configuration config)
167: throws ConfigurationException {
168: this .typeInfos = new HashMap();
169:
170: Configuration[] children = config.getChildren();
171:
172: for (int i = 0; i < children.length; i++) {
173: Configuration child = children[i];
174: String name = child.getName();
175:
176: if ("folder-node".equals(name)) {
177: FolderTypeInfo info = new FolderTypeInfo();
178: String type = child.getAttribute("type");
179: info.newFileType = child.getAttribute("new-file");
180: info.newFolderType = child.getAttribute("new-folder",
181: type);
182:
183: this .typeInfos.put(type, info);
184:
185: } else if ("file-node".equals(name)) {
186: FileTypeInfo info = new FileTypeInfo();
187: info.contentPath = child.getAttribute("content-path",
188: null);
189: info.contentType = child.getAttribute("content-type",
190: null);
191: info.contentRef = child.getAttribute("content-ref",
192: null);
193: if (info.contentPath == null && info.contentRef == null) {
194: throw new ConfigurationException(
195: "One of content-path or content-ref is required at "
196: + child.getLocation());
197: }
198: if (info.contentPath != null
199: && info.contentType == null) {
200: throw new ConfigurationException(
201: "content-type must be present with content-path at "
202: + child.getLocation());
203: }
204: this .typeInfos.put(child.getAttribute("type"), info);
205:
206: } else if ("content-node".equals(name)) {
207: ContentTypeInfo info = new ContentTypeInfo();
208: info.contentProp = child.getAttribute("content-prop");
209: info.lastModifiedProp = child.getAttribute(
210: "lastmodified-prop", null);
211: info.mimeTypeProp = child.getAttribute("mimetype-prop",
212: null);
213: info.validityProp = child.getAttribute("validity-prop",
214: info.lastModifiedProp);
215: this .typeInfos.put(child.getAttribute("type"), info);
216:
217: } else {
218: throw new ConfigurationException(
219: "Unknown configuration " + name + " at "
220: + child.getLocation());
221: }
222: }
223:
224: }
225:
226: protected void lazyInit() {
227: if (this .repo == null) {
228: try {
229: this .repo = (Repository) manager
230: .lookup(Repository.class.getName());
231: } catch (Exception e) {
232: throw new CascadingRuntimeException(
233: "Cannot lookup repository", e);
234: }
235: }
236: }
237:
238: /*
239: * (non-Javadoc)
240: *
241: * @see org.apache.excalibur.source.SourceFactory#getSource(java.lang.String,
242: * java.util.Map)
243: */
244: public Source getSource(String uri, Map parameters)
245: throws IOException, MalformedURLException {
246: lazyInit();
247:
248: if (this .scheme == null) {
249: this .scheme = SourceUtil.getScheme(uri);
250: }
251:
252: Session session;
253: try {
254: // TODO: accept a different workspace?
255: session = repo.login();
256: } catch (LoginException e) {
257: throw new SourceException("Login to repository failed", e);
258: } catch (RepositoryException e) {
259: throw new SourceException("Cannot access repository", e);
260: }
261:
262: // Compute the path
263: String path = SourceUtil.getSpecificPart(uri);
264: if (!path.startsWith("//")) {
265: throw new MalformedURLException("Expecting " + this .scheme
266: + "://path and got " + uri);
267: }
268: // Remove first '/'
269: path = path.substring(1);
270: int pathLen = path.length();
271: if (pathLen > 1) {
272: // Not root: ensure there's no trailing '/'
273: if (path.charAt(pathLen - 1) == '/') {
274: path = path.substring(0, pathLen - 1);
275: }
276: }
277:
278: return createSource(session, path);
279: }
280:
281: /*
282: * (non-Javadoc)
283: *
284: * @see org.apache.excalibur.source.SourceFactory#release(org.apache.excalibur.source.Source)
285: */
286: public void release(Source source) {
287: // nothing
288: }
289:
290: public String getScheme() {
291: return this .scheme;
292: }
293:
294: /**
295: * Get the type info for a node.
296: *
297: * @param node the node
298: * @return the type info
299: * @throws RepositoryException if node type couldn't be accessed or if no type info is found
300: */
301: public NodeTypeInfo getTypeInfo(Node node)
302: throws RepositoryException {
303: String typeName = node.getPrimaryNodeType().getName();
304: NodeTypeInfo result = (NodeTypeInfo) this .typeInfos
305: .get(typeName);
306: if (result == null) {
307: // TODO: build a NodeTypeInfo using introspection
308: throw new RepositoryException(
309: "No type info found for node type '" + typeName
310: + "' at " + node.getPath());
311: }
312:
313: return result;
314: }
315:
316: /**
317: * Get the type info for a given node type name.
318: * @param typeName the type name
319: * @return the type info
320: * @throws RepositoryException if no type info is found
321: */
322: public NodeTypeInfo getTypeInfo(String typeName)
323: throws RepositoryException {
324: NodeTypeInfo result = (NodeTypeInfo) this .typeInfos
325: .get(typeName);
326: if (result == null) {
327: // TODO: build a NodeTypeInfo using introspection
328: throw new RepositoryException(
329: "No type info found for node type '" + typeName
330: + "'");
331: }
332:
333: return result;
334: }
335:
336: /**
337: * Get the content node for a given node
338: *
339: * @param node the node for which we want the content node
340: * @return the content node
341: * @throws RepositoryException if some error occurs, or if the given node isn't a file node or a content node
342: */
343: public Node getContentNode(Node node) throws RepositoryException {
344: NodeTypeInfo info = getTypeInfo(node);
345:
346: if (info instanceof ContentTypeInfo) {
347: return node;
348:
349: } else if (info instanceof FileTypeInfo) {
350: FileTypeInfo finfo = (FileTypeInfo) info;
351: if (".".equals(finfo.contentPath)) {
352: return node;
353: } else if (finfo.contentPath != null) {
354: return node.getNode(finfo.contentPath);
355: } else {
356: Property ref = node.getProperty(finfo.contentRef);
357: return getContentNode(ref.getNode());
358: }
359: } else {
360: // A folder
361: throw new RepositoryException(
362: "Can't get content node for folder node at "
363: + node.getPath());
364: }
365: }
366:
367: /**
368: * Creates a new source given its parent and its node
369: *
370: * @param parent the parent
371: * @param node the node
372: * @return a new source
373: * @throws SourceException
374: */
375: public JCRNodeSource createSource(JCRNodeSource parent, Node node)
376: throws SourceException {
377: return new JCRNodeSource(parent, node);
378: }
379:
380: /**
381: * Creates a new source given a session and a path
382: *
383: * @param session the session
384: * @param path the absolute path
385: * @return a new source
386: * @throws SourceException
387: */
388: public JCRNodeSource createSource(Session session, String path)
389: throws SourceException {
390: return new JCRNodeSource(this , session, path);
391: }
392:
393: /**
394: * Create a child file node in a folder node.
395: *
396: * @param folderNode the folder node
397: * @param name the child's name
398: * @return the newly created child node
399: * @throws RepositoryException if some error occurs
400: */
401: public Node createFileNode(Node folderNode, String name)
402: throws RepositoryException {
403: NodeTypeInfo info = getTypeInfo(folderNode);
404: if (!(info instanceof FolderTypeInfo)) {
405: throw new RepositoryException("Node type "
406: + folderNode.getPrimaryNodeType().getName()
407: + " is not a folder type");
408: }
409:
410: FolderTypeInfo folderInfo = (FolderTypeInfo) info;
411: return folderNode.addNode(name, folderInfo.newFileType);
412: }
413:
414: /**
415: * Create the content node for a file node.
416: *
417: * @param fileNode the file node
418: * @return the content node for this file node
419: * @throws RepositoryException if some error occurs
420: */
421: public Node createContentNode(Node fileNode)
422: throws RepositoryException {
423:
424: NodeTypeInfo info = getTypeInfo(fileNode);
425: if (!(info instanceof FileTypeInfo)) {
426: throw new RepositoryException("Node type "
427: + fileNode.getPrimaryNodeType().getName()
428: + " is not a file type");
429: }
430:
431: FileTypeInfo fileInfo = (FileTypeInfo) info;
432: Node contentNode = fileNode.addNode(fileInfo.contentPath,
433: fileInfo.contentType);
434:
435: return contentNode;
436: }
437:
438: /**
439: * Get the content property for a given node
440: *
441: * @param node a file or content node
442: * @return the content property
443: * @throws RepositoryException if some error occurs
444: */
445: public Property getContentProperty(Node node)
446: throws RepositoryException {
447: Node contentNode = getContentNode(node);
448: ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode);
449: return contentNode.getProperty(info.contentProp);
450: }
451:
452: /**
453: * Get the mime-type property for a given node
454: *
455: * @param node a file or content node
456: * @return the mime-type property, or <code>null</code> if no such property exists
457: * @throws RepositoryException if some error occurs
458: */
459: public Property getMimeTypeProperty(Node node)
460: throws RepositoryException {
461: Node contentNode = getContentNode(node);
462: ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode);
463:
464: String propName = info.mimeTypeProp;
465: if (propName != null && contentNode.hasProperty(propName)) {
466: return contentNode.getProperty(propName);
467: } else {
468: return null;
469: }
470: }
471:
472: /**
473: * Get the lastmodified property for a given node
474: *
475: * @param node a file or content node
476: * @return the lastmodified property, or <code>null</code> if no such property exists
477: * @throws RepositoryException if some error occurs
478: */
479: public Property getLastModifiedDateProperty(Node node)
480: throws RepositoryException {
481: Node contentNode = getContentNode(node);
482: ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode);
483:
484: String propName = info.lastModifiedProp;
485: if (propName != null && contentNode.hasProperty(propName)) {
486: return contentNode.getProperty(propName);
487: } else {
488: return null;
489: }
490: }
491:
492: /**
493: * Get the validity property for a given node
494: *
495: * @param node a file or content node
496: * @return the validity property, or <code>null</code> if no such property exists
497: * @throws RepositoryException if some error occurs
498: */
499: public Property getValidityProperty(Node node)
500: throws RepositoryException {
501: Node contentNode = getContentNode(node);
502: ContentTypeInfo info = (ContentTypeInfo) getTypeInfo(contentNode);
503:
504: String propName = info.validityProp;
505: if (propName != null && contentNode.hasProperty(propName)) {
506: return contentNode.getProperty(propName);
507: } else {
508: return null;
509: }
510: }
511:
512: /**
513: * Does a node represent a collection (i.e. folder-node)?
514: *
515: * @param node the node
516: * @return <code>true</code> if it's a collection
517: * @throws RepositoryException if some error occurs
518: */
519: public boolean isCollection(Node node) throws RepositoryException {
520: return getTypeInfo(node) instanceof FolderTypeInfo;
521: }
522:
523: /**
524: * Get the node type to create a new subfolder of a given folder node.
525: *
526: * @param folderNode
527: * @return the child folder node type
528: * @throws RepositoryException if some error occurs
529: */
530: public String getFolderNodeType(Node folderNode)
531: throws RepositoryException {
532: FolderTypeInfo info = (FolderTypeInfo) getTypeInfo(folderNode);
533: return info.newFolderType;
534: }
535: }
|