/* * eXist Open Source Native XML Database * Copyright (C) 2001-2015 The eXist Project * http://exist-db.org * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.exist.xmldb; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import org.apache.xmlrpc.XmlRpcException; import org.apache.commons.io.output.ByteArrayOutputStream; import org.exist.security.Permission; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.util.EXistInputSource; import org.exist.util.VirtualTempFile; 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.XMLDBException; import static java.nio.charset.StandardCharsets.UTF_8; public abstract class AbstractRemoteResource extends AbstractRemote implements EXistResource, ExtendedResource, Resource { protected final XmldbURI path; private String mimeType; protected VirtualTempFile vfile = null; private VirtualTempFile contentVFile = null; protected InputSource inputSource = null; private boolean isLocal = false; private long contentLen = 0L; private Permission permissions = null; Date dateCreated = null; Date dateModified = null; protected AbstractRemoteResource(final RemoteCollection parent, final XmldbURI documentName, final String mimeType) throws XMLDBException { super(parent); if (documentName.numSegments() > 1) { this.path = documentName; } else { this.path = parent.getPathURI().append(documentName); } this.mimeType = mimeType; } protected Properties getProperties() { return collection.getProperties(); } @Override public Object getContent() throws XMLDBException { final Object res = getExtendedContent(); // Backward compatibility if (isLocal) { return res; } else if (res != null) { if (res instanceof Path) { return readFile((Path)res); } else if (res instanceof java.io.File) { return readFile(((java.io.File) res).toPath()); } else if (res instanceof InputSource) { return readFile((InputSource) res); } } return res; } @Override protected void finalize() throws Throwable { try { freeResources(); } finally { super.finalize(); } } @Override public void freeResources() { vfile = null; inputSource = null; if (contentVFile != null) { contentVFile.delete(); contentVFile = null; } isLocal = true; } /** * @deprecated Here for backward compatibility, instead use {@see org.xmldb.api.base.Resource#getContent()} */ @Deprecated protected byte[] getData() throws XMLDBException { final Object res = getExtendedContent(); if (res != null) { if (res instanceof Path) { return readFile((Path)res); } else if (res instanceof java.io.File) { return readFile(((java.io.File) res).toPath()); } else if (res instanceof InputSource) { return readFile((InputSource) res); } else if (res instanceof String) { return ((String) res).getBytes(UTF_8); } } return (byte[]) res; } @Override public long getContentLength() throws XMLDBException { return contentLen; } @Override public Date getCreationTime() throws XMLDBException { return dateCreated; } @Override public Date getLastModificationTime() throws XMLDBException { return dateModified; } @Override public void setLastModificationTime(final Date dateModified) throws XMLDBException { if (dateModified != null) { if(dateModified.before(getCreationTime())) { throw new XMLDBException(ErrorCodes.PERMISSION_DENIED, "Modification time must be after creation time."); } final List params = new ArrayList(2); params.add(path.toString()); params.add(dateModified.getTime()); try { collection.getClient().execute("setLastModified", params); } catch (final XmlRpcException e) { throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, e.getMessage(), e); } this.dateModified = dateModified; } } public long getExtendedContentLength() throws XMLDBException { return contentLen; } @Override public String getMimeType() { return mimeType; } @Override public Collection getParentCollection() throws XMLDBException { return collection; } @Override public Permission getPermissions() { return permissions; } protected boolean setContentInternal(final Object value) throws XMLDBException { freeResources(); boolean wasSet = false; if (value instanceof VirtualTempFile) { vfile = (VirtualTempFile) value; // Assuring the virtual file is close state try { vfile.close(); } catch (final IOException ioe) { // IgnoreIT(R) } setExtendendContentLength(vfile.length()); wasSet = true; } else if (value instanceof Path) { vfile = new VirtualTempFile(((Path) value).toFile()); setExtendendContentLength(vfile.length()); wasSet = true; } else if (value instanceof java.io.File) { vfile = new VirtualTempFile((java.io.File) value); setExtendendContentLength(vfile.length()); wasSet = true; } else if (value instanceof InputSource) { inputSource = (InputSource) value; wasSet = true; } else if (value instanceof byte[]) { vfile = new VirtualTempFile((byte[]) value); setExtendendContentLength(vfile.length()); wasSet = true; } else if (value instanceof String) { vfile = new VirtualTempFile(((String) value).getBytes(UTF_8)); setExtendendContentLength(vfile.length()); wasSet = true; } return wasSet; } protected void setExtendendContentLength(final long len) { this.contentLen = len; } public void setContentLength(final int len) { this.contentLen = len; } public void setContentLength(final long len) { this.contentLen = len; } @Override public void setMimeType(final String mimeType) { this.mimeType = mimeType; } public void setPermissions(final Permission perms) { permissions = perms; } @Override public void getContentIntoAFile(final Path localfile) throws XMLDBException { try(final OutputStream os = Files.newOutputStream(localfile)) { getContentIntoAStream(os); } catch (final IOException ioe) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, ioe.getMessage(), ioe); } } protected void getRemoteContentIntoLocalFile(final OutputStream os, final boolean isRetrieve, final int handle, final int pos) throws XMLDBException { final String command; final List<Object> params = new ArrayList<>(); if (isRetrieve) { command = "retrieveFirstChunk"; params.add(Integer.valueOf(handle)); params.add(Integer.valueOf(pos)); } else { command = "getDocumentData"; params.add(path.toString()); } params.add(getProperties()); try { final VirtualTempFile vtmpfile = new VirtualTempFile(); vtmpfile.setTempPrefix("eXistARR"); vtmpfile.setTempPostfix("XMLResource".equals(getResourceType()) ? ".xml" : ".bin"); Map table = (Map) collection.getClient().execute(command, params); final String method; final boolean useLongOffset; if (table.containsKey("supports-long-offset") && (Boolean)table.get("supports-long-offset")) { useLongOffset = true; method = "getNextExtendedChunk"; } else { useLongOffset = false; method = "getNextChunk"; } long offset = ((Integer) table.get("offset")).intValue(); byte[] data = (byte[]) table.get("data"); final boolean isCompressed = "yes".equals(getProperties().getProperty(EXistOutputKeys.COMPRESS_OUTPUT, "no")); // One for the local cached file Inflater dec = null; byte[] decResult = null; int decLength; if (isCompressed) { dec = new Inflater(); decResult = new byte[65536]; dec.setInput(data); do { decLength = dec.inflate(decResult); vtmpfile.write(decResult, 0, decLength); // And other for the stream where we want to save it! if (os != null) { os.write(decResult, 0, decLength); } } while (decLength == decResult.length || !dec.needsInput()); } else { vtmpfile.write(data); // And other for the stream where we want to save it! if (os != null) { os.write(data); } } while (offset > 0) { params.clear(); params.add(table.get("handle")); params.add(useLongOffset ? Long.toString(offset) : Integer.valueOf((int) offset)); table = (Map<?, ?>) collection.getClient().execute(method, params); offset = useLongOffset ? Long.parseLong((String) table.get("offset")) : ((Integer) table.get("offset")); data = (byte[]) table.get("data"); // One for the local cached file if (isCompressed) { dec.setInput(data); do { decLength = dec.inflate(decResult); vtmpfile.write(decResult, 0, decLength); // And other for the stream where we want to save it! if (os != null) { os.write(decResult, 0, decLength); } } while (decLength == decResult.length || !dec.needsInput()); } else { vtmpfile.write(data); // And other for the stream where we want to save it! if (os != null) { os.write(data); } } } if (dec != null) { dec.end(); } isLocal = false; contentVFile = vtmpfile; } catch (final XmlRpcException xre) { throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, xre.getMessage(), xre); } catch (final IOException | DataFormatException e) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e); } finally { if (contentVFile != null) { try { contentVFile.close(); } catch (final IOException ioe) { //IgnoreIT(R) } } } } protected static InputStream getAnyStream(final Object obj) throws XMLDBException { if (obj instanceof String) { return new ByteArrayInputStream(((String) obj).getBytes(UTF_8)); } else if (obj instanceof byte[]) { return new ByteArrayInputStream((byte[]) obj); } else { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "don't know how to handle value of type " + obj.getClass().getName()); } } protected void getContentIntoAStreamInternal(final OutputStream os, final Object obj, final boolean isRetrieve, final int handle, final int pos) throws XMLDBException { if (vfile != null || contentVFile != null || inputSource != null || obj != null) { InputStream bis = null; try { // First, the local content, then the remote one!!!! if (vfile != null) { bis = vfile.getByteStream(); } else if (inputSource != null) { bis = inputSource.getByteStream(); } else if (obj != null) { bis = getAnyStream(obj); } else { bis = contentVFile.getByteStream(); } copy(bis, os); } catch (final IOException ioe) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, ioe.getMessage(), ioe); } finally { if (inputSource != null) { if (bis != null) { // As it comes from an input source, we cannot blindly close it, // but at least let's reset it! (if it is possible) if (bis.markSupported()) { try { bis.reset(); } catch (final IOException ioe) { //IgnoreIT(R) } } } } else { if (bis != null) { try { bis.close(); } catch (final IOException ioe) { //IgnoreIT(R) } } } } } else { // Let's fetch it, and save just in time!!! getRemoteContentIntoLocalFile(os, isRetrieve, handle, pos); } } protected Object getExtendedContentInternal(final Object obj, final boolean isRetrieve, final int handle, final int pos) throws XMLDBException { if (obj != null) { return obj; } else if (vfile != null) { return vfile.getContent(); } else if (inputSource != null) { return inputSource; } else { if (contentVFile == null) { getRemoteContentIntoLocalFile(null, isRetrieve, handle, pos); } return contentVFile.getContent(); } } protected InputStream getStreamContentInternal(final Object obj, final boolean isRetrieve, final int handle, final int pos) throws XMLDBException { final InputStream retval; try { if (vfile != null) { retval = vfile.getByteStream(); } else if (inputSource != null) { retval = inputSource.getByteStream(); } else if (obj != null) { retval = getAnyStream(obj); } else { // At least one value, please!!! if (contentVFile == null) { getRemoteContentIntoLocalFile(null, isRetrieve, handle, pos); } retval = contentVFile.getByteStream(); } } catch (final IOException e) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e); } return retval; } protected long getStreamLengthInternal(final Object obj) throws XMLDBException { final long retval; if (vfile != null) { retval = vfile.length(); } else if (inputSource != null && inputSource instanceof EXistInputSource) { retval = ((EXistInputSource) inputSource).getByteStreamLength(); } else if (obj != null) { if (obj instanceof String) { retval = ((String) obj).getBytes(UTF_8).length; } else if (obj instanceof byte[]) { retval = ((byte[]) obj).length; } else { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "don't know how to handle value of type " + obj.getClass().getName()); } } else if (contentVFile != null) { retval = contentVFile.length(); } else { final List<Object> params = new ArrayList<>(); params.add(path.toString()); params.add(getProperties()); try { final Map table = (Map) collection.getClient().execute("describeResource", params); if (table.containsKey("content-length-64bit")) { final Object o = table.get("content-length-64bit"); if (o instanceof Long) { retval = ((Long) o); } else { retval = Long.parseLong((String) o); } } else { retval = ((Integer) table.get("content-length")); } } catch (final XmlRpcException xre) { throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, xre.getMessage(), xre); } } return retval; } protected byte[] readFile(final Path file) throws XMLDBException { try(final ByteArrayOutputStream os = new ByteArrayOutputStream()) { Files.copy(file, os); return os.toByteArray(); } catch (final IOException e) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e); } } protected byte[] readFile(final InputSource in) throws XMLDBException { final InputStream bis = in.getByteStream(); try { return readFile(bis); } finally { //TODO(AR) why do we do this? should probably close it? // As it comes from an input source, we cannot blindly close it, // but at least let's reset it! (if it is possible) if (bis.markSupported()) { try { bis.reset(); } catch (final IOException ioe) { //IgnoreIT(R) } } } } private byte[] readFile(final InputStream is) throws XMLDBException { try(final ByteArrayOutputStream bos = new ByteArrayOutputStream()) { copy(is, bos); return bos.toByteArray(); } catch (final IOException e) { throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e); } } private void copy(final InputStream is, final OutputStream os) throws IOException { int read; final byte buffer[] = new byte[65536]; //64KB while ((read = is.read(buffer)) > -1) { os.write(buffer, 0, read); } } }