/* * 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.TransientFileFactory; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; 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; /** * <code>QValueFactoryImpl</code>... */ public class QValueFactoryImpl extends AbstractQValueFactory { private static final QValueFactory INSTANCE = new QValueFactoryImpl(); protected QValueFactoryImpl() { } public static QValueFactory getInstance() { return INSTANCE; } /** * @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>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 extends AbstractQValue implements Serializable { /** * A dummy value for calling the constructor of AbstractQValue */ private static final Object DUMMY_VALUE = new Serializable() { private static final long serialVersionUID = 2849470089518940117L; }; /** * 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; /** * 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 { super(DUMMY_VALUE, PropertyType.BINARY); 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) { super(DUMMY_VALUE, PropertyType.BINARY); 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 { super(DUMMY_VALUE, PropertyType.BINARY); 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 >--- /** * 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() */ @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 { // this instance is backed by an in-memory buffer return buffer.length; } } /** * @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() */ @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 <code>BinaryQValue</code> 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; } } //-----------------------------------------------< 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. */ @Override 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} */ @Override 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() */ @Override public int hashCode() { return 0; } //---------------------------------------------------< 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; } } }