/*
* 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.util.Date;
import java.util.Calendar;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParser;
import com.funambol.org.kxml2.io.KXmlParser;
import com.funambol.util.DateUtil;
import com.funambol.util.StringUtil;
import com.funambol.util.Log;
/**
* This class represents a FileObject which is a file with its meta information
* as defined by OMA (see OMA File Data Object Specification for more details).
* This class actually stores the meta information, while the actual content is
* not part of it. The reason is that in general the content of a file cannot be
* kept in memory, so the class could just store a URL or a stream, but at the
* moment this is not even supported as this need is not forseen.
* Beside storing the file object meta information, this class has also the
* ability to parse/format an item (at least its meta info). For this purpose
* there are two sets of methods: <br>
*
* parsePrologue, parseEpilogue and parse <br>
* formatPrologue and formatEpilogue
*
* Parsing is split between the prologue (everything up to the body content
* [escluded]) and the epilogue (everything after the body content). If the
* item is known to be small, then it can be parsed in one shot via the parse
* method.
* Formatting is als split between the prologue and the epilogue
*/
public class FileObject {
private static final String FILE_TAG = "File";
private static final String NAME_TAG = "name";
private static final String BODY_TAG = "body";
private static final String CREATED_TAG = "created";
private static final String MODIFIED_TAG = "modified";
private static final String ACCESSED_TAG = "accessed";
private static final String SIZE_TAG = "size";
private static final String ATTRIBUTES_TAG = "attributes";
private static final String HIDDEN_TAG = "h";
private static final String SYSTEM_TAG = "s";
private static final String ARCHIVED_TAG = "a";
private static final String DELETED_TAG = "d";
private static final String WRITABLE_TAG = "w";
private static final String READABLE_TAG = "r";
private static final String EXECUTABLE_TAG = "x";
private static final String ENC_ATTR = "enc";
private static final String BASE64_ENC = "base64";
private static final String TRUE = "TRUE";
private static final String FALSE = "FALSE";
private String name = null;
private Date modified = null;
private Date created = null;
private Date accessed = null;
private int size = -1;
private boolean hidden = false;
private boolean system = false;
private boolean archived = false;
private boolean deleted = false;
private boolean writable = false;
private boolean readable = false;
private boolean executable = false;
private boolean bodyBase64 = false;
private String fileTagName = FILE_TAG;
private String bodyTagName = BODY_TAG;
public FileObject() {
}
/**
* Sets the file name
* @param name the name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the file name
*/
public String getName() {
return name;
}
/**
* Gets the modification time
*/
public Date getModified() {
return modified;
}
/**
* Sets the modification time
*/
public void setModified(Date modified) {
this.modified = modified;
}
/**
* Sets the creation time
*/
public void setCreated(Date created) {
this.created = created;
}
/**
* Gets the creation time
*/
public Date getCreated() {
return created;
}
/**
* Sets the last accessed time
*/
public void setAccessed(Date accessed) {
this.accessed = accessed;
}
/**
* Gets the last accessed time
*/
public Date getAccessed() {
return accessed;
}
/**
* Sets the hidden attribute
*/
public void setHidden(boolean h) {
hidden = h;
}
/**
* Gets the hidden attribute
*/
public boolean getHidden() {
return hidden;
}
/**
* Sets the system attribute
*/
public void setSystem(boolean s) {
system = s;
}
/**
* Gets the system attribute
*/
public boolean getSystem() {
return system;
}
/**
* Sets the archived attribute
*/
public void setArchived(boolean a) {
archived = a;
}
/**
* Gets the archived attribute
*/
public boolean getArchived() {
return archived;
}
/**
* Sets the deleted attribute
*/
public void setDeleted(boolean d) {
deleted = d;
}
/**
* Gets the deleted attribute
*/
public boolean getDeleted() {
return deleted;
}
/**
* Sets the writable attribute
*/
public void setWritable(boolean w) {
writable = w;
}
/**
* Gets the writable attribute
*/
public boolean getWritable() {
return writable;
}
/**
* Sets the readable attribute
*/
public void setReadable(boolean r) {
readable = r;
}
/**
* Gets the readable attribute
*/
public boolean getReadable() {
return readable;
}
/**
* Sets the executable attribute
*/
public void setExecutable(boolean e) {
executable = e;
}
/**
* Gets the executable attribute
*/
public boolean getExecutable() {
return executable;
}
/**
* Sets the file size
*/
public void setSize(int size) {
this.size = size;
}
/**
* Gets the file size. This attribute is not mandatory and if the
* information is not available, -1 is returned.
*/
public int getSize() {
return size;
}
/**
* Returns true if the body is encoded in base64 (if not it is assumed to be
* plain text)
*/
public boolean isBodyBase64() {
return bodyBase64;
}
/**
* Formats the prologue of this file object. The prologue is everything from
* the File tag up to the body content (excluded)
*
* @return the formatted prologue
*/
public String formatPrologue() {
return formatPrologue(true);
}
/**
* Formats the prologue of this file object. The prologue is everything from
* the File tag up to the body content (excluded)
*
* @param formatBody specifies if the body tag must be formatted
*
* @return the formatted prologue
*/
public String formatPrologue(boolean formatBody) {
StringBuffer buf = new StringBuffer();
formatStartTag(buf, FILE_TAG);
buf.append("\n");
if (name != null) {
formatCompleteTag(buf, NAME_TAG, name);
}
if (modified != null) {
String mod = DateUtil.formatDateTimeUTC(modified);
formatCompleteTag(buf, MODIFIED_TAG, mod);
}
if (created != null) {
String cre = DateUtil.formatDateTimeUTC(created);
formatCompleteTag(buf, CREATED_TAG, cre);
}
if (accessed != null) {
String acc = DateUtil.formatDateTimeUTC(accessed);
formatCompleteTag(buf, ACCESSED_TAG, acc);
}
// Format all the attributes
formatStartTag(buf, ATTRIBUTES_TAG);
buf.append("\n");
formatCompleteTag(buf, HIDDEN_TAG, (hidden ? TRUE : FALSE));
formatCompleteTag(buf, SYSTEM_TAG, (system ? TRUE : FALSE));
formatCompleteTag(buf, ARCHIVED_TAG, (archived ? TRUE : FALSE));
formatCompleteTag(buf, DELETED_TAG, (deleted ? TRUE : FALSE));
formatCompleteTag(buf, WRITABLE_TAG, (writable ? TRUE : FALSE));
formatCompleteTag(buf, READABLE_TAG, (readable ? TRUE : FALSE));
formatCompleteTag(buf, EXECUTABLE_TAG, (executable ? TRUE : FALSE));
formatEndTag(buf, ATTRIBUTES_TAG);
buf.append("\n");
// Format the size
if (size != -1) {
formatCompleteTag(buf, SIZE_TAG, ""+size);
}
// Format the body tag if required
if (formatBody) {
buf.append("<").append(BODY_TAG).append(" enc=\"base64\">");
}
return buf.toString();
}
/**
* Formats the epilogue of this file object. The epilogue is everything
* after the body content (excluded)
*
* @return the formatted epilogue
*/
public String formatEpilogue() {
return formatEpilogue(true);
}
/**
* Formats the epilogue of this file object. The epilogue is everything
* after the body content (excluded)
*
* @param formatBody specifies if the body tag must be formatted
*
* @return the formatted epilogue
*/
public String formatEpilogue(boolean formatBody) {
StringBuffer buf = new StringBuffer();
if (formatBody) {
formatEndTag(buf, BODY_TAG);
buf.append("\n");
}
formatEndTag(buf, FILE_TAG);
return buf.toString();
}
/**
* Parses the prologue of a file object. This method checks the syntax of
* the item and it stops its analysis once it finds the body tag. During the
* analysis it sets the memebers of this file object accordingly to the file
* object properties.
* If the item contains more than then body (at least the body closure tag)
* then the method returns null, as the user shall use the parse method to
* parse the entire item in one shot.
*
* @param is the input stream representing the input
* @return the body content contained in the input stream. In other words
* the analysis scans all the input stream and returns everything comes
* after the body tag. The method may return null if the body is not found
* or the there is no body content.
* @throws FileObjectException if the parsing fail for any reason
*/
public String parsePrologue(InputStream is) throws FileObjectException {
KXmlParser parser = new KXmlParser();
try {
parser.setInput(is, "UTF-8");
// the first token must be the file tag
parser.next();
require(parser, parser.START_TAG, null, FILE_TAG);
fileTagName = parser.getName();
// parse the rest of the xml
return parseFile(parser, true);
} catch (FileObjectException foe) {
Log.error("Error parsing FileObject: " + foe.toString());
throw foe;
} catch (Exception e) {
Log.error("Error parsing FileObject: " + e.toString());
throw new FileObjectException("Cannot parse file object: " + e.toString());
}
}
/**
* Parses the epilogue of a file object. The epilogue is everything after
* the body content (starting with the body closure tag)
*/
public void parseEpilogue(String epilogue) throws FileObjectException {
KXmlParser parser = new KXmlParser();
// The epilogue does not have the opening tags, and this will make the
// parser fails, so we add them here
StringBuffer ep = new StringBuffer();
ep.append("<").append(fileTagName).append(">").
append("<").append(bodyTagName).append(">").append(epilogue);
epilogue = ep.toString();
ByteArrayInputStream bis = new ByteArrayInputStream(epilogue.getBytes());
try {
parser.setInput(bis, "UTF-8");
// the first token must be body tag closure (unless the body was
// empty)
// the first token must be the file tag
parser.next();
require(parser, parser.START_TAG, null, FILE_TAG);
// the second token must be the body tag
nextSkipSpaces(parser);
require(parser, parser.START_TAG, null, BODY_TAG);
// Now we expect the body end tag
nextSkipSpaces(parser);
require(parser, parser.END_TAG, null, BODY_TAG);
// Now we can parse all the remaining items
parseFile(parser, false);
// the last token must be the file tag
require(parser, parser.END_TAG, null, FILE_TAG);
} catch (FileObjectException foe) {
Log.error("Error parsing FileObject: " + foe.toString());
throw foe;
} catch (Exception e) {
Log.error("Error parsing FileObject: " + e.toString());
throw new FileObjectException("Cannot parse file object: " + e.toString());
}
}
/**
* This method parses a file object which is readable entirely from the
* input stream. This method loads in the memory the entire item's body,
* so it must be used only when it is known that the item body is small
* enough.
*
* @param is the stream from which data is read
* @return the item body content
* @throws FileObjectException if any error occurs during the parsing
*/
public String parse(InputStream is) throws FileObjectException {
KXmlParser parser = new KXmlParser();
String body = null;
try {
parser.setInput(is, "UTF-8");
// the first token must be the file tag
parser.next();
require(parser, parser.START_TAG, null, FILE_TAG);
// parse the rest of the xml
body = parseFile(parser, false);
// Now we expect the body end tag
nextSkipSpaces(parser);
require(parser, parser.END_TAG, null, BODY_TAG);
// Now we can parse all the remaining items
parseFile(parser, false);
// the last token must be the file tag
require(parser, parser.END_TAG, null, FILE_TAG);
} catch (FileObjectException foe) {
Log.error("Error parsing FileObject: " + foe.toString());
throw foe;
} catch (Exception e) {
Log.error("Error parsing FileObject: " + e.toString());
throw new FileObjectException("Cannot parse file object: " + e.toString());
}
return body;
}
private String parseFile(XmlPullParser parser, boolean checkBodyInterrupted)
throws XmlPullParserException,
IOException,
FileObjectException
{
// Scan until we find the BODY
boolean bodyFound = false;
String bodyText = null;
nextSkipSpaces(parser);
while (parser.getEventType() == parser.START_TAG) {
String tagName = parser.getName();
if (StringUtil.equalsIgnoreCase(NAME_TAG,tagName)) {
parseName(parser);
} else if (StringUtil.equalsIgnoreCase(CREATED_TAG,tagName)) {
parseCreated(parser);
} else if (StringUtil.equalsIgnoreCase(MODIFIED_TAG,tagName)) {
parseModified(parser);
} else if (StringUtil.equalsIgnoreCase(ACCESSED_TAG,tagName)) {
parseAccessed(parser);
} else if (StringUtil.equalsIgnoreCase(SIZE_TAG,tagName)) {
parseSize(parser);
} else if (StringUtil.equalsIgnoreCase(ATTRIBUTES_TAG,tagName)) {
parseAttributes(parser);
} else if (StringUtil.equalsIgnoreCase(BODY_TAG,tagName)) {
bodyTagName = parser.getName();
// Check if the body is base64
int numAttributes = parser.getAttributeCount();
for(int i=0;i<numAttributes;++i) {
String attribute = parser.getAttributeName(i);
if (StringUtil.equalsIgnoreCase(attribute, ENC_ATTR)) {
String value = parser.getAttributeValue(i);
if (StringUtil.equalsIgnoreCase(value, BASE64_ENC)) {
bodyBase64 = true;
}
}
}
// Get the first part of the body
parser.next();
bodyText = parser.getText();
bodyFound = true;
if (checkBodyInterrupted) {
// Check that we are at the end of the document. If we are not,
// then return null
try {
parser.next();
if (parser.getEventType() != parser.END_DOCUMENT) {
bodyText = null;
}
} catch (XmlPullParserException xppe) {
// This exception is thrown if the closure tag is not
// complete. we interpret this as we have part of the
// closure tag
bodyText = null;
}
}
break;
} else {
Log.error("Unknown token: " + tagName);
throw new FileObjectException("Unknown token: " + tagName);
}
nextSkipSpaces(parser);
}
return bodyText;
}
private void parseName(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
name = parser.getText();
parser.next();
require(parser, parser.END_TAG, null, NAME_TAG);
}
private void parseCreated(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
created = parseDate(parser.getText());
parser.next();
require(parser, parser.END_TAG, null, CREATED_TAG);
}
private void parseModified(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
modified = parseDate(parser.getText());
parser.next();
require(parser, parser.END_TAG, null, MODIFIED_TAG);
}
private void parseAccessed(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
accessed = parseDate(parser.getText());
parser.next();
require(parser, parser.END_TAG, null, ACCESSED_TAG);
}
private void parseSize(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
size = parseInt(parser.getText());
parser.next();
require(parser, parser.END_TAG, null, SIZE_TAG);
}
private void parseAttributes(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
// If we have text here, we check it is only whitespaces or CR
nextSkipSpaces(parser);
while (parser.getEventType() == parser.START_TAG) {
String tagName = parser.getName();
if (StringUtil.equalsIgnoreCase(HIDDEN_TAG,tagName)) {
parseHidden(parser);
} else if (StringUtil.equalsIgnoreCase(SYSTEM_TAG,tagName)) {
parseSystem(parser);
} else if (StringUtil.equalsIgnoreCase(DELETED_TAG,tagName)) {
parseDeleted(parser);
} else if (StringUtil.equalsIgnoreCase(ARCHIVED_TAG,tagName)) {
parseArchived(parser);
} else if (StringUtil.equalsIgnoreCase(WRITABLE_TAG,tagName)) {
parseWritable(parser);
} else if (StringUtil.equalsIgnoreCase(READABLE_TAG,tagName)) {
parseReadable(parser);
} else if (StringUtil.equalsIgnoreCase(EXECUTABLE_TAG,tagName)) {
parseExecutable(parser);
} else {
throw new FileObjectException("Unknown attribute in file object " + tagName);
}
// If we have text here, we check it is only whitespaces or CR
nextSkipSpaces(parser);
}
require(parser, parser.END_TAG, null, ATTRIBUTES_TAG);
}
private void nextSkipSpaces(XmlPullParser parser) throws FileObjectException,
XmlPullParserException,
IOException {
int eventType = parser.next();
if (eventType == parser.TEXT) {
if (!parser.isWhitespace()) {
Log.error("Unexpected text: " + parser.getText());
throw new FileObjectException("Unexpected text: " + parser.getText());
}
parser.next();
}
}
private void parseHidden(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
hidden = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, HIDDEN_TAG);
}
private void parseSystem(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
system = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, SYSTEM_TAG);
}
private void parseDeleted(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
deleted = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, DELETED_TAG);
}
private void parseArchived(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
archived = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, ARCHIVED_TAG);
}
private void parseWritable(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
writable = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, WRITABLE_TAG);
}
private void parseReadable(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
readable = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, READABLE_TAG);
}
private void parseExecutable(XmlPullParser parser) throws XmlPullParserException,
IOException,
FileObjectException {
parser.next();
require(parser, parser.TEXT, null, null);
executable = parseBoolean(parser.getText());
parser.next();
require(parser,parser.END_TAG, null, EXECUTABLE_TAG);
}
private void require(XmlPullParser parser, int type, String namespace,
String name) throws XmlPullParserException
{
if (type != parser.getEventType()
|| (namespace != null && !StringUtil.equalsIgnoreCase(namespace,parser.getNamespace()))
|| (name != null && !StringUtil.equalsIgnoreCase(name,parser.getName())))
{
throw new XmlPullParserException("expected "+ parser.TYPES[ type ]+
parser.getPositionDescription());
}
}
private boolean parseBoolean(String value) throws FileObjectException {
boolean v;
if (value != null && StringUtil.equalsIgnoreCase(value, "true")) {
v = true;
} else if (value != null && StringUtil.equalsIgnoreCase(value, "false")) {
v = false;
} else {
throw new FileObjectException("Expected boolean, found: " + value);
}
return v;
}
private Date parseDate(String value) throws FileObjectException {
try {
Calendar date = DateUtil.parseDateTime(value);
Date d = date.getTime();
return d;
} catch (Exception e) {
Log.error("Cannot parse date " + value + " " + e.toString());
throw new FileObjectException("Cannot parse date " + value);
}
}
private int parseInt(String value) throws FileObjectException {
int v;
try {
v = Integer.parseInt(value);
} catch (Exception e) {
Log.error("Cannot parse int value: " + value);
throw new FileObjectException("Cannot parse int " + value);
}
return v;
}
private void formatCompleteTag(StringBuffer buf, String tagName, String tagValue) {
buf.append("<").append(tagName).append(">")
.append(tagValue)
.append("</").append(tagName).append(">").append("\n");
}
private void formatStartTag(StringBuffer buf, String tagName) {
buf.append("<").append(tagName).append(">");
}
private void formatEndTag(StringBuffer buf, String tagName) {
buf.append("</").append(tagName).append(">");
}
}