/*
* Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* 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 version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.j2me.pim.formats;
import com.sun.j2me.pim.ContactImpl;
import com.sun.j2me.pim.LineReader;
import com.sun.j2me.pim.PIMFormat;
import com.sun.j2me.pim.PIMHandler;
import com.sun.j2me.pim.UnsupportedPIMFormatException;
import com.sun.j2me.pim.AbstractPIMList;
import com.sun.j2me.pim.AbstractPIMItem;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import javax.microedition.pim.Contact;
import javax.microedition.pim.PIM;
import javax.microedition.pim.PIMException;
import javax.microedition.pim.PIMItem;
import javax.microedition.pim.PIMList;
import com.sun.j2me.jsr75.StringUtil;
/**
* Partial implementation of PIMEncoding for VCard/2.1 and VCard/3.0.
*
*/
public abstract class VCardFormat extends EndMatcher implements PIMFormat {
/**
* Returns the version number of vCard implemented.
* @return the VCard version number
*/
protected abstract String getVersion();
/**
* Gets the vCard property name used to store categories.
* @return the vCard category property name
*/
protected abstract String getCategoryProperty();
/**
* Gets the vCard property name used to store classes.
* @return the class property name
*/
protected abstract String getClassProperty();
/**
* Gets the binary value describing all flags in a vCard line.
* @param attributes fields to be parsed
* @return binary coded settings
*/
protected abstract int parseAttributes(String[] attributes);
/**
* Gets the name of the default binary encoding.
* This is "BASE64" for vCard 2.1 and "B" for vCard 3.0.
* @return the default binary encoding
*/
protected abstract String getBinaryEncodingName();
/**
* VCard formatting class.
*/
public VCardFormat() {
super("VCARD");
}
/**
* Gets the code name of this encoding (e.g. "VCARD/2.1").
* @return the encoding name
*/
public String getName() {
return "VCARD/" + getVersion();
}
/**
* Checks to see if a given PIM list type is supported by this encoding.
* @param pimListType int representing the PIM list type to check
* @return true if the type can be read and written by this encoding,
* false otherwise
*/
public boolean isTypeSupported(int pimListType) {
return pimListType == PIM.CONTACT_LIST;
}
/**
* Serializes a PIMItem.
* @param out Stream to which serialized data is written
* @param encoding Character encoding to use for serialized data
* @param pimItem The item to write to the stream
* @throws IOException if an error occurs while writing
*/
public void encode(OutputStream out, String encoding, PIMItem pimItem)
throws IOException {
Writer w = new OutputStreamWriter(out, encoding);
w.write("BEGIN:VCARD\r\n");
w.write("VERSION:");
w.write(getVersion());
w.write("\r\n");
// write all fields
int[] fields = pimItem.getFields();
FormatSupport.sort(fields);
for (int i = 0; i < fields.length; i++) {
int valueCount = pimItem.countValues(fields[i]);
for (int j = 0; j < valueCount; j++) {
writeValue(w, pimItem, fields[i], j);
}
}
// write categories.
String categories = StringUtil.join(pimItem.getCategories(), ",");
if (categories.length() > 0) {
w.write(getCategoryProperty());
w.write(":");
w.write(categories);
w.write("\r\n");
}
w.write("END:VCARD\r\n");
w.flush();
}
/**
* Writes a single vCard line.
* @param w output stream target
* @param item the data to to written
* @param field the attribute to be processed
* @param index the offset of the data to be processed
* @throws IOException if an error occurs while writing
*/
protected void writeValue(Writer w, PIMItem item, int field, int index)
throws IOException {
String label = VCardSupport.getFieldLabel(field);
switch (field) {
case Contact.FORMATTED_NAME:
case Contact.FORMATTED_ADDR:
case Contact.PHOTO_URL:
case Contact.TEL:
case Contact.EMAIL:
case Contact.TITLE:
case Contact.ORG:
case Contact.NICKNAME:
case Contact.NOTE:
case Contact.UID:
case Contact.URL:
case Contact.PUBLIC_KEY_STRING: {
String sValue = item.getString(field, index);
if (sValue != null) {
w.write(label);
writeAttributes(w, item.getAttributes(field, index));
w.write(":");
w.write(sValue);
w.write("\r\n");
}
break;
}
case Contact.NAME:
case Contact.ADDR: {
String[] aValue = item.getStringArray(field, index);
if (aValue != null) {
w.write(label);
writeAttributes(w, item.getAttributes(field, index));
w.write(":");
writeStringArray(w, aValue);
w.write("\r\n");
}
break;
}
case Contact.PHOTO: {
String beeValue = new String(item.getBinary(field, index));
if ( beeValue != null ){
w.write(label);
w.write(";ENCODING=");
w.write(getBinaryEncodingName());
writeAttributes(w, item.getAttributes(field, index));
byte[] rawData = Base64Encoding.fromBase64( beeValue );
if (rawData.length > 0) {
String imageType = getImageType( rawData );
if( imageType != null ) {
w.write(";TYPE=");
w.write(imageType);
}
}
w.write(":\r\n");
//Re-encode the image data to make sure that each line
//of the Base64 format data has at most 76 + 4 characters
w.write(Base64Encoding.toBase64( rawData, 76, 4 ));
w.write("\r\n");
}
break;
}
case Contact.PUBLIC_KEY: {
String beeValue = new String(item.getBinary(field, index));
if (beeValue != null) {
w.write(label);
w.write(";ENCODING=");
w.write(getBinaryEncodingName());
writeAttributes(w, item.getAttributes(field, index));
w.write(":\r\n");
//Force to re-encode the binary data
w.write(Base64Encoding.toBase64(
Base64Encoding.fromBase64( beeValue ),
76, 4 ));
w.write("\r\n");
}
break;
}
case Contact.BIRTHDAY:
w.write(label);
writeAttributes(w, item.getAttributes(field, index));
w.write(":");
writeDate(w, item.getDate(field, index));
w.write("\r\n");
break;
case Contact.REVISION:
w.write(label);
writeAttributes(w, item.getAttributes(field, index));
w.write(":");
writeDateTime(w, item.getDate(field, index));
w.write("\r\n");
break;
case Contact.CLASS: {
int iValue = item.getInt(field, index);
String sValue = VCardSupport.getClassType(iValue);
if (sValue != null) {
w.write(getClassProperty());
writeAttributes(w, item.getAttributes(field, index));
w.write(":");
w.write(sValue);
w.write("\r\n");
}
break;
}
default:
// field cannot be written. ignore it.
}
}
/**
* Writes a vCard field with multiple elements, such as ADR.
* @param w output stream target
* @param data the strings to write
* @throws IOException if an error occurs while writing
*/
protected void writeStringArray(Writer w, String[] data)
throws IOException {
for (int i = 0; i < data.length; i++) {
if (data[i] != null) {
w.write(data[i]);
}
if (i != data.length -1) {
w.write(';');
}
}
}
/**
* Writes a vCard date field.
* @param w output stream target
* @param date data to be written
* @throws IOException if an error occurs while writing
*/
protected void writeDate(Writer w, long date) throws IOException {
w.write(PIMHandler.getInstance().composeDate(date));
}
/**
* Writes a vCard date time field.
* @param w output stream target
* @param date data to be written
* @throws IOException if an error occurs while writing
*/
protected void writeDateTime(Writer w, long date) throws IOException {
w.write(PIMHandler.getInstance().composeDateTime(date));
}
/**
* Writes the attributes for a field.
* @param w output stream target
* @param attributes data to be written
* @throws IOException if an error occurs while writing
*/
protected abstract void writeAttributes(Writer w, int attributes)
throws IOException;
/**
* Constructs one or more PIMItems from serialized data.
* @param in Stream containing serialized data
* @param encoding Character encoding of the stream
* @param list PIMList to which items should be added, or null if the items
* should not be part of a list
* @throws UnsupportedPIMFormatException if the serialized data cannot be
* interpreted by this encoding.
* @return a non-empty array of PIMItems containing the objects described in
* the serialized data, or null if no items are available
* @throws IOException if an error occurs while reading
*/
public PIMItem[] decode(InputStream in, String encoding, PIMList list)
throws IOException {
LineReader r = new LineReader(in, encoding, this);
ContactImpl contact = decode(r, list);
if (contact == null) {
return null;
} else {
return new ContactImpl[] { contact };
}
}
/**
* Constructs a single PIMItem from serialized data.
* @param in LineReader containing serialized data
* @param list PIM list to which the item belongs
* @throws UnsupportedPIMFormatException if the serialized data cannot be
* interpreted by this encoding.
* @return an unserialized Contact, or null if no data was available
*/
private ContactImpl decode(LineReader in, PIMList list)
throws IOException {
String line = in.readLine();
if (line == null) {
return null;
}
if (!line.toUpperCase().equals("BEGIN:VCARD")) {
throw new UnsupportedPIMFormatException("Not a vCard :'"
+ line + "'");
}
String categoryProperty = getCategoryProperty();
ContactImpl contact = new ContactImpl((AbstractPIMList)list);
while ((line = in.readLine()) != null) {
FormatSupport.DataElement element =
FormatSupport.parseObjectLine(line);
if ( element.propertyName.equals( "PHOTO" ) ) {
element = parsePhoto(in, element, contact );
} else if ( element.propertyName.equals( "KEY" ) ) {
element = parsePublicKey(in, element, contact );
}
if (element.propertyName.equals("END")) {
return contact;
} else if (element.propertyName.equals("VERSION")) {
if (!element.data.equals(getVersion())) {
throw new UnsupportedPIMFormatException("Version "
+ element.data + " not supported");
}
} else if (element.propertyName.equals(categoryProperty)) {
String[] categories = StringUtil.split(element.data, ',', 0);
for (int j = 0; j < categories.length; j++) {
try {
contact.addToCategory(categories[j]);
} catch (PIMException e) {
// cannot add to category
}
}
} else {
importData(contact,
element.propertyName,
element.attributes,
element.data,
contact.getPIMListHandle());
}
}
throw new IOException("Unterminated vCard");
}
/**
* Parses a single vCard line.
* @param contact element to populate
* @param prefix filter for selecting fields
* @param attributes fields to be processed
* @param data input to be filtered
* @param listHandle handle of the list containing the contact being parsed
*/
private void importData(Contact contact,
String prefix, String[] attributes, String data, Object listHandle) {
int attr = parseAttributes(attributes);
int field = VCardSupport.getFieldCode(prefix);
if (field == -1 && prefix.equals(getClassProperty())) {
field = Contact.CLASS;
}
if (!PIMHandler.getInstance().isSupportedField(listHandle, field)) {
return;
}
switch (field) {
case Contact.FORMATTED_NAME:
case Contact.FORMATTED_ADDR:
case Contact.TEL:
case Contact.EMAIL:
case Contact.TITLE:
case Contact.ORG:
case Contact.NICKNAME:
case Contact.NOTE:
case Contact.UID:
case Contact.URL: {
String sdata = FormatSupport.parseString(attributes, data);
contact.addString(field, attr, sdata);
break;
}
case Contact.NAME:
case Contact.ADDR: {
String[] elements =
FormatSupport.parseStringArray(attributes, data);
int elementCount = PIMHandler.getInstance()
.getStringArraySize(listHandle, field);
if (elements.length != elementCount) {
String[] a = new String[elementCount];
System.arraycopy(elements, 0, a, 0,
Math.min(elements.length, elementCount));
elements = a;
}
contact.addStringArray(field, attr, elements);
break;
}
case Contact.BIRTHDAY:
case Contact.REVISION: {
int dateLen = data.length();
// end date is either date in yyyyMMdd format or
// date/time in yyyymmddThhmmss(Z).
long date = (dateLen < 15) ?
PIMHandler.getInstance().parseDate(data) :
PIMHandler.getInstance().parseDateTime(data);
contact.addDate(field, attr, date);
break;
}
case Contact.PHOTO: {
String valueType =
FormatSupport.getAttributeValue(attributes, "VALUE=", null);
if (valueType == null) {
// binary data represented in "B" encoded format
byte[] bData = data.getBytes();
if (bData.length != 0) {
contact.addBinary(Contact.PHOTO, attr, bData, 0,
bData.length);
}
} else if (valueType.equals("URL")) {
String sdata = FormatSupport.parseString(attributes, data);
contact.addString(Contact.PHOTO_URL, attr, sdata);
} else {
// ignore; value type not recognized
}
break;
}
case Contact.PUBLIC_KEY: {
byte[] beeData = data.getBytes();
if (beeData.length != 0) {
contact.addBinary(Contact.PUBLIC_KEY, attr, beeData, 0,
beeData.length);
}
break;
}
case Contact.CLASS: {
int i = VCardSupport.getClassCode(data);
if (i != -1) {
contact.addInt(Contact.CLASS, Contact.ATTR_NONE, i);
}
break;
}
default:
// System.err.println("No match for field prefix '"
// + prefix + "' (REMOVE THIS MESSAGE)");
}
}
private String getImageType(byte[] rawData){
if ( rawData[0] == (byte)0x42 && rawData[1] == (byte)0x4d ) {
return "BMP";
}
if ( rawData[0] == (byte)0xff && rawData[1] == (byte)0xd8
&& rawData[2] == (byte)0xff && rawData[3] == (byte)0xe0) {
return "JPEG";
}
if ( rawData[0] == (byte)0x49 && rawData[1] == (byte)0x49
&& rawData[2] == (byte)0x2a ) {
return "TIFF";
}
if ( rawData[0] == (byte)0x47 && rawData[1] == (byte)0x49
&& rawData[2] == (byte)0x46 ) {
return "GIF";
}
if ( rawData[0] == (byte)0x89 && rawData[1] == (byte)0x50
&& rawData[2] == (byte)0x4e && rawData[3] == (byte)0x47
&& rawData[4] == (byte)0x0d && rawData[5] == (byte)0x0a
&& rawData[6] == (byte)0x1a && rawData[7] == (byte)0x0a
&& rawData[8] == (byte)0x00 && rawData[9] == (byte)0x00
&& rawData[10] == (byte)0x00 && rawData[11] == (byte)0x0d
&& rawData[12] == (byte)0x49 && rawData[13] == (byte)0x48
&& rawData[14] == (byte)0x44 && rawData[15] == (byte)0x52 ) {
return "PNG";
}
return null;
}
private FormatSupport.DataElement parsePhoto( LineReader in, FormatSupport.DataElement element,
ContactImpl contact )
throws IOException {
String encodingName = FormatSupport.getAttributeValue(
element.attributes, "ENCODING=", null);
if ( encodingName == null ) {//Use URL, only 1 line for PHOTO field
return element;
}
do {
String line = in.readLine();
if ( line.indexOf( ':' ) != -1 || line.indexOf( ';' ) != -1 ){
if ( encodingName.equals(FormatSupport.QUOTED_PRINTABLE)){
byte[] rawBinaryData =
QuotedPrintableEncoding.fromQuotedPrintable( element.data );
element.data = Base64Encoding.toBase64(rawBinaryData, 76, 4);
}else if ( encodingName.equals(FormatSupport.BASE64) == false
&& encodingName.equals(FormatSupport.BEE) == false){
element = FormatSupport.parseObjectLine(line);
break;
}
importData(contact,
element.propertyName,
element.attributes,
element.data,
contact.getPIMListHandle());
element =
FormatSupport.parseObjectLine(line);
break;
}
element.data += line;
}while (true);
if ( element.propertyName.equals("KEY")) {
return parsePublicKey( in, element, contact );
}
return element;
}
private FormatSupport.DataElement parsePublicKey( LineReader in, FormatSupport.DataElement element,
ContactImpl contact )
throws IOException {
String encodingName = FormatSupport.getAttributeValue(
element.attributes, "ENCODING=", null);
do {
String line = in.readLine();
if ( line.indexOf( ':' ) != -1 || line.indexOf( ';' ) != -1 ){
if ( encodingName.equals(FormatSupport.QUOTED_PRINTABLE)){
//Re-encode binary data from QUOTED_PRINTABLE format to "B" format
byte[] rawBinaryData =
QuotedPrintableEncoding.fromQuotedPrintable( element.data );
element.data = Base64Encoding.toBase64(rawBinaryData, 76, 4);
}else if ( encodingName.equals(FormatSupport.PLAIN_TEXT )){
//Re-encode binary data from PLAIN_TEXT format to "B" format
byte[] rawBinaryData = element.data.getBytes();
element.data = Base64Encoding.toBase64(rawBinaryData, 76, 4);
}else if ( encodingName.equals(FormatSupport.BASE64) ==false
&& encodingName.equals(FormatSupport.BEE) == false){
//The encoding type not supported, decod the next line and break
element = FormatSupport.parseObjectLine(line);
break;
}
importData(contact,
element.propertyName,
element.attributes,
element.data,
contact.getPIMListHandle());
element =
FormatSupport.parseObjectLine(line);
break;
}
element.data += line;
}while (true);
if ( element.propertyName.equals("PHOTO")) {
return parsePhoto( in, element, contact );
}
return element;
}
}