/** * This class is a PIMSyncSource for ICal and VCal data, which could include * either an Event or a Todo. NB: The Todo part is not currenlty implemented. * @author Diorgenes Felipe Grzesiuk * @copyright Copyright 2007-2008 Prognus * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 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 for more details. * * You should have received a copy of the GNU General Public License * along with Foobar; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package br.com.prognus.psync.engine.source; import java.io.ByteArrayInputStream; import java.sql.Timestamp; import java.util.List; import java.util.StringTokenizer; import br.com.prognus.psync.exception.EntityException; import br.com.prognus.psync.items.manager.PIMCalendarManager; import br.com.prognus.psync.items.model.CalendarWrapper; import br.com.prognus.psync.util.Def; import com.funambol.common.pim.calendar.Calendar; import com.funambol.common.pim.converter.BaseConverter; import com.funambol.common.pim.converter.CalendarToSIFE; import com.funambol.common.pim.converter.TaskToSIFT; import com.funambol.common.pim.converter.VCalendarConverter; import com.funambol.common.pim.converter.VComponentWriter; import com.funambol.common.pim.icalendar.ICalendarParser; import com.funambol.common.pim.model.VCalendar; import com.funambol.common.pim.sif.SIFCalendarParser; import com.funambol.common.pim.xvcalendar.XVCalendarParser; import com.funambol.framework.engine.SyncItem; import com.funambol.framework.engine.SyncItemImpl; import com.funambol.framework.engine.SyncItemKey; import com.funambol.framework.engine.SyncItemState; import com.funambol.framework.engine.source.SyncContext; import com.funambol.framework.engine.source.SyncSourceException; import com.funambol.framework.tools.beans.BeanInitializationException; import com.sun.org.apache.xerces.internal.impl.xs.SubstitutionGroupHandler; public class PIMCalendarSyncSource extends PIMSyncSource { // ------------------------------------------------------------- Private // data private PIMCalendarManager manager; // --------------------------------------------------------------- // Properties private Class entityType; // CalendarContent or one of its subclasses public Class getEntityType() { return entityType; } public void setEntityType(Class entityType) { this.entityType = entityType; } // ----------------------------------------------------------- Public // methods public void beginSync(SyncContext context) { try { this.manager = new PIMCalendarManager(JNDI_DATA_SOURCE_NAME, context.getPrincipal(), context.getSourceQuery()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } super.manager = this.manager; super.beginSync(context); } /** * Makes an array of SyncItemKey objects representing the ID(s) of the * twin(s) of a given calendar. * * @param syncItem * the SyncItem representing the calendar whose twin(s) are * looked for * @throws SyncSourceException * @return possibly, just one or no key should be in the array, but it can't * be ruled out a priori that several keys get returned by this * method */ public SyncItemKey[] getSyncItemKeysFromTwin(SyncItem syncItem) throws SyncSourceException { try { Calendar calendar = convert(getContentFromSyncItem(syncItem), syncItem.getType()); List idList = null; idList = manager.getTwins(calendar); SyncItemKey[] keyList = new SyncItemKey[idList.size()]; for (int i = 0; i < idList.size(); i++) { keyList[i] = new SyncItemKey((String) idList.get(i)); } return keyList; } catch (EntityException e) { throw new SyncSourceException("Error retrieving twin item keys.", e); } } /** * Adds a SyncItem object (representing a calendar). * * @param syncItem * the SyncItem representing the calendar * * @return a newly created syncItem based on the input object but with its * status set at SyncItemState.NEW and the GUID retrieved by the * back-end */ public SyncItem addSyncItem(SyncItem syncItem) throws SyncSourceException { if (log.isTraceEnabled()) { log.trace("PIMCalendarSyncSource addSyncItem begin"); } Calendar c = null; String content = null; try { content = getContentFromSyncItem(syncItem); String contentType = syncItem.getType(); c = convert(content, contentType); Timestamp ts = syncItem.getTimestamp(); // Adds the calendar, wraps it in sync information and uses it to // create a new SyncItem which is the return value of this method SyncItemImpl newSyncItem = new SyncItemImpl(this, // syncSource manager.addItem(c, ts), // key null, // mappedKey SyncItemState.NEW, // state content.getBytes(), // content null, // format contentType, // type ts // timestamp ); return newSyncItem; } catch (Exception e) { log.error("SyncSource error adding a new synchronization item."); throw new SyncSourceException("Error adding the item " + syncItem, e); } } /** * Updates a SyncItem object (representing a calendar). * * @param syncItem * the SyncItem representing the calendar * * @return a newly created syncItem based on the input object but with its * status set at SyncItemState.UPDATED and the GUID retrieved by the * back-end */ public SyncItem updateSyncItem(SyncItem syncItem) throws SyncSourceException { if (log.isTraceEnabled()) { log.trace("Updates a SyncItem from " + sourceURI); } Calendar c = null; String content = null; try { String id = syncItem.getKey().getKeyAsString(); content = getContentFromSyncItem(syncItem); String contentType = syncItem.getType(); c = convert(content, contentType); // Modifies the calendar, wraps it in sync information and uses it // to // create a new SyncItem which is the return value of this method SyncItemImpl newSyncItem = new SyncItemImpl(this, // syncSource manager.updateItem(id, c, syncItem.getTimestamp()), // key null, // mappedKey SyncItemState.UPDATED, // state content.getBytes(), // content null, // format contentType, // type null // timestamp ); return newSyncItem; } catch (Exception e) { log.error("SyncSource error updating a synchronization item.", e); throw new SyncSourceException( "Error updating the item " + syncItem, e); } } /** * Deletes the item with a given syncItemKey. * * @param syncItemKey * @param timestamp * in case of a soft deletion, this will be the registered moment * of deletion; if a hard deletion is used, this field is * irrelevant and may also be null * @param softDelete * it is true if the client requires a soft deletion * @throws SyncSourceException */ public void removeSyncItem(SyncItemKey syncItemKey, Timestamp timestamp, boolean softDelete) throws SyncSourceException { try { if (!softDelete) { if (log.isTraceEnabled()) { log.trace("PIMCalendarSyncSource remove the SyncItem " + syncItemKey.getKeyAsString()); } manager.removeItem(syncItemKey.getKeyAsString()); } } catch (EntityException e) { log.error("Sync source error: could not delete item with key" + syncItemKey, e); throw new SyncSourceException("Error deleting item. ", e); } } public SyncItem getSyncItemFromId(SyncItemKey syncItemKey) throws SyncSourceException { String id = null; SyncItem syncItem = null; id = syncItemKey.getKeyAsString(); if (log.isTraceEnabled()) { log.trace("PIMCalendarSyncSource get SyncItem from " + id); } try { CalendarWrapper cw; try { cw = manager.getItem(id); } catch (Exception e) { return null; } // Retrieves the calendar, wraps it in sync information and uses it // to create a new SyncItem which is the return value of this method syncItem = createSyncItem(id, cw.getCalendar(), SyncItemState.NEW); } catch (EntityException e) { throw new SyncSourceException("Error seeking SyncItem with ID: " + id, e); } return syncItem; } public boolean mergeSyncItems(SyncItemKey syncItemKey, SyncItem syncItem) throws SyncSourceException { try { Calendar calendar = convert(getContentFromSyncItem(syncItem), syncItem.getType()); boolean clientUpdateRequired = manager.mergeItems(syncItemKey .getKeyAsString(), calendar, syncItem.getTimestamp()); if (clientUpdateRequired) { syncItem = getSyncItemFromId(syncItemKey); } return true; //clientUpdateRequired 418 } catch (EntityException e) { log.error("SyncSource error: a merge did not succeed.", e); throw new SyncSourceException("Error merging SyncItem with ID " + syncItemKey.getKeyAsString() + "with SyncItem " + syncItem, e); } } public void init() throws BeanInitializationException { } /** * Makes an array of SyncItemKey objects representing all new calendar IDs, * filtered according to a given time interval. * * @param since * the earlier limit of the time interval * @param to * the later limit of the time interval * @return a SyncItemKey array */ public SyncItemKey[] getNewSyncItemKeys(Timestamp since, Timestamp to) throws SyncSourceException { saveSyncTiming(since, to); try { List idList = manager.getNewItems(since, to); return extractKeyArrayFromIdList(idList); } catch (EntityException e) { throw new SyncSourceException("Error retrieving new item keys.", e); } } /** * Makes an array of SyncItemKey objects representing all deleted calendar * IDs, filtered according to a given time interval. * * @param since * the earlier limit of the time interval * @param to * the later limit of the time interval * @return a SyncItemKey array */ public SyncItemKey[] getUpdatedSyncItemKeys(Timestamp since, Timestamp to) throws SyncSourceException { saveSyncTiming(since, to); try { List idList = manager.getUpdatedItems(since, to); return extractKeyArrayFromIdList(idList); } catch (EntityException e) { throw new SyncSourceException( "Error retrieving updated item keys.", e); } } /** * Makes an array of SyncItemKey objects representing all deleted calendar * IDs, filtered according to a given time interval. * * @param since * the earlier limit of the time interval * @param to * the later limit of the time interval * @return a SyncItemKey array */ public SyncItemKey[] getDeletedSyncItemKeys(Timestamp since, Timestamp to) throws SyncSourceException { saveSyncTiming(since, to); try { List idList = manager.getDeletedItems(since, to); return extractKeyArrayFromIdList(idList); } catch (EntityException e) { throw new SyncSourceException( "Error retrieving deleted item keys.", e); } } /** * Makes an array of SyncItemKey objects representing all calendar IDs. * * @return a SyncItemKey array */ public SyncItemKey[] getAllSyncItemKeys() throws SyncSourceException { try { List idList = manager.getAllItems(); return extractKeyArrayFromIdList(idList); } catch (EntityException e) { throw new SyncSourceException("Error retrieving all item keys. ", e); } } /** * Gets the status of the SyncItem with the given key. * * @param syncItemKey * as a SyncItemKey object * @throws SyncSourceException * @return the status as a char */ public char getSyncItemStateFromId(SyncItemKey syncItemKey) throws SyncSourceException { String id = "N/A"; // default value for error tracking try { // Slow sync // @todo Implement, depending on a syncMode check // Fast sync id = syncItemKey.getKeyAsString(); if (log.isTraceEnabled()) { log .trace("PIMCalendarSyncSource get SyncItem state from " + id); } char itemRawState = manager.getItemState(id, previousSyncTime); if (itemRawState == Def.PIM_STATE_UNCHANGED) { return SyncItemState.SYNCHRONIZED; } else { return itemRawState; // Def uses SyncItemState.* as constant // values for N, D and U states } } catch (EntityException e) { throw new SyncSourceException( "Error getting the state of SyncItem " + "with ID " + id, e); } } // ---------------------------------------------------------- Private // methods /** * Extracts the content from a syncItem. * * @param syncItem * @return as a String object (same as * PIMSyncSource#getContentFromSyncItem(String), but trimmed) */ protected String getContentFromSyncItem(SyncItem syncItem) { String raw = super.getContentFromSyncItem(syncItem); return raw.trim(); } private String hackFix(String text) { String result, head, title, clas, description, tail, new_title; int p_categories, p_classes, p_location, p_dstart, values1, values2, op1, op2, op3, op4, op5, op6, op7, op8, qt1, qt2; qt1 = 0; qt2 = 0; // Pega as posicoes das tags p_categories = text.indexOf("CATEGORIES"); p_classes = text.indexOf("CLASS"); p_location = text.indexOf("LOCATION"); p_dstart = text.indexOf("DTSTART"); // Pega as posicoes das varias formas da tag SUMMARY op1 = text.indexOf("SUMMARY:"); op2 = text.indexOf("SUMMARY;CHARSET=UTF-8:"); op3 = text.indexOf("SUMMARY;ENCODING=QUOTED-PRINTABLE:"); op4 = text.indexOf("SUMMARY;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:"); // Pega as posicoes das varias formas da tag DESCRIPTION op5 = text.indexOf("DESCRIPTION:"); op6 = text.indexOf("DESCRIPTION;CHARSET=UTF-8:"); op7 = text.indexOf("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:"); op8 = text.indexOf("DESCRIPTION;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:"); qt1 += (op1 == -1) ? 0 : op1 + 8; qt1 += (op2 == -1) ? 0 : op2 + 22; qt1 += (op3 == -1) ? 0 : op3 + 34; qt1 += (op4 == -1) ? 0 : op4 + 48; qt2 += (op5 == -1) ? 0 : op5 + 12; qt2 += (op6 == -1) ? 0 : op6 + 26; qt2 += (op7 == -1) ? 0 : op7 + 38; qt2 += (op8 == -1) ? 0 : op8 + 52; values1 = (p_categories == -1) ? p_classes : p_categories; values2 = (p_location == -1) ? p_dstart : p_location; head = text.substring(0, qt1); title = text.substring(qt1, values1); clas = text.substring(values1, qt2); description = text.substring(qt2, values2); tail = text.substring(values2); new_title = title.replaceAll("\r\n ", " "); // Retira os enters no final do titulo int title_t, title_bl; while(true){ title_t = new_title.length(); title_bl = new_title.lastIndexOf("=0D=0A=\r\n"); if(title_bl != -1 && title_t == title_bl + 11){ new_title = new_title.substring(0, title_bl) + "\r\n"; } else { break; } } // Retira os enters no final da descricao int description_t, description_bl; while(true){ description_t = description.length(); description_bl = description.lastIndexOf("=0D=0A=\r\n"); if(description_bl != -1 && description_t == description_bl + 11){ description = description.substring(0, description_bl) + "\r\n"; } else { break; } } result = head + new_title + clas + description + tail; return result; } private Calendar webCalendar2Calendar(String text, String vCalType) throws EntityException { try { // BugFix XVCalendarParser if (vCalType.equals(PIMSyncSource.TYPE[PIMSyncSource.VCAL])) { text = hackFix(text); } ByteArrayInputStream buffer = new ByteArrayInputStream(text.getBytes()); VCalendar vcalendar; String version; String charset; if (log.isTraceEnabled()) { StringBuilder sb = new StringBuilder(text.length() + 60); sb.append("Converting: ").append(vCalType).append( " => Calendar ").append("\nINPUT = {").append(text) .append('}'); log.trace(sb.toString()); } if (vCalType.equals(PIMSyncSource.TYPE[PIMSyncSource.VCAL])) { // vCalendar (1.0): XVCalendarParser parser = new XVCalendarParser(buffer); vcalendar = (VCalendar) parser.XVCalendar(); version = "1.0"; charset = BaseConverter.CHARSET_UTF7; // (versit spec) } else { // iCalendar (2.0): ICalendarParser parser = new ICalendarParser(buffer); vcalendar = (VCalendar) parser.ICalendar(); version = "2.0"; charset = BaseConverter.CHARSET_UTF8; // (RFC 2445) } if (deviceCharset != null) { charset = deviceCharset; // overrides the default character // set } String retrievedVersion = null; if (vcalendar.getProperty("VERSION") != null) { retrievedVersion = vcalendar.getProperty("VERSION").getValue(); } vcalendar.addProperty("VERSION", version); if (retrievedVersion == null) { if (log.isTraceEnabled()) { log.trace("No version property was found in the vCal/iCal " + "data: version set to " + version); } } else if (!retrievedVersion.equals(version)) { if (log.isTraceEnabled()) { log.trace("The version in the data was " + retrievedVersion + " but it's been changed to " + version); } } VCalendarConverter vcf = new VCalendarConverter(deviceTimeZone, charset); Calendar c = null; c = vcf.vcalendar2calendar(vcalendar); if (log.isTraceEnabled()) { log.trace("Conversion done."); } return c; } catch (Exception e) { throw new EntityException("Error converting " + vCalType + " to Calendar. ", e); } } private String calendar2webCalendar(Calendar calendar, String vCalType) throws EntityException { try { String charset; if (vCalType.equals(PIMSyncSource.TYPE[PIMSyncSource.VCAL])) { // vCalendar (1.0): charset = BaseConverter.CHARSET_UTF7; // (versit spec) } else { // iCalendar (2.0): charset = BaseConverter.CHARSET_UTF8; // (RFC 2445) } if (deviceCharset != null) { charset = deviceCharset; // overrides the default character // set } VCalendarConverter vcf = new VCalendarConverter(deviceTimeZone, charset); VCalendar vcalendar; String vcal; if (log.isTraceEnabled()) { log.trace("Converting: Calendar => " + vCalType); } if (vCalType.equals(PIMSyncSource.TYPE[VCAL])) { // VCAL vcalendar = vcf.calendar2vcalendar(calendar, true); // text/x-vcalendar } else { // ICAL vcalendar = vcf.calendar2vcalendar(calendar, false); // text/calendar } VComponentWriter writer = new VComponentWriter( VComponentWriter.NO_FOLDING); vcal = writer.toString(vcalendar); if (log.isTraceEnabled()) { log.trace("OUTPUT = {" + vcal + "}. Conversion done."); } return vcal; } catch (Exception e) { throw new EntityException("Error converting Calendar to " + vCalType, e); } } /** * Create a new SyncItem from a Calendar. The status is passed as an * argument. * * @param calendar * the Calendar object representing the input information * @param status * @throws EntityException * if the content type is wrong or any problem occurs while * creating a new SyncItem * @return a newly created SyncItem object */ private SyncItem createSyncItem(String id, Calendar calendar, char status) throws EntityException { String contentType = getInfo().getPreferredType().getType(); if (log.isTraceEnabled()) { StringBuilder sb = new StringBuilder(100); sb.append("PIMCalendarSyncSource - creating item with:").append( "\n> id: ").append(id).append("\n> status: ") .append(status).append("\n> content-type: ").append( contentType); log.trace(sb.toString()); } SyncItem syncItem = null; String stream = convert(calendar, contentType); try { syncItem = new SyncItemImpl(this, id, status); } catch (Exception e) { throw new EntityException(e); } syncItem.setType(contentType); syncItem.setContent(stream.getBytes()); if (log.isTraceEnabled()) { log.trace("PIMCalendarSyncSource created SyncItem"); } return syncItem; } private SyncItemKey[] extractKeyArrayFromIdList(List idList) { SyncItemKey[] keyList = new SyncItemKey[idList.size()]; for (int i = 0; i < idList.size(); i++) { keyList[i] = new SyncItemKey((String) idList.get(i)); } return keyList; } /** * Converts a calendar in vCalendar/iCalendar, SIF-E or SIF-T format to a * Calendar object. * * @param content * as a String * @param contentType * @throws EntityException * if the contentType is wrong or the conversion attempt doesn't * succeed. * @return a Calendar object */ private Calendar convert(String content, String contentType) throws EntityException { // Finds out which target type is required for (int i = 0; i < TYPE.length; i++) { if (contentType.equals(TYPE[i])) { // Uses the proper converter method switch (i) { case VCAL: case ICAL: return webCalendar2Calendar(content, contentType); case SIFE: case SIFT: return sif2Calendar(content, contentType); default: throw new EntityException("Can't make a Contact " + "out of a " + TYPE[i] + "!"); } } } throw new EntityException("Content type unknown: " + contentType); } /** * Converts a Calendar back to a streamable (vCalendar/iCalendar, SIF-E or * SIF-T) format. * * @param calendar * @param contentType * @throws EntityException * if the contentType is wrong or the conversion attempt doesn't * succeed. * @return the result in the required format */ private String convert(Calendar calendar, String contentType) throws EntityException { // Finds out which target type is required for (int i = 0; i < TYPE.length; i++) { if (contentType.equals(TYPE[i])) { // Uses the proper converter method switch (i) { case VCAL: case ICAL: return calendar2webCalendar(calendar, contentType); case SIFE: case SIFT: return calendar2sif(calendar, contentType); default: throw new EntityException("Can't make a " + TYPE[i] + "out of a Contact!"); } } } throw new EntityException("Content type unknown: " + contentType); } private Calendar sif2Calendar(String xml, String sifType) throws EntityException { if (log.isTraceEnabled()) { StringBuilder sb = new StringBuilder(xml.length() + 60); sb.append("Converting: ").append(sifType).append(" => Calendar ") .append("\nINPUT = {").append(xml).append('}'); log.trace(sb.toString()); } ByteArrayInputStream buffer = null; Calendar calendar = null; try { calendar = new Calendar(); buffer = new ByteArrayInputStream(xml.getBytes()); if ((xml.getBytes()).length > 0) { SIFCalendarParser parser = new SIFCalendarParser(buffer); calendar = parser.parse(); } } catch (Exception e) { throw new EntityException("Error converting " + sifType + " to Calendar. ", e); } if (log.isTraceEnabled()) { log.trace("Conversion done."); } return calendar; } private String calendar2sif(Calendar calendar, String sifType) throws EntityException { if (log.isTraceEnabled()) { log.trace("Converting: Calendar => " + sifType); } String xml = null; BaseConverter c2xml; Object thing; try { if (sifType.equals(PIMSyncSource.TYPE[SIFE])) { // SIF-E c2xml = new CalendarToSIFE(deviceTimeZone, deviceCharset); thing = calendar; // NB: A CalendarToSIFE converts a Calendar into a SIF-E } else { // SIF-T c2xml = new TaskToSIFT(deviceTimeZone, deviceCharset); thing = calendar.getTask(); // NB: A TaskToSIFT converts just a Task into a SIF-T } xml = c2xml.convert(thing); if (log.isTraceEnabled()) { log.trace("OUTPUT = {" + xml + "}. Conversion done."); } } catch (Exception e) { throw new EntityException( "Error converting Calendar to " + sifType, e); } return xml; } }