blob: 17e9ca9f2b784dd5ce984cea59038e128df35214 [file] [log] [blame]
/*
* 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.core.xml;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.name.NamespaceResolver;
import org.apache.jackrabbit.util.Base64;
import org.apache.jackrabbit.util.TransientFileFactory;
import org.apache.jackrabbit.value.ValueHelper;
import org.apache.jackrabbit.value.ValueFactoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
/**
* <code>BufferedStringValue</code> represents an appendable
* serialized value that is either buffered in-memory or backed
* by a temporary file if its size exceeds a certain limit.
* <p/>
* <b>Important:</b> Note that in order to free resources
* <code>{@link #dispose()}</code> should be called as soon as
* <code>BufferedStringValue</code> instance is not used anymore.
*/
class BufferedStringValue implements TextValue {
private static Logger log = LoggerFactory.getLogger(BufferedStringValue.class);
/**
* max size for buffering data in memory
*/
private static final int MAX_BUFFER_SIZE = 0x10000;
/**
* size of increment if capacity buffer needs to be enlarged
*/
private static final int BUFFER_INCREMENT = 0x2000;
/**
* in-memory buffer
*/
private char[] buffer;
/**
* current position within buffer (size of actual data in buffer)
*/
private int bufferPos;
/**
* backing temporary file created when size of data exceeds
* MAX_BUFFER_SIZE
*/
private File tmpFile;
/**
* writer used to write to tmpFile; writer & tmpFile are always
* instantiated together, i.e. they are either both null or both not null.
*/
private Writer writer;
private final NamespaceResolver nsContext;
/**
* Constructs a new empty <code>BufferedStringValue</code>.
*/
protected BufferedStringValue(NamespaceResolver nsContext) {
buffer = new char[0x2000];
bufferPos = 0;
tmpFile = null;
writer = null;
this.nsContext = nsContext;
}
/**
* Returns the length of the serialized value.
*
* @return the length of the serialized value
* @throws IOException if an I/O error occurs
*/
public long length() throws IOException {
if (buffer != null) {
return bufferPos;
} else if (tmpFile != null) {
// flush writer first
writer.flush();
return tmpFile.length();
} else {
throw new IOException("this instance has already been disposed");
}
}
/**
* Retrieves the serialized value.
*
* @return the serialized value
* @throws IOException if an I/O error occurs
*/
public String retrieve() throws IOException {
if (buffer != null) {
return new String(buffer, 0, bufferPos);
} else if (tmpFile != null) {
// flush writer first
writer.flush();
if (tmpFile.length() > Integer.MAX_VALUE) {
throw new IOException("size of value is too big, use reader()");
}
StringBuffer sb = new StringBuffer((int) tmpFile.length());
char[] chunk = new char[0x2000];
int read;
Reader reader = new FileReader(tmpFile);
try {
while ((read = reader.read(chunk)) > -1) {
sb.append(chunk, 0, read);
}
} finally {
reader.close();
}
return sb.toString();
} else {
throw new IOException("this instance has already been disposed");
}
}
/**
* Returns a <code>Reader</code> for reading the serialized value.
*
* @return a <code>Reader</code> for reading the serialized value.
* @throws IOException if an I/O error occurs
*/
public Reader reader() throws IOException {
if (buffer != null) {
return new StringReader(new String(buffer, 0, bufferPos));
} else if (tmpFile != null) {
// flush writer first
writer.flush();
return new FileReader(tmpFile);
} else {
throw new IOException("this instance has already been disposed");
}
}
/**
* Append a portion of an array of characters.
*
* @param chars the characters to be appended
* @param start the index of the first character to append
* @param length the number of characters to append
* @throws IOException if an I/O error occurs
*/
public void append(char[] chars, int start, int length)
throws IOException {
if (buffer != null) {
if (bufferPos + length > MAX_BUFFER_SIZE) {
// threshold for keeping data in memory exceeded;
// create temp file and spool buffer contents
TransientFileFactory fileFactory = TransientFileFactory.getInstance();
tmpFile = fileFactory.createTransientFile("txt", null, null);
final FileOutputStream fout = new FileOutputStream(tmpFile);
writer = new OutputStreamWriter(fout) {
public void flush() throws IOException {
// flush this writer
super.flush();
// force synchronization with underlying file
fout.getFD().sync();
}
};
writer.write(buffer, 0, bufferPos);
writer.write(chars, start, length);
// reset fields
buffer = null;
bufferPos = 0;
} else {
if (bufferPos + length > buffer.length) {
// reallocate new buffer and spool old buffer contents
char[] newBuffer = new char[buffer.length + BUFFER_INCREMENT];
System.arraycopy(buffer, 0, newBuffer, 0, bufferPos);
buffer = newBuffer;
}
System.arraycopy(chars, start, buffer, bufferPos, length);
bufferPos += length;
}
} else if (tmpFile != null) {
writer.write(chars, start, length);
} else {
throw new IOException("this instance has already been disposed");
}
}
/**
* Close this value. Once a value has been closed,
* further append() invocations will cause an IOException to be thrown.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (buffer != null) {
// nop
} else if (tmpFile != null) {
writer.close();
} else {
throw new IOException("this instance has already been disposed");
}
}
//--------------------------------------------------------< TextValue >
public Value getValue(int targetType, NamespaceResolver resolver)
throws ValueFormatException, RepositoryException {
try {
if (targetType == PropertyType.NAME
|| targetType == PropertyType.PATH) {
// NAME and PATH require special treatment because
// they depend on the current namespace context
// of the xml document
// convert serialized value to InternalValue using
// current namespace context of xml document
InternalValue ival =
InternalValue.create(ValueHelper.convert(
retrieve(), targetType, ValueFactoryImpl.getInstance()), nsContext);
// convert InternalValue to Value using this
// session's namespace mappings
return ival.toJCRValue(resolver);
} else if (targetType == PropertyType.BINARY) {
if (length() < 0x10000) {
// < 65kb: deserialize BINARY type using String
return ValueHelper.deserialize(retrieve(), targetType, false, ValueFactoryImpl.getInstance());
} else {
// >= 65kb: deserialize BINARY type using Reader
Reader reader = reader();
try {
return ValueHelper.deserialize(reader, targetType, false, ValueFactoryImpl.getInstance());
} finally {
reader.close();
}
}
} else {
// all other types
return ValueHelper.deserialize(retrieve(), targetType, true, ValueFactoryImpl.getInstance());
}
} catch (IOException e) {
String msg = "failed to retrieve serialized value";
log.debug(msg, e);
throw new RepositoryException(msg, e);
}
}
public InternalValue getInternalValue(int type)
throws ValueFormatException, RepositoryException {
try {
if (type == PropertyType.BINARY) {
// base64 encoded BINARY type;
// decode using Reader
if (length() < 0x10000) {
// < 65kb: deserialize BINARY type in memory
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Base64.decode(retrieve(), baos);
// no need to close ByteArrayOutputStream
//baos.close();
return InternalValue.create(baos.toByteArray());
} else {
// >= 65kb: deserialize BINARY type
// using Reader and temporay file
TransientFileFactory fileFactory = TransientFileFactory.getInstance();
File tmpFile = fileFactory.createTransientFile("bin", null, null);
FileOutputStream out = new FileOutputStream(tmpFile);
Reader reader = reader();
try {
Base64.decode(reader, out);
} finally {
reader.close();
out.close();
}
return InternalValue.create(tmpFile);
}
} else {
// convert serialized value to InternalValue using
// current namespace context of xml document
return InternalValue.create(ValueHelper.convert(
retrieve(), type, ValueFactoryImpl.getInstance()), nsContext);
}
} catch (IOException e) {
throw new RepositoryException("Error accessing property value", e);
}
}
/**
* {@inheritDoc}
*/
public void dispose() {
if (buffer != null) {
buffer = null;
bufferPos = 0;
} else if (tmpFile != null) {
try {
writer.close();
tmpFile.delete();
tmpFile = null;
writer = null;
} catch (IOException e) {
log.warn("Problem disposing property value", e);
}
} else {
log.warn("this instance has already been disposed");
}
}
}