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.components.source.impl;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.ByteArrayOutputStream;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.io.OutputStream;
024: import java.net.MalformedURLException;
025: import java.util.ArrayList;
026:
027: import javax.xml.transform.TransformerFactory;
028: import javax.xml.transform.sax.SAXTransformerFactory;
029: import javax.xml.transform.sax.TransformerHandler;
030: import javax.xml.transform.stream.StreamResult;
031:
032: import org.apache.avalon.framework.logger.AbstractLogEnabled;
033: import org.apache.avalon.framework.logger.Logger;
034: import org.apache.cocoon.CascadingIOException;
035: import org.apache.cocoon.xml.IncludeXMLConsumer;
036: import org.apache.excalibur.source.ModifiableTraversableSource;
037: import org.apache.excalibur.source.Source;
038: import org.apache.excalibur.source.SourceException;
039: import org.apache.excalibur.source.SourceNotFoundException;
040: import org.apache.excalibur.source.SourceUtil;
041: import org.apache.excalibur.source.SourceValidity;
042: import org.apache.excalibur.xml.sax.XMLizable;
043: import org.xml.sax.ContentHandler;
044: import org.xml.sax.SAXException;
045: import org.xml.sax.helpers.AttributesImpl;
046: import org.xmldb.api.DatabaseManager;
047: import org.xmldb.api.base.Collection;
048: import org.xmldb.api.base.Resource;
049: import org.xmldb.api.base.ResourceIterator;
050: import org.xmldb.api.base.ResourceSet;
051: import org.xmldb.api.base.XMLDBException;
052: import org.xmldb.api.modules.BinaryResource;
053: import org.xmldb.api.modules.CollectionManagementService;
054: import org.xmldb.api.modules.XMLResource;
055: import org.xmldb.api.modules.XPathQueryService;
056:
057: /**
058: * This class implements the xmldb:// pseudo-protocol and allows to get XML
059: * content from an XML:DB enabled XML database.
060: *
061: * @version CVS $Id: XMLDBSource.java 579454 2007-09-26 03:48:46Z vgritsenko $
062: */
063: public class XMLDBSource extends AbstractLogEnabled implements
064: ModifiableTraversableSource, XMLizable {
065:
066: private static final int ST_UNKNOWN = 0;
067: private static final int ST_COLLECTION = 1;
068: private static final int ST_RESOURCE = 2;
069: private static final int ST_NO_PARENT = 3;
070: private static final int ST_NO_RESOURCE = 4;
071:
072: //
073: // Static Strings used for XML Collection representation
074: //
075:
076: /** Source namespace */
077: public static final String URI = "http://apache.org/cocoon/xmldb/1.0";
078:
079: /** Source prefix */
080: public static final String PREFIX = "db";
081:
082: /** Root element <code><collections></code> */
083: protected static final String COLLECTIONS = "collections";
084: /** Root element <code><xmldb:collections></code> (raw name) */
085: protected static final String QCOLLECTIONS = PREFIX + ":"
086: + COLLECTIONS;
087: /** Attribute <code>resources</code> on the root element indicates count of resources in the collection */
088: protected static final String RESOURCE_COUNT_ATTR = "resources";
089: /** Attribute <code>collections</code> on the root element indicates count of collections in the collection */
090: protected static final String COLLECTION_COUNT_ATTR = "collections";
091: protected static final String COLLECTION_BASE_ATTR = "base";
092:
093: /** Element <code><collection></code> */
094: protected static final String COLLECTION = "collection";
095: /** Element <code><xmldb:collection></code> (raw name) */
096: protected static final String QCOLLECTION = PREFIX + ":"
097: + COLLECTION;
098:
099: /** Element <code><resource></code> */
100: protected static final String RESOURCE = "resource";
101: /** Element <code><resource></code> (raw name) */
102: protected static final String QRESOURCE = PREFIX + ":" + RESOURCE;
103: /** Attribute <code>name</code> on the collection/resource element */
104: protected static final String NAME_ATTR = "name";
105:
106: /** Root element <code><results></code> */
107: protected static final String RESULTSET = "results";
108: /** Root element <code><xmldb:results></code> (raw name) */
109: protected static final String QRESULTSET = PREFIX + ":" + RESULTSET;
110: protected static final String QUERY_ATTR = "query";
111: protected static final String RESULTS_COUNT_ATTR = "resources";
112:
113: /** Element <code><result></code> */
114: protected static final String RESULT = "result";
115: /** Element <code><xmldb:result></code> (raw name) */
116: protected static final String QRESULT = PREFIX + ":" + RESULT;
117: protected static final String RESULT_DOCID_ATTR = "docid";
118: protected static final String RESULT_ID_ATTR = "id";
119:
120: protected static final String CDATA = "CDATA";
121:
122: //
123: // Instance variables
124: //
125:
126: /** The requested URL */
127: protected String url;
128:
129: /** The supplied user */
130: protected String user;
131:
132: /** The supplied password */
133: protected String password;
134:
135: /** The part of URL after # sign */
136: protected String query;
137:
138: /** The path for the collection (same as url if it's a collection) */
139: private final String colPath;
140:
141: /** The name of the resource in the collection (null if a collection) */
142: private String resName;
143:
144: /** Collection corresponding to {@link #colPath} */
145: private Collection collection;
146:
147: /** Resource corresponding to {@link #resName} */
148: private Resource resource;
149:
150: private int status = ST_UNKNOWN;
151:
152: /**
153: * The constructor.
154: *
155: * @param logger the Logger instance.
156: * @param user username
157: * @param password password
158: * @param srcUrl the URL being queried.
159: */
160: public XMLDBSource(Logger logger, String user, String password,
161: String srcUrl) {
162: enableLogging(logger);
163:
164: this .user = user;
165: this .password = password;
166:
167: // Parse URL
168: int start = srcUrl.indexOf('#');
169: if (start != -1) {
170: this .url = srcUrl.substring(0, start);
171: this .query = srcUrl.substring(start + 1);
172: if (query.length() == 0) {
173: query = null;
174: }
175: } else {
176: this .url = srcUrl;
177: }
178:
179: // Split path in collection name and resource name (if any)
180: if (url.endsWith("/")) {
181: colPath = url.substring(0, url.length() - 1);
182: } else {
183: int pos = url.lastIndexOf('/');
184: colPath = url.substring(0, pos);
185: resName = url.substring(pos + 1);
186: }
187: }
188:
189: private void setup() throws XMLDBException, SourceException {
190: if (status == ST_UNKNOWN) {
191: try {
192: // This can be a collection
193: collection = DatabaseManager.getCollection(colPath,
194: user, password);
195: if (collection == null) {
196: // Nope
197: status = ST_NO_PARENT;
198: return;
199: }
200:
201: if (resName == null) {
202: status = ST_COLLECTION;
203: } else {
204: // Or this can be a resource
205: resource = collection.getResource(resName);
206: if (resource == null) {
207: // Nope
208: status = ST_NO_RESOURCE;
209: } else {
210: status = ST_RESOURCE;
211: }
212: }
213: } finally {
214: if (status == ST_UNKNOWN) {
215: // Something went wrong: ensure any collection is closed
216: cleanup();
217: }
218: }
219: }
220: }
221:
222: private void cleanup() {
223: close(collection);
224: }
225:
226: private Collection createCollection(String path)
227: throws XMLDBException, SourceException {
228: Collection coll = DatabaseManager.getCollection(path,
229: this .user, this .password);
230: if (coll != null) {
231: return coll;
232: }
233: // Need to create the collection
234:
235: // Remove any trailing '/'
236: if (path.endsWith("/")) {
237: path = path.substring(0, path.length() - 1);
238: }
239:
240: int pos = path.lastIndexOf('/');
241: if (pos == -1) {
242: throw new SourceException("Invalid collection path " + path);
243: }
244: // Recurse
245: Collection parentColl = createCollection(path.substring(0, pos));
246:
247: // And create the child collection
248: CollectionManagementService mgtService = (CollectionManagementService) parentColl
249: .getService("CollectionManagementService", "1.0");
250: coll = mgtService.createCollection(path.substring(pos + 1));
251:
252: return coll;
253: }
254:
255: /**
256: * Close an XMLDB collection, ignoring any exception
257: * @param collection collection to be closed
258: */
259: private void close(Collection collection) {
260: if (collection != null) {
261: try {
262: collection.close();
263: } catch (XMLDBException e) {
264: // ignore;
265: }
266: }
267: }
268:
269: /**
270: * Stream SAX events to a given ContentHandler. If the requested
271: * resource is a collection, build an XML view of it.
272: */
273: public void toSAX(ContentHandler handler) throws SAXException {
274: try {
275: setup();
276: if (status == ST_COLLECTION) {
277: collectionToSAX(handler);
278: } else if (status == ST_RESOURCE) {
279: resourceToSAX(handler);
280: } else {
281: throw new SourceNotFoundException(getURI());
282: }
283: } catch (SAXException se) {
284: throw se;
285: } catch (Exception e) {
286: throw new SAXException("Error processing " + getURI(), e);
287: } finally {
288: cleanup();
289: }
290: }
291:
292: private void resourceToSAX(ContentHandler handler)
293: throws SAXException, XMLDBException {
294:
295: if (!(resource instanceof XMLResource)) {
296: throw new SAXException("Not an XML resource: " + getURI());
297: }
298:
299: if (query != null) {
300: // Query resource
301: if (getLogger().isDebugEnabled()) {
302: getLogger().debug(
303: "Querying resource " + resName
304: + " from collection " + url
305: + "; query= " + this .query);
306: }
307:
308: queryToSAX(handler, collection, resName);
309: } else {
310: // Return entire resource
311: if (getLogger().isDebugEnabled()) {
312: getLogger().debug(
313: "Obtaining resource " + resName
314: + " from collection " + colPath);
315: }
316:
317: ((XMLResource) resource).getContentAsSAX(handler);
318: }
319: }
320:
321: private void collectionToSAX(ContentHandler handler)
322: throws SAXException, XMLDBException {
323:
324: AttributesImpl attributes = new AttributesImpl();
325:
326: if (query != null) {
327: // Query collection
328: if (getLogger().isDebugEnabled()) {
329: getLogger().debug(
330: "Querying collection " + url + "; query= "
331: + this .query);
332: }
333:
334: queryToSAX(handler, collection, null);
335: } else {
336: // List collection
337: if (getLogger().isDebugEnabled()) {
338: getLogger().debug("Listing collection " + url);
339: }
340:
341: final String nresources = Integer.toString(collection
342: .getResourceCount());
343: attributes.addAttribute("", RESOURCE_COUNT_ATTR,
344: RESOURCE_COUNT_ATTR, "CDATA", nresources);
345: final String ncollections = Integer.toString(collection
346: .getChildCollectionCount());
347: attributes.addAttribute("", COLLECTION_COUNT_ATTR,
348: COLLECTION_COUNT_ATTR, "CDATA", ncollections);
349: attributes.addAttribute("", COLLECTION_BASE_ATTR,
350: COLLECTION_BASE_ATTR, "CDATA", url);
351:
352: handler.startDocument();
353: handler.startPrefixMapping(PREFIX, URI);
354: handler.startElement(URI, COLLECTIONS, QCOLLECTIONS,
355: attributes);
356:
357: // Print child collections
358: String[] collections = collection.listChildCollections();
359: for (int i = 0; i < collections.length; i++) {
360: attributes.clear();
361: attributes.addAttribute("", NAME_ATTR, NAME_ATTR,
362: CDATA, collections[i]);
363: handler.startElement(URI, COLLECTION, QCOLLECTION,
364: attributes);
365: handler.endElement(URI, COLLECTION, QCOLLECTION);
366: }
367:
368: // Print child resources
369: String[] resources = collection.listResources();
370: for (int i = 0; i < resources.length; i++) {
371: attributes.clear();
372: attributes.addAttribute("", NAME_ATTR, NAME_ATTR,
373: CDATA, resources[i]);
374: handler.startElement(URI, RESOURCE, QRESOURCE,
375: attributes);
376: handler.endElement(URI, RESOURCE, QRESOURCE);
377: }
378:
379: handler.endElement(URI, COLLECTIONS, QCOLLECTIONS);
380: handler.endPrefixMapping(PREFIX);
381: handler.endDocument();
382: }
383: }
384:
385: private void queryToSAX(ContentHandler handler,
386: Collection collection, String resource)
387: throws SAXException, XMLDBException {
388:
389: AttributesImpl attributes = new AttributesImpl();
390:
391: XPathQueryService service = (XPathQueryService) collection
392: .getService("XPathQueryService", "1.0");
393: ResourceSet resultSet = (resource == null) ? service
394: .query(query) : service.queryResource(resource, query);
395:
396: attributes.addAttribute("", QUERY_ATTR, QUERY_ATTR, "CDATA",
397: query);
398: attributes.addAttribute("", RESULTS_COUNT_ATTR,
399: RESULTS_COUNT_ATTR, "CDATA", Long.toString(resultSet
400: .getSize()));
401:
402: handler.startDocument();
403: handler.startPrefixMapping(PREFIX, URI);
404: handler.startElement(URI, RESULTSET, QRESULTSET, attributes);
405:
406: IncludeXMLConsumer includeHandler = new IncludeXMLConsumer(
407: handler);
408:
409: // Print search results
410: ResourceIterator results = resultSet.getIterator();
411: while (results.hasMoreResources()) {
412: XMLResource result = (XMLResource) results.nextResource();
413:
414: final String id = result.getId();
415: final String documentId = result.getDocumentId();
416:
417: attributes.clear();
418: if (id != null) {
419: attributes.addAttribute("", RESULT_ID_ATTR,
420: RESULT_ID_ATTR, CDATA, id);
421: }
422: if (documentId != null) {
423: attributes.addAttribute("", RESULT_DOCID_ATTR,
424: RESULT_DOCID_ATTR, CDATA, documentId);
425: }
426:
427: handler.startElement(URI, RESULT, QRESULT, attributes);
428: try {
429: result.getContentAsSAX(includeHandler);
430: } catch (XMLDBException xde) {
431: // That may be a text-only result
432: Object content = result.getContent();
433: if (content instanceof String) {
434: String text = (String) content;
435: handler.characters(text.toCharArray(), 0, text
436: .length());
437: } else {
438: // Cannot do better
439: throw xde;
440: }
441: }
442: handler.endElement(URI, RESULT, QRESULT);
443: }
444:
445: handler.endElement(URI, RESULTSET, QRESULTSET);
446: handler.endPrefixMapping(PREFIX);
447: handler.endDocument();
448: }
449:
450: public String getURI() {
451: return url;
452: }
453:
454: public long getContentLength() {
455: return -1;
456: }
457:
458: public long getLastModified() {
459: return 0;
460: }
461:
462: public boolean exists() {
463: try {
464: setup();
465: return status == ST_COLLECTION || status == ST_RESOURCE;
466: } catch (Exception e) {
467: return false;
468: } finally {
469: cleanup();
470: }
471: }
472:
473: public String getMimeType() {
474: return null;
475: }
476:
477: public String getScheme() {
478: return SourceUtil.getScheme(url);
479: }
480:
481: public SourceValidity getValidity() {
482: return null;
483: }
484:
485: public void refresh() {
486: }
487:
488: /**
489: * Get an InputSource for the given URL.
490: */
491: public InputStream getInputStream() throws IOException {
492: try {
493: setup();
494:
495: // Check if it's binary
496: if (resource instanceof BinaryResource) {
497: Object obj = resource.getContent();
498: if (obj == null)
499: obj = new byte[0];
500: if (obj instanceof byte[]) {
501: return new ByteArrayInputStream((byte[]) obj);
502: }
503:
504: throw new SourceException(
505: "Binary resource has returned a "
506: + obj.getClass() + " for " + getURI());
507: } else {
508: // Serialize SAX result
509: TransformerFactory tf = TransformerFactory
510: .newInstance();
511: TransformerHandler th = ((SAXTransformerFactory) tf)
512: .newTransformerHandler();
513: ByteArrayOutputStream bOut = new ByteArrayOutputStream();
514: StreamResult result = new StreamResult(bOut);
515: th.setResult(result);
516:
517: toSAX(th);
518:
519: return new ByteArrayInputStream(bOut.toByteArray());
520: }
521: } catch (IOException ioe) {
522: throw ioe;
523: } catch (Exception e) {
524: throw new CascadingIOException(
525: "Exception during processing of " + getURI(), e);
526: } finally {
527: cleanup();
528: }
529: }
530:
531: /**
532: * Return an {@link OutputStream} to write to. This method expects an XML document to be
533: * written in that stream. To create a binary resource, use {@link #getBinaryOutputStream()}.
534: */
535: public OutputStream getOutputStream() throws IOException,
536: MalformedURLException {
537: if (query != null) {
538: throw new MalformedURLException(
539: "Cannot modify a resource that includes an XPATH expression");
540: }
541:
542: return new XMLDBOutputStream(false);
543: }
544:
545: /**
546: * Return an {@link OutputStream} to write data to a binary resource.
547: */
548: public OutputStream getBinaryOutputStream() throws IOException,
549: MalformedURLException {
550: if (query != null) {
551: throw new MalformedURLException(
552: "Cannot modify a resource that includes an XPATH expression");
553: }
554:
555: return new XMLDBOutputStream(true);
556: }
557:
558: /**
559: * Create a new identifier for a resource within a collection. The current source must be
560: * an existing collection.
561: *
562: * @throws SourceException if collection does not exist or failed to create id
563: */
564: public String createId() throws SourceException {
565: try {
566: setup();
567: if (status != ST_COLLECTION) {
568: throw new SourceNotFoundException(
569: "Collection for createId not found: "
570: + getURI());
571: }
572:
573: return collection.createId();
574: } catch (XMLDBException xdbe) {
575: throw new SourceException("Cannot get Id for " + getURI(),
576: xdbe);
577: } finally {
578: cleanup();
579: }
580: }
581:
582: private void writeOutputStream(ByteArrayOutputStream baos,
583: boolean binary) throws SourceException {
584: try {
585: setup();
586: if (status == ST_NO_PARENT) {
587: // If there's no parent collection, create it
588: collection = createCollection(colPath);
589: status = ST_NO_RESOURCE;
590: }
591:
592: // If it's a collection - create an id for a new child resource.
593: String name;
594: if (status == ST_COLLECTION) {
595: name = collection.createId();
596: } else {
597: name = this .resName;
598: }
599:
600: Resource resource;
601: if (binary) {
602: resource = collection.createResource(name,
603: BinaryResource.RESOURCE_TYPE);
604: resource.setContent(baos.toByteArray());
605: } else {
606: resource = collection.createResource(name,
607: XMLResource.RESOURCE_TYPE);
608: // FIXME: potential encoding problems here, as we don't know the one use in the stream
609: resource.setContent(new String(baos.toByteArray()));
610: }
611:
612: collection.storeResource(resource);
613:
614: getLogger().debug("Written to resource " + name);
615: } catch (XMLDBException e) {
616: String message = "Failed to create resource " + resName
617: + ": " + e.errorCode;
618: throw new SourceException(message, e);
619: } finally {
620: cleanup();
621: }
622: }
623:
624: /**
625: * Delete the source
626: */
627: public void delete() throws SourceException {
628: try {
629: setup();
630: if (status == ST_RESOURCE) {
631: collection.removeResource(resource);
632: } else if (status == ST_COLLECTION) {
633: Collection parent = collection.getParentCollection();
634: CollectionManagementService service = (CollectionManagementService) parent
635: .getService("CollectionManagementService",
636: "1.0");
637: service.removeCollection(collection.getName());
638: close(parent);
639: }
640: } catch (SourceException se) {
641: throw se;
642: } catch (XMLDBException xdbe) {
643: throw new SourceException("Could not delete " + getURI());
644: } finally {
645: cleanup();
646: }
647: }
648:
649: /**
650: * Can the data sent to an <code>OutputStream</code> returned by
651: * {@link #getOutputStream()} be cancelled ?
652: *
653: * @return true if the stream can be cancelled
654: */
655: public boolean canCancel(OutputStream stream) {
656: return stream instanceof XMLDBOutputStream
657: && !((XMLDBOutputStream) stream).isClosed();
658: }
659:
660: /**
661: * Cancel the data sent to an <code>OutputStream</code> returned by
662: * {@link #getOutputStream()}.
663: *
664: * <p>After cancelling, the stream should no longer be used.</p>
665: */
666: public void cancel(OutputStream stream) throws IOException {
667: if (!canCancel(stream)) {
668: throw new SourceException("Cannot cancel stream for "
669: + getURI());
670: }
671:
672: ((XMLDBOutputStream) stream).cancel();
673: }
674:
675: private class XMLDBOutputStream extends OutputStream {
676: private ByteArrayOutputStream baos;
677: private boolean isClosed;
678: private boolean binary;
679:
680: public XMLDBOutputStream(boolean binary) {
681: baos = new ByteArrayOutputStream();
682: isClosed = false;
683: this .binary = binary;
684: }
685:
686: public void write(int b) throws IOException {
687: baos.write(b);
688: }
689:
690: public void write(byte b[]) throws IOException {
691: baos.write(b);
692: }
693:
694: public void write(byte b[], int off, int len)
695: throws IOException {
696: baos.write(b, off, len);
697: }
698:
699: public void close() throws IOException, SourceException {
700: if (!isClosed) {
701: writeOutputStream(baos, this .binary);
702: baos.close();
703: this .isClosed = true;
704: }
705: }
706:
707: public void flush() throws IOException {
708: }
709:
710: public int size() {
711: return baos.size();
712: }
713:
714: public boolean isClosed() {
715: return this .isClosed;
716: }
717:
718: public void cancel() {
719: this .isClosed = true;
720: }
721: }
722:
723: public void makeCollection() throws SourceException {
724: try {
725: createCollection(url);
726: } catch (SourceException e) {
727: throw e;
728: } catch (XMLDBException e) {
729: throw new SourceException("Cannot make collection with "
730: + getURI());
731: }
732: }
733:
734: public boolean isCollection() {
735: try {
736: setup();
737: return status == ST_COLLECTION;
738: } catch (Exception e) {
739: return false;
740: } finally {
741: cleanup();
742: }
743: }
744:
745: public java.util.Collection getChildren() throws SourceException {
746: try {
747: setup();
748: if (status != ST_COLLECTION) {
749: throw new SourceException("Not a collection: "
750: + getURI());
751: }
752:
753: String[] childColl = collection.listChildCollections();
754: String[] childRes = collection.listResources();
755:
756: ArrayList children = new ArrayList(childColl.length
757: + childRes.length);
758: for (int i = 0; i < childColl.length; i++) {
759: children.add(new XMLDBSource(getLogger(), user,
760: password, url + childColl[i]));
761: }
762: for (int i = 0; i < childRes.length; i++) {
763: children.add(new XMLDBSource(getLogger(), user,
764: password, url + childRes[i]));
765: }
766:
767: return children;
768: } catch (SourceException e) {
769: throw e;
770: } catch (XMLDBException e) {
771: throw new SourceException("Cannot list children of "
772: + getURI());
773: } finally {
774: cleanup();
775: }
776: }
777:
778: public Source getChild(String name) throws SourceException {
779: if (resName != null) {
780: throw new SourceException("Resource at " + url
781: + " can not have child resources.");
782: }
783:
784: return new XMLDBSource(getLogger(), user, password, url + name);
785: }
786:
787: public String getName() {
788: if (resName == null) {
789: int pos = colPath.lastIndexOf('/');
790: return colPath.substring(pos + 1);
791: }
792:
793: return resName;
794: }
795:
796: public Source getParent() throws SourceException {
797: if (resName == null) {
798: int pos = colPath.lastIndexOf('/');
799: return new XMLDBSource(getLogger(), user, password, colPath
800: .substring(0, pos + 1));
801: }
802:
803: return new XMLDBSource(getLogger(), user, password, colPath);
804: }
805: }
|