/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2009 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.syncml.client;
import java.io.IOException;
import java.io.OutputStream;
import java.io.ByteArrayInputStream;
import com.funambol.util.Log;
import com.funambol.util.Base64;
import com.funambol.util.StringUtil;
/**
* This class represents a stream into which a file object can be written
* directly. It is responsibility of this stream to separate the meta
* information from the real file content. The user can simply stream bytes as
* they arrive from a file object source into this output stream. When the "body"
* of the item starts, then its content is written into an output stream that
* must be provided to this class. After the stream is closed, the user can ask
* for a FileObject which contains all the file object meta data.
*/
public class FileObjectOutputStream extends OutputStream {
private static final String TAG_LOG = "FileObjectOutputStream";
private static final int B64BUFFER_SIZE = 4;
private StringBuffer currentToken = null;
private StringBuffer buffer = new StringBuffer();
private OutputStream os;
private boolean content = false;
private byte[] b64Buffer = new byte[B64BUFFER_SIZE];
private int b64Idx = 0;
private FileObject fo;
private boolean prologue = true;
private boolean body = false;
private boolean epilogue = false;
private boolean invalidObject = false;
/**
* Builds the output stream.
* @param fo is the file object which is filled with the meta information
* which comes into the stream
* @param os is the output stream into which the file object body is written
* into
*/
public FileObjectOutputStream(FileObject fo, OutputStream os) {
this.os = os;
this.fo = fo;
}
/**
* Writes a single byte into the stream. If the body tag has not been found
* yet, then the info is kept as meta information. Once the body is
* encountered, then all the subsequent bytes are written into the real
* output stream. If the content is base64 encoded, then the method performs
* also decoding.
* The body tag is recognized by the FileObject parser, which parses the
* information received so far and it extracts the body content. This makes
* the parsing mechanism safe.
*
* @param b the byte write
* @throws IOException if the operation cannot be performed. This can be due
* to several reasons, including a failure of the underlying output stream
* or an invalid file object content that cannot be parsed.
*/
public void write(int b) throws IOException {
if (prologue) {
buffer.append((char)b);
if (buffer.length() == 1024) {
ByteArrayInputStream bis = new ByteArrayInputStream(buffer.toString().getBytes());
try {
String bod = fo.parsePrologue(bis);
// Note that if the body cannot be found, or it contains
// part of the epilogue, then we do not parse anything here
// because the item is small enough to be parsed entirely in
// the close method
if (bod != null) {
// We shall write the first part of the body
// but first we must decode it (if the body is b64)
if (fo.isBodyBase64()) {
int bodyBytesSize = (bod.length() / 4) * 4;
byte bodyBytes[] = bod.getBytes();
byte tBuf[] = new byte[bodyBytesSize];
for(int i=0;i<bodyBytesSize;++i) {
tBuf[i] = bodyBytes[i];
}
byte tBuf2[] = Base64.decode(tBuf);
os.write(tBuf2);
b64Idx = 0;
// The remainder must be copied into the b64 buffer
for(int i=0;i<bod.length() % 4;++i) {
b64Buffer[b64Idx++] = bodyBytes[bodyBytesSize + i];
}
} else {
os.write(bod.getBytes());
}
body = true;
prologue = false;
}
} catch (Exception e) {
invalidObject = true;
Log.error(TAG_LOG, "Error parsing file object prologue: ", e);
throw new IOException("Error parsing file object epilogue: " + e.toString());
}
}
} else if (body) {
if (b == '<') {
// If we finished the body we may have few bytes left behind
if (b64Idx > 0) {
byte tBuf[] = new byte[b64Idx];
for(int i=0;i<b64Idx;++i) {
tBuf[i] = b64Buffer[i];
}
tBuf = Base64.decode(tBuf);
os.write(tBuf);
b64Idx = 0;
}
body = false;
epilogue = true;
buffer = new StringBuffer();
buffer.append((char)b);
} else {
// We must make decodable chunks (size must be multiple of 4)
// if the body is base64
if (fo.isBodyBase64()) {
b64Buffer[b64Idx++] = (byte)b;
if (b64Idx == B64BUFFER_SIZE) {
byte tBuf[] = Base64.decode(b64Buffer);
os.write(tBuf);
b64Idx = 0;
}
} else {
os.write(b);
}
}
} else {
buffer.append((char)b);
}
}
/**
* Close the output stream. When the stream is closed, the method makes sure
* that no pending bytes are present. If there are, then the body and the
* FileObject may get updated. After the stream has been closed, it is safe
* to read the FileObject.
*
* @throws IOException if the operation cannot be performed. Either because
* the underlying output stream has a failure or because the the meta
* information cannot be parsed properly.
*/
public void close() throws IOException {
if (epilogue) {
os.close();
if (!invalidObject) {
try {
fo.parseEpilogue(buffer.toString());
} catch (Exception e) {
Log.error(TAG_LOG, "Error parsing file object epilogue: ", e);
throw new IOException("Error parsing file object epilogue: " + e.toString());
}
}
} else if (prologue) {
// It is possible the entire file object fits in 1024 bytes,
// so we can try to parse everything
try {
if (!invalidObject) {
String obj = buffer.toString();
ByteArrayInputStream bis = new ByteArrayInputStream(obj.getBytes());
String bod = fo.parse(bis);
if (bod != null) {
if (fo.isBodyBase64()) {
byte tBuf[] = Base64.decode(bod.getBytes());
os.write(tBuf);
} else {
os.write(bod.getBytes());
}
os.close();
} else {
// The file has an empty content
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Received an emtpy file");
}
}
} else {
os.close();
}
} catch (Exception e) {
Log.error(TAG_LOG, "Error while parsing file object: ", e);
throw new IOException("Error parsing file object epilogue: " + e.toString());
}
} else {
os.close();
// The stream is interrupted in the middle of the
// body or the prologue. We shall throw an exception to signal that this
// item is invalid
throw new IOException("Incomplete file object item");
}
}
/**
* Flushes the underlying stream.
*/
public void flush() throws IOException {
os.flush();
}
/**
* Get the file object descrption (meta information). It is safe to invoke
* this method only after the stream was closed. If writing or closing
* generated an exception, then the content of the returned object may be
* inaccurate.
*/
public FileObject getFileObject() {
return fo;
}
}