/* * 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.util.Base64; import org.apache.jackrabbit.util.TransientFileFactory; import org.apache.jackrabbit.value.ValueHelper; import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; import org.apache.jackrabbit.spi.commons.value.ValueFormat; 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 javax.jcr.ValueFactory; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; 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); /** * The maximum size for buffering data in memory. */ private static final int MAX_BUFFER_SIZE = 0x10000; /** * The in-memory buffer. */ private StringWriter buffer; /** * The number of characters written so far. * If the in-memory buffer is used, this is position within buffer (size of actual data in buffer) */ private long length; /** * Backing temporary file created when size of data exceeds * MAX_BUFFER_SIZE. */ private File tmpFile; /** * Writer used to write to tmpFile. */ private Writer writer; private final NamePathResolver nsContext; private final ValueFactory valueFactory; /** * Whether the value is base64 encoded. */ private boolean base64; /** * Constructs a new empty <code>BufferedStringValue</code>. * @param nsContext */ protected BufferedStringValue(NamePathResolver nsContext, ValueFactory valueFactory) { buffer = new StringWriter(); length = 0; tmpFile = null; writer = null; this.nsContext = nsContext; this.valueFactory = valueFactory; } /** * 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 { return length; } private String retrieveString() throws IOException { String value = retrieve(); if (base64) { ByteArrayOutputStream out = new ByteArrayOutputStream(); Base64.decode(value, out); value = new String(out.toByteArray(), "UTF-8"); } return value; } /** * 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 buffer.toString(); } else if (tmpFile != null) { // close writer first writer.close(); if (tmpFile.length() > Integer.MAX_VALUE) { throw new IOException("size of value is too big, use reader()"); } StringBuilder sb = new StringBuilder((int) length); char[] chunk = new char[0x2000]; Reader reader = openReader(); try { int read; 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"); } } private Reader openReader() throws IOException { return new InputStreamReader( new BufferedInputStream(new FileInputStream(tmpFile)), "UTF-8"); } /** * 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(retrieve()); } else if (tmpFile != null) { // close writer first writer.close(); return openReader(); } 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 len the number of characters to append * @throws IOException if an I/O error occurs */ public void append(char[] chars, int start, int len) throws IOException { if (buffer != null) { if (this.length + len > 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); BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(tmpFile)); writer = new OutputStreamWriter(fout, "UTF-8"); writer.write(buffer.toString()); writer.write(chars, start, len); // reset the in-memory buffer buffer = null; } else { buffer.write(chars, start, len); } } else if (tmpFile != null) { writer.write(chars, start, len); } else { throw new IOException("this instance has already been disposed"); } length += len; } /** * 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, NamePathResolver 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, valueFactory), nsContext); // convert InternalValue to Value using this // session's namespace mappings return ValueFormat.getJCRValue(ival, resolver, valueFactory); } else if (targetType == PropertyType.BINARY) { if (length() < 0x10000) { // < 65kb: deserialize BINARY type using String return ValueHelper.deserialize(retrieve(), targetType, false, valueFactory); } else { // >= 65kb: deserialize BINARY type using Reader Reader reader = reader(); try { return ValueHelper.deserialize(reader, targetType, false, valueFactory); } finally { reader.close(); } } } else { // all other types return ValueHelper.deserialize(retrieveString(), targetType, false, valueFactory); } } 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 temporary file Base64ReaderInputStream in = new Base64ReaderInputStream(reader()); return InternalValue.createTemporary(in); } } else { // convert serialized value to InternalValue using // current namespace context of xml document return InternalValue.create(ValueHelper.convert( retrieveString(), type, valueFactory), nsContext); } } catch (IOException e) { throw new RepositoryException("Error accessing property value", e); } } /** * This class converts the text read Converts a base64 reader to an input stream. */ private static class Base64ReaderInputStream extends InputStream { private static final int BUFFER_SIZE = 1024; private final char[] chars; private final ByteArrayOutputStream out; private final Reader reader; private int pos; private int remaining; private byte[] buffer; public Base64ReaderInputStream(Reader reader) { chars = new char[BUFFER_SIZE]; this.reader = reader; out = new ByteArrayOutputStream(BUFFER_SIZE); } private void fillBuffer() throws IOException { int len = reader.read(chars, 0, BUFFER_SIZE); if (len < 0) { remaining = -1; return; } Base64.decode(chars, 0, len, out); buffer = out.toByteArray(); pos = 0; remaining = buffer.length; out.reset(); } public int read() throws IOException { if (remaining == 0) { fillBuffer(); } if (remaining < 0) { return -1; } remaining--; return buffer[pos++] & 0xff; } } /** * {@inheritDoc} */ public void dispose() { if (buffer != null) { buffer = null; } 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"); } } /** * Whether this value is base64 encoded * * @param base64 the flag */ public void setBase64(boolean base64) { this.base64 = base64; } }