/*
* eXist Open Source Native XML Database
* Copyright (C) 2001 Wolfgang M. Meier
* meier@ifs.tu-darmstadt.de
* http://exist.sourceforge.net
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.xmldb;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.exist.security.Permission;
import org.exist.security.PermissionFactory;
import org.exist.util.Compressor;
import org.exist.util.EXistInputSource;
import org.exist.validation.service.RemoteValidationService;
import org.xml.sax.InputSource;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.ErrorCodes;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.Service;
import org.xmldb.api.base.XMLDBException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* A remote implementation of the Collection interface. This
* implementation communicates with the server through the XMLRPC
* protocol.
*
* @author wolf
* Updated Andy Foster - Updated code to allow child collection cache to
* resync with the remote collection.
*/
public class RemoteCollection implements CollectionImpl {
// max size of a resource to be send to the server
// if the resource exceeds this limit, the data is split into
// junks and uploaded to the server via the update() call
private static final int MAX_CHUNK_LENGTH = 512 * 1024;
private static final int MAX_UPLOAD_CHUNK = 10 * 1024 * 1024;
protected Map childCollections = null;
protected XmldbURI path;
protected Permission permissions = null;
protected RemoteCollection parent = null;
protected XmlRpcClient rpcClient = null;
protected Properties properties = null;
public RemoteCollection(XmlRpcClient client, XmldbURI path)
throws XMLDBException {
this(client, null, path);
}
public RemoteCollection(
XmlRpcClient client,
RemoteCollection parent,
XmldbURI path)
throws XMLDBException {
this.parent = parent;
this.path = path.toCollectionPathURI();
this.rpcClient = client;
}
protected void addChildCollection(Collection child) throws XMLDBException {
if (childCollections == null)
readCollection();
try {
childCollections.put(XmldbURI.xmldbUriFor(child.getName()), child);
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
}
public void close() throws XMLDBException {
try {
rpcClient.execute("sync", new ArrayList());
} catch (XmlRpcException e) {
throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, "failed to close collection", e);
}
}
public String createId() throws XMLDBException {
List params = new ArrayList(1);
params.add(getPath());
try {
return (String)rpcClient.execute("createResourceId", params);
} catch (XmlRpcException e) {
throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, "failed to close collection", e);
}
}
public Resource createResource(String id, String type) throws XMLDBException {
XmldbURI newId;
try {
newId = (id == null) ? XmldbURI.xmldbUriFor(createId()) : XmldbURI.xmldbUriFor(id);
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
Resource r;
if(type.equals("XMLResource"))
r = new RemoteXMLResource(this, -1, -1, newId, null);
else if(type.equals("BinaryResource"))
r = new RemoteBinaryResource(this, newId);
else
throw new XMLDBException(ErrorCodes.UNKNOWN_RESOURCE_TYPE, "unknown resource type: " + type);
return r;
}
public Collection getChildCollection(String name) throws XMLDBException {
try {
return getChildCollection(XmldbURI.xmldbUriFor(name));
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
}
public Collection getChildCollection(XmldbURI name) throws XMLDBException {
// AF get the child collection refreshing cache from server if not found
return getChildCollection(name,true);
}
// AF - NEW METHOD
protected Collection getChildCollection(XmldbURI name, boolean refreshCacheIfNotFound) throws XMLDBException {
if (childCollections == null) {
readCollection();
refreshCacheIfNotFound = false;
}
// stores reference to the collection found
Collection foundCollection = null;
if (name.numSegments()>1)
foundCollection = (Collection) childCollections.get(name);
else
foundCollection = (Collection) childCollections.get(getPathURI().append(name));
// if we did not find collection in cache set cache back to null to force full refresh
if (foundCollection == null && refreshCacheIfNotFound) {
childCollections = null;
return getChildCollection(name,false);
}
// return the found collection
return foundCollection;
}
public int getChildCollectionCount() throws XMLDBException {
// AF Always refresh cache for latest set - if (childCollections == null)
readCollection();
return childCollections.size();
}
protected XmlRpcClient getClient() {
return rpcClient;
}
public String getName() throws XMLDBException {
return path.toString();
}
public Collection getParentCollection() throws XMLDBException {
if(parent == null && !path.equals(XmldbURI.ROOT_COLLECTION_URI)) {
XmldbURI parentUri = path.removeLastSegment();
return new RemoteCollection(rpcClient, null, parentUri);
}
return parent;
}
public String getPath() throws XMLDBException {
return getPathURI().toString();
}
public XmldbURI getPathURI() {
if (parent == null) {
/*
if(name != null)
return name;
else
*/
return XmldbURI.ROOT_COLLECTION_URI;
}
return path;
}
public String getProperty(String property) throws XMLDBException {
if(properties == null) return null;
return (String)properties.get(property);
}
public Properties getProperties() {
if(properties == null)
properties = new Properties();
return properties;
}
public int getResourceCount() throws XMLDBException {
List params = new ArrayList(1);
params.add(getPath());
try {
return ((Integer)rpcClient.execute("getResourceCount", params)).intValue();
} catch (XmlRpcException e) {
throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, "failed to close collection", e);
}
}
public Service getService(String name, String version) throws XMLDBException {
if (name.equals("XPathQueryService"))
return new RemoteXPathQueryService(this);
if (name.equals("XQueryService"))
return new RemoteXPathQueryService(this);
if (name.equals("CollectionManagementService") || name.equals("CollectionManager"))
return new RemoteCollectionManagementService(this, rpcClient);
if (name.equals("UserManagementService"))
return new RemoteUserManagementService(this);
if (name.equals("DatabaseInstanceManager"))
return new RemoteDatabaseInstanceManager(rpcClient);
if (name.equals("IndexQueryService"))
return new RemoteIndexQueryService(rpcClient, this);
if (name.equals("XUpdateQueryService"))
return new RemoteXUpdateQueryService(this);
if (name.equals("ValidationService"))
return new RemoteValidationService(this, rpcClient);
throw new XMLDBException(ErrorCodes.NO_SUCH_SERVICE);
}
public Service[] getServices() throws XMLDBException {
Service[] services = new Service[7];
services[0] = new RemoteXPathQueryService(this);
services[1] = new RemoteCollectionManagementService(this, rpcClient);
services[2] = new RemoteUserManagementService(this);
services[3] = new RemoteDatabaseInstanceManager(rpcClient);
services[4] = new RemoteIndexQueryService(rpcClient, this);
services[5] = new RemoteXUpdateQueryService(this);
services[6] = new RemoteValidationService(this, rpcClient);
return services;
}
protected boolean hasChildCollection(String name) throws XMLDBException {
// AF Always refresh cache for latest set - if (childCollections == null)
readCollection();
try {
return childCollections.containsKey(XmldbURI.xmldbUriFor(name));
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
}
public boolean isOpen() throws XMLDBException {
return true;
}
/**
* Returns a list of collection names naming all child collections of the
* current collection. Only the name of the collection is returned - not
* the entire path to the collection.
*
*@return Description of the Return Value
*@exception XMLDBException Description of the Exception
*/
public String[] listChildCollections() throws XMLDBException {
// Always refresh cache for latest set - if (childCollections == null)
readCollection();
String coll[] = new String[childCollections.size()];
int j = 0;
XmldbURI uri;
for (Iterator i = childCollections.keySet().iterator(); i.hasNext(); j++) {
uri = (XmldbURI) i.next();
coll[j] = uri.lastSegment().toString();
}
return coll;
}
public String[] getChildCollections() throws XMLDBException {
return listChildCollections();
}
public String[] listResources() throws XMLDBException {
List params = new ArrayList(1);
params.add(getPath());
try {
Object[] r = (Object[]) rpcClient.execute("getDocumentListing", params);
String[] resources = new String[r.length];
System.arraycopy(r, 0, resources, 0, r.length);
return resources;
} catch (XmlRpcException xre) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, xre.getMessage(), xre);
}
}
/* (non-Javadoc)
* @see org.exist.xmldb.CollectionImpl#getResources()
*/
public String[] getResources() throws XMLDBException {
return listResources();
}
public Resource getResource(String name) throws XMLDBException {
List params = new ArrayList(1);
XmldbURI docUri;
try {
docUri = XmldbURI.xmldbUriFor(name);
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
params.add(getPathURI().append(docUri).toString());
HashMap hash;
try {
hash = (HashMap) rpcClient.execute("describeResource", params);
} catch (XmlRpcException xre) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, xre.getMessage(), xre);
}
String docName = (String) hash.get("name");
if(docName == null)
return null; // resource does not exist!
try {
docUri = XmldbURI.xmldbUriFor(docName).lastSegment();
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
Permission perm = PermissionFactory.getPermission(
(String) hash.get("owner"),
(String) hash.get("group"),
((Integer) hash.get("permissions")).intValue());
String type = (String)hash.get("type");
int contentLen = 0;
if(hash.containsKey("content-length"))
contentLen = ((Integer)hash.get("content-length")).intValue();
if(type == null || type.equals("XMLResource")) {
RemoteXMLResource r = new RemoteXMLResource(this, -1, -1, docUri, null);
r.setPermissions(perm);
r.setContentLength(contentLen);
r.setDateCreated((Date) hash.get("created"));
r.setDateModified((Date) hash.get("modified"));
if (hash.containsKey("mime-type"))
r.setMimeType((String) hash.get("mime-type"));
return r;
} else {
RemoteBinaryResource r = new RemoteBinaryResource(this, docUri);
r.setContentLength(contentLen);
r.setPermissions(perm);
r.setDateCreated((Date) hash.get("created"));
r.setDateModified((Date) hash.get("modified"));
if (hash.containsKey("mime-type"))
r.setMimeType((String) hash.get("mime-type"));
return r;
}
}
private void readCollection() throws XMLDBException {
childCollections = new HashMap();
List params = new ArrayList(1);
params.add(getPath());
HashMap collection;
try {
collection = (HashMap) rpcClient.execute("describeCollection", params);
} catch (XmlRpcException xre) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, xre.getMessage(), xre);
}
Object[] collections = (Object[]) collection.get("collections");
permissions = PermissionFactory.getPermission(
(String) collection.get("owner"),
(String) collection.get("group"),
((Integer) collection.get("permissions")).intValue());
String childName;
for (int i = 0; i < collections.length; i++) {
childName = (String) collections[i];
try {
//TODO: Should this use the checked version instead?
RemoteCollection child =
new RemoteCollection(rpcClient, this, getPathURI().append(XmldbURI.create(childName)));
addChildCollection(child);
} catch (XMLDBException e) {
}
}
}
public void registerService(Service serv) throws XMLDBException {
throw new XMLDBException(ErrorCodes.NOT_IMPLEMENTED);
}
public void removeChildCollection(String name) throws XMLDBException {
try {
removeChildCollection(XmldbURI.xmldbUriFor(name));
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
}
public void removeChildCollection(XmldbURI name) throws XMLDBException {
if (childCollections == null)
readCollection();
childCollections.remove(name);
}
public void removeResource(Resource res) throws XMLDBException {
List params = new ArrayList(1);
try {
params.add(getPathURI().append(XmldbURI.xmldbUriFor(res.getId())).toString());
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
try {
rpcClient.execute("remove", params);
} catch (XmlRpcException xre) {
throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, xre.getMessage(), xre);
}
}
public Date getCreationTime() throws XMLDBException {
List params = new ArrayList(1);
params.add(getPath());
try {
return (Date) rpcClient.execute("getCreationDate", params);
} catch (XmlRpcException e) {
throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, e.getMessage(), e);
}
}
public void setProperty(String property, String value) throws XMLDBException {
if(properties == null)
properties = new Properties();
properties.setProperty(property, value);
}
public void storeResource(Resource res) throws XMLDBException {
storeResource(res, null, null);
}
public void storeResource(Resource res, Date a, Date b) throws XMLDBException {
Object content = (res instanceof ExtendedResource)?
((ExtendedResource)res).getExtendedContent():
res.getContent();
if (content instanceof File || content instanceof InputSource) {
long fileLength=-1;
if(content instanceof File) {
File file = (File) content;
if (!file.canRead())
throw new XMLDBException(
ErrorCodes.INVALID_RESOURCE,
"failed to read resource from file " + file.getAbsolutePath());
fileLength=file.length();
} else if(content instanceof EXistInputSource) {
fileLength=((EXistInputSource)content).getByteStreamLength();
}
if(res.getResourceType().equals("BinaryResource")) {
((RemoteBinaryResource)res).dateCreated =a;
((RemoteBinaryResource)res).dateModified =b;
} else {
((RemoteXMLResource)res).dateCreated =a;
((RemoteXMLResource)res).dateModified =b;
}
if (!res.getResourceType().equals("BinaryResource") && fileLength!=-1 && fileLength < MAX_CHUNK_LENGTH) {
store((RemoteXMLResource)res);
} else {
uploadAndStore(res);
}
} else if(res.getResourceType().equals("BinaryResource")) {
((RemoteBinaryResource)res).dateCreated =a;
((RemoteBinaryResource)res).dateModified =b;
store((RemoteBinaryResource)res);
} else {
((RemoteXMLResource)res).dateCreated =a;
((RemoteXMLResource)res).dateModified =b;
store((RemoteXMLResource)res);
}
}
private void store(RemoteXMLResource res) throws XMLDBException {
byte[] data = res.getData();
List params = new ArrayList(1);
params.add(data);
try {
params.add(getPathURI().append(XmldbURI.xmldbUriFor(res.getId())).toString());
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
params.add(new Integer(1));
if (res.dateCreated != null) {
params.add((Date)res.dateCreated );
params.add((Date)res.dateModified );
}
try {
rpcClient.execute("parse", params);
} catch (XmlRpcException xre) {
throw new XMLDBException(
ErrorCodes.INVALID_RESOURCE,
xre == null ? "unknown error" : xre.getMessage(),
xre);
}
}
private void store(RemoteBinaryResource res) throws XMLDBException {
byte[] data = (byte[])res.getContent();
List params = new ArrayList(1);
params.add(data);
try {
params.add(getPathURI().append(XmldbURI.xmldbUriFor(res.getId())).toString());
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
params.add(res.getMimeType());
params.add(Boolean.TRUE);
if (res.dateCreated != null) {
params.add((Date)res.dateCreated );
params.add((Date)res.dateModified );
}
try {
rpcClient.execute("storeBinary", params);
} catch (XmlRpcException xre) {
/* the error code previously was INVALID_RESOURCE, but this was also thrown
* in case of insufficient persmissions. As you cannot tell here any more what the
* error really was, use UNKNOWN_ERROR. The reason is in XmlRpcResponseProcessor#processException
* which will only pass on the error message.
*/
throw new XMLDBException(
ErrorCodes.UNKNOWN_ERROR,
xre == null ? "unknown error" : xre.getMessage(),
xre);
}
}
private void uploadAndStore(Resource res) throws XMLDBException {
InputStream is=null;
String descstring="<unknown>";
if(res instanceof RemoteBinaryResource) {
is=((RemoteBinaryResource)res).getStreamContent();
descstring=((RemoteBinaryResource)res).getStreamSymbolicPath();
} else {
Object content=((RemoteXMLResource)res).getContent();
if(content instanceof File) {
File file=(File)content;
try {
is=new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new XMLDBException(
ErrorCodes.INVALID_RESOURCE,
"could not read resource from file " + file.getAbsolutePath(),
e);
}
} else if(content instanceof InputSource) {
is=((InputSource)content).getByteStream();
if(content instanceof EXistInputSource) {
descstring=((EXistInputSource)content).getSymbolicPath();
}
}
}
byte[] chunk = new byte[MAX_UPLOAD_CHUNK];
try {
int len;
String fileName = null;
List params;
byte[] compressed;
while ((len = is.read(chunk)) > -1) {
compressed = Compressor.compress(chunk, len);
params = new ArrayList(3);
if (fileName != null)
params.add(fileName);
params.add(compressed);
params.add(new Integer(len));
fileName = (String) rpcClient.execute("uploadCompressed", params);
}
// Zero length stream? Let's get a fileName!
if(fileName==null) {
compressed=Compressor.compress(new byte[0],0);
params = new ArrayList(3);
params.add(compressed);
params.add(new Integer(0));
fileName = (String) rpcClient.execute("uploadCompressed", params);
}
params = new ArrayList(6);
List paramsEx = new ArrayList(7);
params.add(fileName);
paramsEx.add(fileName);
try {
String resURI=getPathURI().append(XmldbURI.xmldbUriFor(res.getId())).toString();
params.add(resURI);
paramsEx.add(resURI);
} catch(URISyntaxException e) {
throw new XMLDBException(ErrorCodes.INVALID_URI,e);
}
params.add(Boolean.TRUE);
paramsEx.add(Boolean.TRUE);
if(res instanceof EXistResource) {
EXistResource rxres=(EXistResource)res;
params.add(rxres.getMimeType());
paramsEx.add(rxres.getMimeType());
// This one is only for the new style!!!!
paramsEx.add((res.getResourceType().equals("BinaryResource"))?Boolean.FALSE:Boolean.TRUE);
if(rxres.getCreationTime() != null) {
params.add(rxres.getCreationTime());
paramsEx.add(rxres.getCreationTime());
params.add(rxres.getLastModificationTime());
paramsEx.add(rxres.getLastModificationTime());
}
}
try {
rpcClient.execute("parseLocalExt", paramsEx);
} catch(XmlRpcException e) {
// Identifying old versions
String excMsg=e.getMessage();
if(excMsg.contains("No such handler") || excMsg.contains("No method matching")) {
rpcClient.execute("parseLocal", params);
} else {
throw e;
}
}
} catch (IOException e) {
throw new XMLDBException(
ErrorCodes.INVALID_RESOURCE,
"failed to read resource from " + descstring,
e);
} catch (XmlRpcException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "networking error", e);
}
}
public Permission getPermissions() {
return permissions;
}
/* (non-Javadoc)
* @see org.exist.xmldb.CollectionImpl#isRemoteCollection()
*/
public boolean isRemoteCollection() throws XMLDBException {
return true;
}
}