/*
* 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.spi.commons.value;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.QValueFactory;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.jackrabbit.util.TransientFileFactory;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.jcr.Binary;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.io.UnsupportedEncodingException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Calendar;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
/**
* <code>QValueFactoryImpl</code>...
*/
public final class QValueFactoryImpl extends AbstractQValueFactory {
private static final QValueFactory INSTANCE = new QValueFactoryImpl();
private QValueFactoryImpl() {
}
public static QValueFactory getInstance() {
return INSTANCE;
}
//------------------------------------------------------< QValueFactory >---
/**
* @see QValueFactory#create(String, int)
*/
public QValue create(String value, int type) throws RepositoryException {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
try {
switch (type) {
case PropertyType.BOOLEAN:
return (Boolean.valueOf(value).booleanValue()) ?
QValueImpl.TRUE :
QValueImpl.FALSE;
case PropertyType.DATE: {
Calendar cal = ISO8601.parse(value);
if (cal == null) {
throw new ValueFormatException("not a valid date: " + value);
}
return new DateQValue(cal);
}
case PropertyType.DOUBLE:
return new QValueImpl(Double.valueOf(value));
case PropertyType.LONG:
return new QValueImpl(Long.valueOf(value));
case PropertyType.DECIMAL:
return new QValueImpl(new BigDecimal(value));
case PropertyType.URI:
return new QValueImpl(URI.create(value));
case PropertyType.PATH:
return new QValueImpl(PATH_FACTORY.create(value));
case PropertyType.NAME:
return new QValueImpl(NAME_FACTORY.create(value));
case PropertyType.STRING:
case PropertyType.REFERENCE:
case PropertyType.WEAKREFERENCE:
return new QValueImpl(value, type);
case PropertyType.BINARY:
return new BinaryQValue(value.getBytes(DEFAULT_ENCODING));
// default: invalid type specified -> see below.
}
} catch (IllegalArgumentException ex) {
// given String value cannot be converted to Long/Double/Path/Name
throw new ValueFormatException(ex);
} catch (UnsupportedEncodingException ex) {
throw new RepositoryException(ex);
}
// invalid type specified:
throw new IllegalArgumentException("illegal type " + type);
}
/**
* @see QValueFactory#create(Calendar)
*/
public QValue create(Calendar value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
// Calendar is not constant, must create a clone
return new DateQValue((Calendar) value.clone());
}
/**
* @see QValueFactory#create(double)
*/
public QValue create(double value) {
return new QValueImpl(Double.valueOf(value));
}
/**
* @see QValueFactory#create(long)
*/
public QValue create(long value) {
return new QValueImpl(Long.valueOf(value));
}
/**
* @see QValueFactory#create(boolean)
*/
public QValue create(boolean value) {
if (value) {
return QValueImpl.TRUE;
} else {
return QValueImpl.FALSE;
}
}
/**
* @see QValueFactory#create(Name)
*/
public QValue create(Name value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new QValueImpl(value);
}
/**
* @see QValueFactory#create(Path)
*/
public QValue create(Path value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new QValueImpl(value);
}
/**
* @see QValueFactory#create(URI)
*/
public QValue create(URI value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new QValueImpl(value);
}
/**
* @see QValueFactory#create(URI)
*/
public QValue create(BigDecimal value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new QValueImpl(value);
}
/**
* @see QValueFactory#create(byte[])
*/
public QValue create(byte[] value) {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new BinaryQValue(value);
}
/**
* @see QValueFactory#create(InputStream)
*/
public QValue create(InputStream value) throws IOException {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new BinaryQValue(value);
}
/**
* @see QValueFactory#create(File)
*/
public QValue create(File value) throws IOException {
if (value == null) {
throw new IllegalArgumentException("Cannot create QValue from null value.");
}
return new BinaryQValue(value);
}
//--------------------------------------------------------< Inner Class >---
/**
* <code>QValue</code> implementation for all valid <code>PropertyType</code>s
* except for BINARY and DATE.
* @see QValueFactoryImpl.BinaryQValue
*/
private static class QValueImpl extends AbstractQValue implements Serializable {
private static final QValue TRUE = new QValueImpl(Boolean.TRUE);
private static final QValue FALSE = new QValueImpl(Boolean.FALSE);
private QValueImpl(Object value, int type) {
super(value, type);
}
private QValueImpl(String value, int type) {
super(value, type);
}
private QValueImpl(Long value) {
super(value);
}
private QValueImpl(Double value) {
super(value);
}
private QValueImpl(BigDecimal value) {
super(value);
}
private QValueImpl(Boolean value) {
super(value);
}
private QValueImpl(Name value) {
super(value);
}
private QValueImpl(Path value) {
super(value);
}
private QValueImpl(URI value) {
super(value);
}
//---------------------------------------------------------< QValue >---
/**
* @see QValue#getString()
*/
public String getString() {
return val.toString();
}
/**
* @see QValue#getBinary()
*/
public Binary getBinary() throws RepositoryException {
// TODO FIXME consolidate Binary implementations
return new Binary() {
public InputStream getStream() throws RepositoryException {
return QValueImpl.this.getStream();
}
public int read(byte[] b, long position) throws IOException, RepositoryException {
InputStream in = getStream();
try {
in.skip(position);
return in.read(b);
} finally {
in.close();
}
}
public long getSize() throws RepositoryException {
return getLength();
}
public void dispose() {
}
};
}
/**
* @see QValue#getStream()
*/
public InputStream getStream() throws RepositoryException {
try {
// convert via string
return new ByteArrayInputStream(getString().getBytes(DEFAULT_ENCODING));
} catch (UnsupportedEncodingException e) {
throw new RepositoryException(QValueFactoryImpl.DEFAULT_ENCODING + " is not supported encoding on this platform", e);
}
}
}
//--------------------------------------------------------< Inner Class >---
/**
* Extension for values of type {@link PropertyType#DATE}.
*/
private static class DateQValue extends QValueImpl {
private final String formattedStr;
private DateQValue(Calendar value) {
super(value, PropertyType.DATE);
formattedStr = ISO8601.format(value);
}
/**
* @return The formatted String of the internal Calendar value.
* @see QValue#getString()
* @see ISO8601#format(Calendar)
*/
public String getString() {
return formattedStr;
}
/**
* @param obj
* @return true if the given Object is a <code>DateQValue</code> with an
* equal String representation.
* @see Object#equals(Object)
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof DateQValue) {
DateQValue other = (DateQValue) obj;
return formattedStr.equals(other.formattedStr);
}
return false;
}
/**
* @return the hashCode of the formatted String of the Calender value.
* @see Object#hashCode()
*/
public int hashCode() {
return formattedStr.hashCode();
}
}
//--------------------------------------------------------< Inner Class >---
/**
* <code>BinaryQValue</code> represents a binary <code>Value</code> which is
* backed by a resource or byte[]. Unlike <code>BinaryValue</code> it has no
* state, i.e. the <code>getStream()</code> method always returns a fresh
* <code>InputStream</code> instance.
*/
private static class BinaryQValue implements QValue, Binary, Serializable {
/**
* empty array
*/
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
/**
* 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 <i>temporary</i> value
* whose dynamically allocated resources can be explicitly freed on
* {@link #discard()}.
*/
private transient boolean temp;
/**
* Buffer for small-sized data
*/
private byte[] buffer = BinaryQValue.EMPTY_BYTE_ARRAY;
/**
* Converted text
*/
private transient String text = null;
/**
* Creates a new <code>BinaryQValue</code> instance from an
* <code>InputStream</code>. 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}.
* <p/>
* The new instance represents a <i>temporary</i> value whose dynamically
* allocated resources will be freed explicitly on {@link #discard()}.
*
* @param in stream to be represented as a <code>BinaryQValue</code> instance
* @throws IOException if an error occurs while reading from the stream or
* writing to the temporary file
*/
private BinaryQValue(InputStream in) throws IOException {
this(in, true);
}
/**
* Creates a new <code>BinaryQValue</code> instance from an
* <code>InputStream</code>. 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}.
* <p/>
* The <code>temp</code> 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 <code>BinaryQValue</code> instance
* @param temp flag indicating whether this instance represents a
* <i>temporary</i> 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 BinaryQValue(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
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();
}
}
// init vars
file = spoolFile;
this.temp = temp;
// buffer is EMPTY_BYTE_ARRAY (default value)
}
/**
* Creates a new <code>BinaryQValue</code> instance from a
* <code>byte[]</code> array.
*
* @param bytes byte array to be represented as a <code>BinaryQValue</code>
* instance
*/
private BinaryQValue(byte[] bytes) {
buffer = bytes;
file = null;
// this instance is not backed by a temporarily allocated buffer
temp = false;
}
/**
* Creates a new <code>BinaryQValue</code> instance from a <code>File</code>.
*
* @param file file to be represented as a <code>BinaryQValue</code> instance
* @throws IOException if the file can not be read
*/
private BinaryQValue(File file) throws IOException {
String path = file.getCanonicalPath();
if (!file.isFile()) {
throw new IOException(path + ": the specified file does not exist");
}
if (!file.canRead()) {
throw new IOException(path + ": the specified file can not be read");
}
// this instance is backed by a 'real' file
this.file = file;
// this instance is not backed by temporarily allocated resource/buffer
temp = false;
// buffer is EMPTY_BYTE_ARRAY (default value)
}
//---------------------------------------------------------< QValue >---
/**
* @see QValue#getType()
*/
public int getType() {
return PropertyType.BINARY;
}
/**
* Returns the length of this <code>BinaryQValue</code>.
*
* @return The length, in bytes, of this <code>BinaryQValue</code>,
* or -1L if the length can't be determined.
* @see QValue#getLength()
*/
public long getLength() {
if (file != null) {
// this instance is backed by a 'real' file
if (file.exists()) {
return file.length();
} else {
return -1;
}
} else {
// this instance is backed by an in-memory buffer
return buffer.length;
}
}
/**
* @see QValue#getString()
*/
public String getString() throws RepositoryException {
if (text == null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
spool(out);
byte[] data = out.toByteArray();
text = new String(data, QValueFactoryImpl.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new RepositoryException(QValueFactoryImpl.DEFAULT_ENCODING
+ " not supported on this platform", e);
} catch (IOException e) {
throw new ValueFormatException("conversion from stream to string failed", e);
} finally {
try {
out.close();
} catch (IOException e) {
// ignore
}
}
}
return text;
}
/**
* @see QValue#getStream()
*/
public InputStream getStream() throws RepositoryException {
// 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()
*/
public Name getName() throws RepositoryException {
throw new UnsupportedOperationException();
}
/**
* @see QValue#getCalendar()
*/
public Calendar getCalendar() throws RepositoryException {
Calendar cal = ISO8601.parse(getString());
if (cal == null) {
throw new ValueFormatException("not a date string: " + getString());
} else {
return cal;
}
}
/**
* @see QValue#getDouble()
*/
public double getDouble() throws RepositoryException {
try {
return Double.parseDouble(getString());
} catch (NumberFormatException ex) {
throw new ValueFormatException(ex);
}
}
/**
* @see QValue#getLong()
*/
public long getLong() throws RepositoryException {
try {
return Long.parseLong(getString());
} catch (NumberFormatException ex) {
throw new ValueFormatException(ex);
}
}
/**
* @see QValue#getBoolean()
*/
public boolean getBoolean() throws RepositoryException {
return Boolean.valueOf(getString());
}
/**
* @see QValue#getPath()
*/
public Path getPath() throws RepositoryException {
throw new UnsupportedOperationException();
}
/**
* @see QValue#getDecimal()
*/
public BigDecimal getDecimal() throws RepositoryException {
try {
return new BigDecimal(getString());
} catch (NumberFormatException ex) {
throw new ValueFormatException(ex);
}
}
/**
* @see QValue#getURI()
*/
public URI getURI() throws RepositoryException {
try {
return new URI(getString());
} catch (URISyntaxException ex) {
throw new ValueFormatException(ex);
}
}
/**
* @see QValue#getBinary()
*/
public Binary getBinary() throws RepositoryException {
return this;
}
/**
* Frees temporarily allocated resources such as temporary file, buffer, etc.
* If this <code>BinaryQValue</code> is backed by a persistent resource
* calling this method will have no effect.
* @see QValue#discard()
*/
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;
}
}
public void dispose() {
discard();
}
//-----------------------------------------------< java.lang.Object >---
/**
* Returns a string representation of this <code>BinaryQValue</code>
* 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 <code>BinaryQValue</code> instance.
*/
public String toString() {
if (file != null) {
// this instance is backed by a 'real' file
return file.toString();
} else {
// this instance is backed by an in-memory buffer
return buffer.toString();
}
}
/**
* {@inheritDoc}
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof BinaryQValue) {
BinaryQValue other = (BinaryQValue) obj;
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()
*/
public int hashCode() {
return 0;
}
//----------------------------------------------------------------------
/**
* Spools the contents of this <code>BinaryQValue</code> to the given
* output stream.
*
* @param out output stream
* @throws RepositoryException if the input stream for this
* <code>BinaryQValue</code> could not be obtained
* @throws IOException if an error occurs while while spooling
*/
private void spool(OutputStream out) throws RepositoryException, IOException {
InputStream in;
if (file != null) {
// this instance is backed by a 'real' file
try {
in = new FileInputStream(file);
} catch (FileNotFoundException fnfe) {
throw new RepositoryException("file backing binary value not found",
fnfe);
}
} else {
// this instance is backed by an in-memory buffer
in = new ByteArrayInputStream(buffer);
}
try {
byte[] buffer = new byte[0x2000];
int read;
while ((read = in.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
} finally {
try {
in.close();
} catch (IOException ignore) {
}
}
}
//-----------------------------< javx.jcr.Binary >----------------------
/**
* {@inheritDoc}
*/
public int read(byte[] b, long position) throws IOException, RepositoryException {
if (file != null) {
// this instance is backed by a temp file
RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(position);
return raf.read(b);
} else {
// this instance is backed by an in-memory buffer
int length = Math.min(b.length, buffer.length - (int) position);
if (length > 0) {
System.arraycopy(buffer, (int) position, b, 0, length);
return length;
} else {
return -1;
}
}
}
/**
* {@inheritDoc}
*/
public long getSize() throws RepositoryException {
return getLength();
}
//-----------------------------< 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;
}
}
}