/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.util;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.http.Headers;
import org.orbeon.oxf.xml.XMLConstants;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import java.io.IOException;
import java.io.OutputStream;
/**
* An OutputStream that converts the bytes written into it into Base64-encoded characters written to
* a ContentHandler.
*/
public class ContentHandlerOutputStream extends OutputStream {
private static final String DEFAULT_BINARY_DOCUMENT_ELEMENT = "document";
private final ContentHandler contentHandler;
private final boolean doStartEndDocument;
private final byte[] byteBuffer = new byte[76 * 3 / 4]; // maximum bytes that, once decoded, can fit in a line of 76 characters
private int currentBufferSize = 0;
private final char[] resultingLine = new char[76 + 1];
private byte[] singleByte = new byte[1];
private String contentType;
private String statusCode;
private boolean documentStarted;
private boolean closed;
public ContentHandlerOutputStream(ContentHandler contentHandler, boolean doStartEndDocument) {
this.contentHandler = contentHandler;
this.doStartEndDocument = doStartEndDocument;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void setStatusCode(String statusCode) {
this.statusCode = statusCode;
}
private void outputStartIfNeeded() throws SAXException {
if (doStartEndDocument && ! documentStarted) {
// Start document
AttributesImpl attributes = new AttributesImpl();
attributes.addAttribute(XMLConstants.XSI_URI, "type", "xsi:type", "CDATA", XMLConstants.XS_BASE64BINARY_QNAME.getQualifiedName());
if (contentType != null)
attributes.addAttribute("", Headers.ContentTypeLower(), Headers.ContentTypeLower(), "CDATA", contentType);
if (statusCode != null)
attributes.addAttribute("", "status-code", "status-code", "CDATA", statusCode);
contentHandler.startDocument();
contentHandler.startPrefixMapping(XMLConstants.XSI_PREFIX, XMLConstants.XSI_URI);
contentHandler.startPrefixMapping(XMLConstants.XSD_PREFIX, XMLConstants.XSD_URI);
contentHandler.startElement("", DEFAULT_BINARY_DOCUMENT_ELEMENT, DEFAULT_BINARY_DOCUMENT_ELEMENT, attributes);
documentStarted = true;
}
}
private void outputEndIfNeeded() {
if (doStartEndDocument && documentStarted) {
try {
// End document
contentHandler.endElement("", DEFAULT_BINARY_DOCUMENT_ELEMENT, DEFAULT_BINARY_DOCUMENT_ELEMENT);
contentHandler.endPrefixMapping(XMLConstants.XSI_PREFIX);
contentHandler.endPrefixMapping(XMLConstants.XSD_PREFIX);
contentHandler.endDocument();
} catch (SAXException e) {
throw new OXFException(e);
}
}
}
/**
* Close this output stream. This must be called in the end if startDocument() was called, otherwise the document
* won't be properly produced.
*
* @throws IOException
*/
public void close() throws IOException {
if (!closed) {
// Always flush
flushBuffer();
// Only close element and document if startDocument was called
outputEndIfNeeded();
closed = true;
}
}
public void flush() throws IOException {
if (!closed) {
try {
// NOTE: This will only flush on Base64 line boundaries. Is that what we want? Or
// should we just ignore? We can't output an incomplete encoded line unless we have a
// number of bytes in the buffer multiple of 3. Otherwise, we would have to output '='
// characters, which do signal an end of transmission.
contentHandler.processingInstruction("orbeon-serializer", "flush");
} catch (SAXException e) {
throw new OXFException(e);
}
}
}
public void write(byte b[]) throws IOException {
addBytes(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
addBytes(b, off, len);
}
public void write(int b) throws IOException {
singleByte[0] = (byte) b;
addBytes(singleByte, 0, 1);
}
private void addBytes(byte b[], int off, int len) throws IOException {
if (closed)
throw new IOException("ContentHandlerOutputStream already closed");
// Check bounds
if ((off < 0) || (len < 0) || (off > b.length) || ((off + len) > b.length))
throw new IndexOutOfBoundsException();
else if (len == 0)
return;
try {
outputStartIfNeeded();
while (len > 0) {
// Fill buffer as much as possible
int lenToCopy = Math.min(len, byteBuffer.length - currentBufferSize);
System.arraycopy(b, off, byteBuffer, currentBufferSize, lenToCopy);
off += lenToCopy;
len -= lenToCopy;
currentBufferSize += lenToCopy;
// If buffer is full, write it out
if (currentBufferSize == byteBuffer.length) {
String encoded = Base64.encode(byteBuffer, true) + "\n";
// The terminating LF is already added by encode()
encoded.getChars(0, encoded.length(), resultingLine, 0);
// Output characters
contentHandler.characters(resultingLine, 0, encoded.length());
// Reset counter
currentBufferSize = 0;
}
}
} catch (SAXException e) {
throw new OXFException(e);
}
}
private void flushBuffer() {
if (currentBufferSize > 0) {
try {
outputStartIfNeeded();
byte[] tempBuf = new byte[currentBufferSize];
System.arraycopy(byteBuffer, 0, tempBuf, 0, currentBufferSize);
String encoded = Base64.encode(tempBuf, true);
encoded.getChars(0, encoded.length(), resultingLine, 0);
// Output characters
contentHandler.characters(resultingLine, 0, encoded.length());
} catch (SAXException e) {
throw new OXFException(e);
}
// Reset counter
currentBufferSize = 0;
}
}
}