| /* |
| * 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.components.source.impl; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.MalformedURLException; |
| import java.util.ArrayList; |
| |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.sax.SAXTransformerFactory; |
| import javax.xml.transform.sax.TransformerHandler; |
| import javax.xml.transform.stream.StreamResult; |
| |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.logger.Logger; |
| import org.apache.cocoon.CascadingIOException; |
| import org.apache.cocoon.xml.IncludeXMLConsumer; |
| import org.apache.excalibur.source.ModifiableTraversableSource; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceNotFoundException; |
| import org.apache.excalibur.source.SourceUtil; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.apache.excalibur.xml.sax.XMLizable; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| import org.xmldb.api.DatabaseManager; |
| import org.xmldb.api.base.Collection; |
| import org.xmldb.api.base.Resource; |
| import org.xmldb.api.base.ResourceIterator; |
| import org.xmldb.api.base.ResourceSet; |
| import org.xmldb.api.base.XMLDBException; |
| import org.xmldb.api.modules.BinaryResource; |
| import org.xmldb.api.modules.CollectionManagementService; |
| import org.xmldb.api.modules.XMLResource; |
| import org.xmldb.api.modules.XPathQueryService; |
| |
| /** |
| * This class implements the xmldb:// pseudo-protocol and allows to get XML |
| * content from an XML:DB enabled XML database. |
| * |
| * @version CVS $Id$ |
| */ |
| public class XMLDBSource extends AbstractLogEnabled |
| implements ModifiableTraversableSource, XMLizable { |
| |
| private static final int ST_UNKNOWN = 0; |
| private static final int ST_COLLECTION = 1; |
| private static final int ST_RESOURCE = 2; |
| private static final int ST_NO_PARENT = 3; |
| private static final int ST_NO_RESOURCE = 4; |
| |
| // |
| // Static Strings used for XML Collection representation |
| // |
| |
| /** Source namespace */ |
| public static final String URI = "http://apache.org/cocoon/xmldb/1.0"; |
| |
| /** Source prefix */ |
| public static final String PREFIX = "db"; |
| |
| /** Root element <code><collections></code> */ |
| protected static final String COLLECTIONS = "collections"; |
| /** Root element <code><xmldb:collections></code> (raw name) */ |
| protected static final String QCOLLECTIONS = PREFIX + ":" + COLLECTIONS; |
| /** Attribute <code>resources</code> on the root element indicates count of resources in the collection */ |
| protected static final String RESOURCE_COUNT_ATTR = "resources"; |
| /** Attribute <code>collections</code> on the root element indicates count of collections in the collection */ |
| protected static final String COLLECTION_COUNT_ATTR = "collections"; |
| protected static final String COLLECTION_BASE_ATTR = "base"; |
| |
| /** Element <code><collection></code> */ |
| protected static final String COLLECTION = "collection"; |
| /** Element <code><xmldb:collection></code> (raw name) */ |
| protected static final String QCOLLECTION = PREFIX + ":" + COLLECTION; |
| |
| /** Element <code><resource></code> */ |
| protected static final String RESOURCE = "resource"; |
| /** Element <code><resource></code> (raw name) */ |
| protected static final String QRESOURCE = PREFIX + ":" + RESOURCE; |
| /** Attribute <code>name</code> on the collection/resource element */ |
| protected static final String NAME_ATTR = "name"; |
| |
| /** Root element <code><results></code> */ |
| protected static final String RESULTSET = "results"; |
| /** Root element <code><xmldb:results></code> (raw name) */ |
| protected static final String QRESULTSET = PREFIX + ":" + RESULTSET; |
| protected static final String QUERY_ATTR = "query"; |
| protected static final String RESULTS_COUNT_ATTR = "resources"; |
| |
| /** Element <code><result></code> */ |
| protected static final String RESULT = "result"; |
| /** Element <code><xmldb:result></code> (raw name) */ |
| protected static final String QRESULT = PREFIX + ":" + RESULT; |
| protected static final String RESULT_DOCID_ATTR = "docid"; |
| protected static final String RESULT_ID_ATTR = "id"; |
| |
| protected static final String CDATA = "CDATA"; |
| |
| // |
| // Instance variables |
| // |
| |
| /** The requested URL */ |
| protected String url; |
| |
| /** The supplied user */ |
| protected String user; |
| |
| /** The supplied password */ |
| protected String password; |
| |
| /** The part of URL after # sign */ |
| protected String query; |
| |
| /** The path for the collection (same as url if it's a collection) */ |
| private final String colPath; |
| |
| /** The name of the resource in the collection (null if a collection) */ |
| private String resName; |
| |
| /** Collection corresponding to {@link #colPath} */ |
| private Collection collection; |
| |
| /** Resource corresponding to {@link #resName} */ |
| private Resource resource; |
| |
| private int status = ST_UNKNOWN; |
| |
| |
| /** |
| * The constructor. |
| * |
| * @param logger the Logger instance. |
| * @param user username |
| * @param password password |
| * @param srcUrl the URL being queried. |
| */ |
| public XMLDBSource(Logger logger, |
| String user, String password, |
| String srcUrl) { |
| enableLogging(logger); |
| |
| this.user = user; |
| this.password = password; |
| |
| // Parse URL |
| int start = srcUrl.indexOf('#'); |
| if (start != -1) { |
| this.url = srcUrl.substring(0, start); |
| this.query = srcUrl.substring(start + 1); |
| if (query.length() == 0) { |
| query = null; |
| } |
| } else { |
| this.url = srcUrl; |
| } |
| |
| // Split path in collection name and resource name (if any) |
| if (url.endsWith("/")) { |
| colPath = url.substring(0, url.length() - 1); |
| } else { |
| int pos = url.lastIndexOf('/'); |
| colPath = url.substring(0, pos); |
| resName = url.substring(pos + 1); |
| } |
| } |
| |
| private void setup() throws XMLDBException, SourceException { |
| if (status == ST_UNKNOWN) { |
| try { |
| // This can be a collection |
| collection = DatabaseManager.getCollection(colPath, user, password); |
| if (collection == null) { |
| // Nope |
| status = ST_NO_PARENT; |
| return; |
| } |
| |
| if (resName == null) { |
| status = ST_COLLECTION; |
| } else { |
| // Or this can be a resource |
| resource = collection.getResource(resName); |
| if (resource == null) { |
| // Nope |
| status = ST_NO_RESOURCE; |
| } else { |
| status = ST_RESOURCE; |
| } |
| } |
| } finally { |
| if (status == ST_UNKNOWN) { |
| // Something went wrong: ensure any collection is closed |
| cleanup(); |
| } |
| } |
| } |
| } |
| |
| private void cleanup() { |
| close(collection); |
| } |
| |
| private Collection createCollection(String path) throws XMLDBException, SourceException { |
| Collection coll = DatabaseManager.getCollection(path, this.user, this.password); |
| if (coll != null) { |
| return coll; |
| } |
| // Need to create the collection |
| |
| // Remove any trailing '/' |
| if (path.endsWith("/")) { |
| path = path.substring(0, path.length() - 1); |
| } |
| |
| int pos = path.lastIndexOf('/'); |
| if (pos == -1) { |
| throw new SourceException("Invalid collection path " + path); |
| } |
| // Recurse |
| Collection parentColl = createCollection(path.substring(0, pos)); |
| |
| // And create the child collection |
| CollectionManagementService mgtService = (CollectionManagementService) |
| parentColl.getService("CollectionManagementService", "1.0"); |
| coll = mgtService.createCollection(path.substring(pos+1)); |
| |
| return coll; |
| } |
| |
| |
| /** |
| * Close an XMLDB collection, ignoring any exception |
| * @param collection collection to be closed |
| */ |
| private void close(Collection collection) { |
| if (collection != null) { |
| try { |
| collection.close(); |
| } catch (XMLDBException e) { |
| // ignore; |
| } |
| } |
| } |
| |
| /** |
| * Stream SAX events to a given ContentHandler. If the requested |
| * resource is a collection, build an XML view of it. |
| */ |
| public void toSAX(ContentHandler handler) throws SAXException { |
| try { |
| setup(); |
| if (status == ST_COLLECTION) { |
| collectionToSAX(handler); |
| } else if (status == ST_RESOURCE) { |
| resourceToSAX(handler); |
| } else { |
| throw new SourceNotFoundException(getURI()); |
| } |
| } catch (SAXException se) { |
| throw se; |
| } catch (Exception e) { |
| throw new SAXException("Error processing " + getURI(), e); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| private void resourceToSAX(ContentHandler handler) |
| throws SAXException, XMLDBException { |
| |
| if (!(resource instanceof XMLResource)) { |
| throw new SAXException("Not an XML resource: " + getURI()); |
| } |
| |
| if (query != null) { |
| // Query resource |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Querying resource " + resName + " from collection " + url + "; query= " + this.query); |
| } |
| |
| queryToSAX(handler, collection, resName); |
| } else { |
| // Return entire resource |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Obtaining resource " + resName + " from collection " + colPath); |
| } |
| |
| ((XMLResource) resource).getContentAsSAX(handler); |
| } |
| } |
| |
| private void collectionToSAX(ContentHandler handler) |
| throws SAXException, XMLDBException { |
| |
| AttributesImpl attributes = new AttributesImpl(); |
| |
| if (query != null) { |
| // Query collection |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Querying collection " + url + "; query= " + this.query); |
| } |
| |
| queryToSAX(handler, collection, null); |
| } else { |
| // List collection |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Listing collection " + url); |
| } |
| |
| final String nresources = Integer.toString(collection.getResourceCount()); |
| attributes.addAttribute("", RESOURCE_COUNT_ATTR, |
| RESOURCE_COUNT_ATTR, "CDATA", nresources); |
| final String ncollections = Integer.toString(collection.getChildCollectionCount()); |
| attributes.addAttribute("", COLLECTION_COUNT_ATTR, |
| COLLECTION_COUNT_ATTR, "CDATA", ncollections); |
| attributes.addAttribute("", COLLECTION_BASE_ATTR, |
| COLLECTION_BASE_ATTR, "CDATA", url); |
| |
| handler.startDocument(); |
| handler.startPrefixMapping(PREFIX, URI); |
| handler.startElement(URI, COLLECTIONS, QCOLLECTIONS, attributes); |
| |
| // Print child collections |
| String[] collections = collection.listChildCollections(); |
| for (int i = 0; i < collections.length; i++) { |
| attributes.clear(); |
| attributes.addAttribute("", NAME_ATTR, NAME_ATTR, CDATA, collections[i]); |
| handler.startElement(URI, COLLECTION, QCOLLECTION, attributes); |
| handler.endElement(URI, COLLECTION, QCOLLECTION); |
| } |
| |
| // Print child resources |
| String[] resources = collection.listResources(); |
| for (int i = 0; i < resources.length; i++) { |
| attributes.clear(); |
| attributes.addAttribute("", NAME_ATTR, NAME_ATTR, CDATA, resources[i]); |
| handler.startElement(URI, RESOURCE, QRESOURCE, attributes); |
| handler.endElement(URI, RESOURCE, QRESOURCE); |
| } |
| |
| handler.endElement(URI, COLLECTIONS, QCOLLECTIONS); |
| handler.endPrefixMapping(PREFIX); |
| handler.endDocument(); |
| } |
| } |
| |
| private void queryToSAX(ContentHandler handler, Collection collection, String resource) |
| throws SAXException, XMLDBException { |
| |
| AttributesImpl attributes = new AttributesImpl(); |
| |
| XPathQueryService service = |
| (XPathQueryService) collection.getService("XPathQueryService", "1.0"); |
| ResourceSet resultSet = (resource == null) ? |
| service.query(query) : service.queryResource(resource, query); |
| |
| attributes.addAttribute("", QUERY_ATTR, QUERY_ATTR, "CDATA", query); |
| attributes.addAttribute("", RESULTS_COUNT_ATTR, |
| RESULTS_COUNT_ATTR, "CDATA", Long.toString(resultSet.getSize())); |
| |
| handler.startDocument(); |
| handler.startPrefixMapping(PREFIX, URI); |
| handler.startElement(URI, RESULTSET, QRESULTSET, attributes); |
| |
| IncludeXMLConsumer includeHandler = new IncludeXMLConsumer(handler); |
| |
| // Print search results |
| ResourceIterator results = resultSet.getIterator(); |
| while (results.hasMoreResources()) { |
| XMLResource result = (XMLResource)results.nextResource(); |
| |
| final String id = result.getId(); |
| final String documentId = result.getDocumentId(); |
| |
| attributes.clear(); |
| if (id != null) { |
| attributes.addAttribute("", RESULT_ID_ATTR, RESULT_ID_ATTR, |
| CDATA, id); |
| } |
| if (documentId != null) { |
| attributes.addAttribute("", RESULT_DOCID_ATTR, RESULT_DOCID_ATTR, |
| CDATA, documentId); |
| } |
| |
| handler.startElement(URI, RESULT, QRESULT, attributes); |
| try { |
| result.getContentAsSAX(includeHandler); |
| } catch(XMLDBException xde) { |
| // That may be a text-only result |
| Object content = result.getContent(); |
| if (content instanceof String) { |
| String text = (String)content; |
| handler.characters(text.toCharArray(), 0, text.length()); |
| } else { |
| // Cannot do better |
| throw xde; |
| } |
| } |
| handler.endElement(URI, RESULT, QRESULT); |
| } |
| |
| handler.endElement(URI, RESULTSET, QRESULTSET); |
| handler.endPrefixMapping(PREFIX); |
| handler.endDocument(); |
| } |
| |
| public String getURI() { |
| return url; |
| } |
| |
| public long getContentLength() { |
| return -1; |
| } |
| |
| public long getLastModified() { |
| return 0; |
| } |
| |
| public boolean exists() { |
| try { |
| setup(); |
| return status == ST_COLLECTION || status == ST_RESOURCE; |
| } catch (Exception e) { |
| return false; |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| public String getMimeType() { |
| return null; |
| } |
| |
| public String getScheme() { |
| return SourceUtil.getScheme(url); |
| } |
| |
| public SourceValidity getValidity() { |
| return null; |
| } |
| |
| public void refresh() { |
| } |
| |
| /** |
| * Get an InputSource for the given URL. |
| */ |
| public InputStream getInputStream() |
| throws IOException { |
| try { |
| setup(); |
| |
| // Check if it's binary |
| if (resource instanceof BinaryResource) { |
| Object obj = resource.getContent(); |
| if (obj == null) obj = new byte[0]; |
| if (obj instanceof byte[]) { |
| return new ByteArrayInputStream((byte[])obj); |
| } |
| |
| throw new SourceException("Binary resource has returned a " + obj.getClass() + " for " + getURI()); |
| } else { |
| // Serialize SAX result |
| TransformerFactory tf = TransformerFactory.newInstance(); |
| TransformerHandler th = |
| ((SAXTransformerFactory) tf).newTransformerHandler(); |
| ByteArrayOutputStream bOut = new ByteArrayOutputStream(); |
| StreamResult result = new StreamResult(bOut); |
| th.setResult(result); |
| |
| toSAX(th); |
| |
| return new ByteArrayInputStream(bOut.toByteArray()); |
| } |
| } catch (IOException ioe) { |
| throw ioe; |
| } catch (Exception e) { |
| throw new CascadingIOException("Exception during processing of " + getURI(), e); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| /** |
| * Return an {@link OutputStream} to write to. This method expects an XML document to be |
| * written in that stream. To create a binary resource, use {@link #getBinaryOutputStream()}. |
| */ |
| public OutputStream getOutputStream() throws IOException, MalformedURLException { |
| if (query != null) { |
| throw new MalformedURLException("Cannot modify a resource that includes an XPATH expression"); |
| } |
| |
| return new XMLDBOutputStream(false); |
| } |
| |
| /** |
| * Return an {@link OutputStream} to write data to a binary resource. |
| */ |
| public OutputStream getBinaryOutputStream() throws IOException, MalformedURLException { |
| if (query != null) { |
| throw new MalformedURLException("Cannot modify a resource that includes an XPATH expression"); |
| } |
| |
| return new XMLDBOutputStream(true); |
| } |
| |
| /** |
| * Create a new identifier for a resource within a collection. The current source must be |
| * an existing collection. |
| * |
| * @throws SourceException if collection does not exist or failed to create id |
| */ |
| public String createId() throws SourceException { |
| try { |
| setup(); |
| if (status != ST_COLLECTION) { |
| throw new SourceNotFoundException("Collection for createId not found: " + getURI()); |
| } |
| |
| return collection.createId(); |
| } catch (XMLDBException xdbe) { |
| throw new SourceException("Cannot get Id for " + getURI(), xdbe); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| private void writeOutputStream(ByteArrayOutputStream baos, boolean binary) throws SourceException { |
| try { |
| setup(); |
| if (status == ST_NO_PARENT) { |
| // If there's no parent collection, create it |
| collection = createCollection(colPath); |
| status = ST_NO_RESOURCE; |
| } |
| |
| // If it's a collection - create an id for a new child resource. |
| String name; |
| if (status == ST_COLLECTION) { |
| name = collection.createId(); |
| } else { |
| name = this.resName; |
| } |
| |
| Resource resource; |
| if (binary) { |
| resource = collection.createResource(name, BinaryResource.RESOURCE_TYPE); |
| resource.setContent(baos.toByteArray()); |
| } else { |
| resource = collection.createResource(name, XMLResource.RESOURCE_TYPE); |
| // FIXME: potential encoding problems here, as we don't know the one use in the stream |
| resource.setContent(new String(baos.toByteArray())); |
| } |
| |
| collection.storeResource(resource); |
| |
| getLogger().debug("Written to resource " + name); |
| } catch (XMLDBException e) { |
| String message = "Failed to create resource " + resName + ": " + e.errorCode; |
| throw new SourceException(message, e); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| /** |
| * Delete the source |
| */ |
| public void delete() throws SourceException { |
| try { |
| setup(); |
| if (status == ST_RESOURCE) { |
| collection.removeResource(resource); |
| } else if (status == ST_COLLECTION) { |
| Collection parent = collection.getParentCollection(); |
| CollectionManagementService service = |
| (CollectionManagementService) parent.getService("CollectionManagementService", "1.0"); |
| service.removeCollection(collection.getName()); |
| close(parent); |
| } |
| } catch (SourceException se) { |
| throw se; |
| } catch (XMLDBException xdbe) { |
| throw new SourceException("Could not delete " + getURI()); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| /** |
| * Can the data sent to an <code>OutputStream</code> returned by |
| * {@link #getOutputStream()} be cancelled ? |
| * |
| * @return true if the stream can be cancelled |
| */ |
| public boolean canCancel(OutputStream stream) { |
| return stream instanceof XMLDBOutputStream && !((XMLDBOutputStream)stream).isClosed(); |
| } |
| |
| /** |
| * Cancel the data sent to an <code>OutputStream</code> returned by |
| * {@link #getOutputStream()}. |
| * |
| * <p>After cancelling, the stream should no longer be used.</p> |
| */ |
| public void cancel(OutputStream stream) throws IOException { |
| if (!canCancel(stream)) { |
| throw new SourceException("Cannot cancel stream for " + getURI()); |
| } |
| |
| ((XMLDBOutputStream) stream).cancel(); |
| } |
| |
| private class XMLDBOutputStream extends OutputStream { |
| private ByteArrayOutputStream baos; |
| private boolean isClosed; |
| private boolean binary; |
| |
| public XMLDBOutputStream(boolean binary) { |
| baos = new ByteArrayOutputStream(); |
| isClosed = false; |
| this.binary = binary; |
| } |
| |
| public void write(int b) throws IOException { |
| baos.write(b); |
| } |
| |
| public void write(byte b[]) throws IOException { |
| baos.write(b); |
| } |
| |
| public void write(byte b[], int off, int len) throws IOException { |
| baos.write(b, off, len); |
| } |
| |
| public void close() throws IOException, SourceException { |
| if (!isClosed) { |
| writeOutputStream(baos, this.binary); |
| baos.close(); |
| this.isClosed = true; |
| } |
| } |
| |
| public void flush() throws IOException { |
| } |
| |
| public int size() { |
| return baos.size(); |
| } |
| |
| public boolean isClosed() { |
| return this.isClosed; |
| } |
| |
| public void cancel() { |
| this.isClosed = true; |
| } |
| } |
| |
| public void makeCollection() throws SourceException { |
| try { |
| createCollection(url); |
| } catch (SourceException e) { |
| throw e; |
| } catch (XMLDBException e) { |
| throw new SourceException("Cannot make collection with " + getURI()); |
| } |
| } |
| |
| public boolean isCollection() { |
| try { |
| setup(); |
| return status == ST_COLLECTION; |
| } catch (Exception e) { |
| return false; |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| public java.util.Collection getChildren() throws SourceException { |
| try { |
| setup(); |
| if (status != ST_COLLECTION) { |
| throw new SourceException("Not a collection: " + getURI()); |
| } |
| |
| String[] childColl = collection.listChildCollections(); |
| String[] childRes = collection.listResources(); |
| |
| ArrayList children = new ArrayList(childColl.length + childRes.length); |
| for (int i = 0; i < childColl.length; i++) { |
| children.add(new XMLDBSource(getLogger(), user, password, url + childColl[i])); |
| } |
| for (int i = 0; i < childRes.length; i++) { |
| children.add(new XMLDBSource(getLogger(), user, password, url + childRes[i])); |
| } |
| |
| return children; |
| } catch (SourceException e) { |
| throw e; |
| } catch (XMLDBException e) { |
| throw new SourceException("Cannot list children of " + getURI()); |
| } finally { |
| cleanup(); |
| } |
| } |
| |
| public Source getChild(String name) throws SourceException { |
| if (resName != null) { |
| throw new SourceException("Resource at " + url + " can not have child resources."); |
| } |
| |
| return new XMLDBSource(getLogger(), user, password, url + name); |
| } |
| |
| public String getName() { |
| if (resName == null) { |
| int pos = colPath.lastIndexOf('/'); |
| return colPath.substring(pos + 1); |
| } |
| |
| return resName; |
| } |
| |
| public Source getParent() throws SourceException { |
| if (resName == null) { |
| int pos = colPath.lastIndexOf('/'); |
| return new XMLDBSource(getLogger(), user, password, colPath.substring(0, pos + 1)); |
| } |
| |
| return new XMLDBSource(getLogger(), user, password, colPath); |
| } |
| } |