org.apache.jackrabbit.spi2davex.QValueFactoryImpl Maven / Gradle / Ivy
/*
* 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.jackrabbit.spi2davex;
import static org.apache.jackrabbit.webdav.DavConstants.HEADER_ETAG;
import static org.apache.jackrabbit.webdav.DavConstants.HEADER_LAST_MODIFIED;
import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants;
import org.apache.jackrabbit.commons.webdav.ValueUtil;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.value.AbstractQValue;
import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl;
import org.apache.jackrabbit.spi2dav.ItemResourceConstants;
import org.apache.jackrabbit.util.TransientFileFactory;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;
/**
* ValueFactoryImpl
...
*/
class QValueFactoryImpl extends org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl {
/**
* A dummy value for calling the constructor of AbstractQValue
*/
private static final Object DUMMY_VALUE = new Serializable() {
private static final long serialVersionUID = -5667366239976271493L;
};
/**
* empty array
*/
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
static final int NO_INDEX = -1;
private final ValueLoader loader;
private final ValueFactory vf;
public QValueFactoryImpl() {
this(null, null);
}
QValueFactoryImpl(NamePathResolver resolver, ValueLoader loader) {
this.loader = loader;
vf = new ValueFactoryQImpl(this, resolver);
}
/**
* Create a BINARY QValue with the given length and the given uri used
* to retrieve the value.
*
* @param length Length of the binary value.
* @param uri Uri from which the the binary value can be accessed.
* @param index The index of the value within the values array.
* @return a new BINARY QValue.
*/
QValue create(long length, String uri, int index) {
if (loader == null) {
throw new IllegalStateException();
}
return new BinaryQValue(length, uri, index);
}
/**
*
* @param uri The Uri from which the type info can be retrieved.
* @return the type of the property with the given uri
.
* @throws IOException If an error occurs.
* @throws RepositoryException If an error occurs.
*/
int retrieveType(String uri) throws IOException, RepositoryException {
return loader.loadType(uri);
}
//--------------------------------------------------------< Inner Class >---
/**
* BinaryQValue
represents a binary Value
which is
* backed by a resource or byte[]. Unlike BinaryValue
it has no
* state, i.e. the getStream()
method always returns a fresh
* InputStream
instance.
*/
private class BinaryQValue extends AbstractQValue implements ValueLoader.Target {
private static final long serialVersionUID = 2736654000266713469L;
/**
* max size for keeping tmp data in memory
*/
private static final int MAX_BUFFER_SIZE = 0x10000;
/**
* underlying file
*/
private transient File file;
/**
* flag indicating if this instance represents a temporary value
* whose dynamically allocated resources can be explicitly freed on
* {@link #discard()}.
*/
private transient boolean temp;
/**
* Buffer for small-sized data
*/
private byte[] buffer;
private Map headers;
/**
* URI to retrieve the value from
*/
private final String uri;
private final long length;
private final int index;
private boolean initialized = true;
private BinaryQValue(long length, String uri, int index) {
super(DUMMY_VALUE, PropertyType.BINARY);
this.length = length;
this.uri = uri;
this.index = index;
initialized = false;
}
/**
* Creates a new BinaryQValue
instance from an
* InputStream
. The contents of the stream is spooled
* to a temporary file or to a byte buffer if its size is smaller than
* {@link #MAX_BUFFER_SIZE}.
*
* The temp
parameter governs whether dynamically allocated
* resources will be freed explicitly on {@link #discard()}. Note that any
* dynamically allocated resources (temp file/buffer) will be freed
* implicitly once this instance has been gc'ed.
*
* @param in stream to be represented as a BinaryQValue
instance
* @param temp flag indicating whether this instance represents a
* temporary value whose resources can be explicitly freed
* on {@link #discard()}.
* @throws IOException if an error occurs while reading from the stream or
* writing to the temporary file
*/
private void init(InputStream in, boolean temp) throws IOException {
byte[] spoolBuffer = new byte[0x2000];
int read;
int len = 0;
OutputStream out = null;
File spoolFile = null;
try {
while ((read = in.read(spoolBuffer)) > 0) {
if (out != null) {
// spool to temp file
out.write(spoolBuffer, 0, read);
len += read;
} else if (len + read > BinaryQValue.MAX_BUFFER_SIZE) {
// threshold for keeping data in memory exceeded;
// create temp file and spool buffer contents
TransientFileFactory fileFactory = TransientFileFactory.getInstance();
spoolFile = fileFactory.createTransientFile("bin", null, null);
out = new FileOutputStream(spoolFile);
out.write(buffer, 0, len);
out.write(spoolBuffer, 0, read);
buffer = null;
len += read;
} else {
// reallocate new buffer and spool old buffer contents
if (buffer == null) {
buffer = EMPTY_BYTE_ARRAY;
}
byte[] newBuffer = new byte[len + read];
System.arraycopy(buffer, 0, newBuffer, 0, len);
System.arraycopy(spoolBuffer, 0, newBuffer, len, read);
buffer = newBuffer;
len += read;
}
}
} finally {
in.close();
if (out != null) {
out.close();
}
}
if (spoolFile == null && buffer == null) {
// input stream was empty -> initialize an empty binary value
this.temp = false;
buffer = EMPTY_BYTE_ARRAY;
} else {
// init vars
file = spoolFile;
this.temp = temp;
}
initialized = true;
}
//---------------------------------------------------------< QValue >---
/**
* Returns the length of this BinaryQValue
.
*
* @return The length, in bytes, of this BinaryQValue
,
* or -1L if the length can't be determined.
* @see QValue#getLength()
*/
@Override
public long getLength() {
if (file != null) {
// this instance is backed by a 'real' file
if (file.exists()) {
return file.length();
} else {
return -1;
}
} else if (buffer != null) {
// this instance is backed by an in-memory buffer
return buffer.length;
} else {
// value has not yet been read from the server.
return length;
}
}
/**
* @see QValue#getStream()
*/
public InputStream getStream() throws RepositoryException {
// if the value has not yet been loaded -> retrieve it first in
// order to make sure that either 'file' or 'buffer' is set.
if (file == null && buffer == null) {
try {
loadBinary();
} catch (IOException e) {
throw new RepositoryException(e);
}
}
// always return a 'fresh' stream
if (file != null) {
// this instance is backed by a 'real' file
try {
return new FileInputStream(file);
} catch (FileNotFoundException fnfe) {
throw new RepositoryException("file backing binary value not found",
fnfe);
}
} else {
return new ByteArrayInputStream(buffer);
}
}
/**
* @see QValue#getName()
*/
@Override
public Name getName() throws RepositoryException {
throw new UnsupportedOperationException();
}
/**
* @see QValue#getPath()
*/
@Override
public Path getPath() throws RepositoryException {
throw new UnsupportedOperationException();
}
/**
* Frees temporarily allocated resources such as temporary file, buffer, etc.
* If this BinaryQValue
is backed by a persistent resource
* calling this method will have no effect.
* @see QValue#discard()
*/
@Override
public void discard() {
if (!temp) {
// do nothing if this instance is not backed by temporarily
// allocated resource/buffer
return;
}
if (file != null) {
// this instance is backed by a temp file
file.delete();
} else if (buffer != null) {
// this instance is backed by an in-memory buffer
buffer = EMPTY_BYTE_ARRAY;
}
}
/**
* Resets the state of this value. a subsequent call to init() can be
* used to load the binary again.
*
* If this BinaryQValue
is backed by a persistent resource
* calling this method will have no effect.
* @see QValue#discard()
*/
public void reset() {
if (!temp) {
// do nothing if this instance is not backed by temporarily
// allocated resource/buffer
return;
}
if (file != null) {
// this instance is backed by a temp file
file.delete();
}
file = null;
buffer = null;
initialized = false;
}
//-----------------------------------------------< java.lang.Object >---
/**
* Returns a string representation of this BinaryQValue
* instance. The string representation of a resource backed value is
* the path of the underlying resource. If this instance is backed by an
* in-memory buffer the generic object string representation of the byte
* array will be used instead.
*
* @return A string representation of this BinaryQValue
instance.
*/
@Override
public String toString() {
if (file != null) {
// this instance is backed by a 'real' file
return file.toString();
} else if (buffer != null) {
// this instance is backed by an in-memory buffer
return buffer.toString();
} else {
return super.toString();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof BinaryQValue) {
BinaryQValue other = (BinaryQValue) obj;
// Consider unequal urls as unequal values and both urls null as equal values
if (this.uri == null) {
return other.uri == null;
}
if (!this.uri.equals(other.uri)) {
return false;
}
// Consider both uninitialized as equal values
if (!this.preInitialized() && !other.preInitialized()) {
return true;
}
try {
// Initialized the one which is not
if (!this.preInitialized()) {
this.preInitialize(new String[] {HEADER_ETAG, HEADER_LAST_MODIFIED});
} else if (!other.preInitialized()) {
other.preInitialize(new String[] {HEADER_ETAG, HEADER_LAST_MODIFIED});
}
} catch (RepositoryException e) {
return false;
} catch (IOException e) {
return false;
}
// If we have headers try to determine equality from them
if (headers != null && !headers.isEmpty()) {
// Values are (un)equal if we have equal Etags
if (containKey(HEADER_ETAG, this.headers, other.headers)) {
return equalValue(HEADER_ETAG, this.headers, other.headers);
}
// Values are unequal if we have different Last-modified values
if (containKey(HEADER_LAST_MODIFIED, this.headers, other.headers)) {
if (!equalValue(HEADER_LAST_MODIFIED, this.headers, other.headers)) {
return false;
}
}
// Otherwise compare binaries
} else {
return ((file == null ? other.file == null : file.equals(other.file))
&& Arrays.equals(buffer, other.buffer));
}
}
return false;
}
/**
* Returns zero to satisfy the Object equals/hashCode contract.
* This class is mutable and not meant to be used as a hash key.
*
* @return always zero
* @see Object#hashCode()
*/
@Override
public int hashCode() {
return 0;
}
//----------------------------------------------------------------------
private synchronized void loadBinary() throws RepositoryException, IOException {
if (uri == null) {
throw new IllegalStateException();
}
loader.loadBinary(uri, index, this);
}
/**
* Load the header with the given names. If none of the named headers exist, load binary.
*/
private void preInitialize(String[] headerNames) throws IOException, RepositoryException {
headers = loader.loadHeaders(uri, headerNames);
if (headers.isEmpty()) {
loadBinary();
}
}
/**
* @return true
if either initialized or headers have been
* loaded, false
otherwise.
*/
private boolean preInitialized() {
return initialized || headers != null;
}
/**
* @return true
if both maps contain the same value for
* key
, false
otherwise. The
* key
must not map to null
in either
* map.
*/
private boolean equalValue(String key, Map map1, Map map2) {
return map1.get(key).equals(map2.get(key));
}
/**
* @return true
if both maps contains the key
,
* false
otherwise.
*/
private boolean containKey(String key, Map map1, Map map2) {
return map1.containsKey(key) && map2.containsKey(key);
}
//-----------------------------< Serializable >-------------------------
private void writeObject(ObjectOutputStream out)
throws IOException {
out.defaultWriteObject();
// write hasFile marker
out.writeBoolean(file != null);
// then write file if necessary
if (file != null) {
byte[] buffer = new byte[4096];
int bytes;
InputStream stream = new FileInputStream(file);
while ((bytes = stream.read(buffer)) >= 0) {
// Write a segment of the input stream
if (bytes > 0) {
// just to ensure that no 0 is written
out.writeInt(bytes);
out.write(buffer, 0, bytes);
}
}
// Write the end of stream marker
out.writeInt(0);
// close stream
stream.close();
}
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
boolean hasFile = in.readBoolean();
if (hasFile) {
file = File.createTempFile("binary-qvalue", "bin");
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[4096];
for (int bytes = in.readInt(); bytes > 0; bytes = in.readInt()) {
if (buffer.length < bytes) {
buffer = new byte[bytes];
}
in.readFully(buffer, 0, bytes);
out.write(buffer, 0, bytes);
}
out.close();
}
// deserialized value is always temp
temp = true;
}
//---------------------------------------------------------< Target >---
public void setStream(InputStream in) throws IOException {
if (index == NO_INDEX) {
init(in, true);
} else {
// TODO: improve. jcr-server sends XML for multivalued properties
try {
Document doc = DomUtil.parseDocument(in);
Element prop = DomUtil.getChildElement(doc, JcrRemotingConstants.JCR_VALUES_LN, ItemResourceConstants.NAMESPACE);
DavProperty> p = DefaultDavProperty.createFromXml(prop);
Value[] jcrVs = ValueUtil.valuesFromXml(p.getValue(), PropertyType.BINARY, vf);
init(jcrVs[index].getStream(), true);
} catch (RepositoryException e) {
throw new IOException(e.getMessage());
} catch (SAXException e) {
throw new IOException(e.getMessage());
} catch (ParserConfigurationException e) {
throw new IOException(e.getMessage());
}
}
}
}
}