From: Cameron S. <mu...@ea...> - 2007-07-23 10:59:05
|
Author: muriwo Date: 2007-07-23 03:59:07 -0700 (Mon, 23 Jul 2007) New Revision: 3933 Modified: versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/ContactSyncHandler.java versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncHandler.java versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncSource.java versions/1.0/trunk/hot-deploy/funambol/webapp/WEB-INF/opentaps-sync-config.xml Log: 1731743/OSSI-9: Correct Address purposeType. Refactoring. Skeleton of Merge Modified: versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/ContactSyncHandler.java =================================================================== --- versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/ContactSyncHandler.java 2007-07-23 09:40:22 UTC (rev 3932) +++ versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/ContactSyncHandler.java 2007-07-23 10:59:07 UTC (rev 3933) @@ -30,24 +30,32 @@ import org.ofbiz.base.util.GeneralException; import org.ofbiz.base.util.UtilMisc; +import org.ofbiz.base.util.UtilValidate; import org.ofbiz.entity.GenericEntityException; import org.ofbiz.entity.GenericValue; import org.ofbiz.entity.condition.EntityExpr; import org.ofbiz.entity.condition.EntityOperator; import org.ofbiz.service.GenericServiceException; +import com.funambol.common.pim.common.Property; +import com.funambol.common.pim.common.TypifiedProperty; +import com.funambol.common.pim.contact.Address; import com.funambol.common.pim.contact.Contact; +import com.funambol.common.pim.contact.Email; +import com.funambol.common.pim.contact.Name; +import com.funambol.common.pim.contact.Phone; import com.funambol.framework.engine.source.SyncSource; +import com.funambol.framework.engine.source.SyncSourceException; import com.funambol.framework.logging.FunambolLogger; import com.funambol.framework.logging.FunambolLoggerFactory; /** - * Knows how to perform sync for an OT Contact/Lead/Account + * Knows how to perform sync for an OT Contact/Lead/Account, represented remotely as an instance of com.funambol.common.pim.contact.Contact * * @author cameron * */ -public class ContactSyncHandler implements EntitySyncHandler +public class ContactSyncHandler implements EntitySyncHandler<Contact> { private static FunambolLogger log = FunambolLoggerFactory.getLogger("funambol.server"); @@ -77,6 +85,75 @@ _updateServices.put("PROSPECT", new ServiceMapping("crmsfa.updateLead", "partyId")); } + public Contact getItemFromId(String key) throws GeneralException + { + log.warn("Will look for this partyId: " + key); + GenericValue contactPartyGV = _syncSource.findByPrimaryKey("Party", UtilMisc.toMap("partyId", key)); + EntityPreparer contactParty = new EntityPreparer(contactPartyGV); + Contact contact = new Contact(); //TODO: use EBConverter in this direction too, but first need a bean preparer + + //1.1 Person (for Lead/Contact) or PartyGroup (for Account) + Object partyType = contactParty.get("partyTypeId"); + if("PERSON".equals(partyType)) + { + GenericValue person = contactParty.getRelated("Person"); + + //2. convert Entity -> Contact + Name name = new Name(); + name.getFirstName().setPropertyValue(person.get("firstName")); + name.getLastName().setPropertyValue(person.get("lastName")); + contact.setName(name); + } + else if("PARTY_GROUP".equals(partyType)) //TODO: actually we want to do this for the related company as well + { + GenericValue company = contactParty.getRelated("PartyGroup"); + + //2. convert Entity -> Contact + contact.getBusinessDetail().setCompanies(company.getString("groupName")); + } + else { log.warn("Unrecognized party type: " + partyType); } + + //3. now deal with the contact numbers for this party + buildPhone(contact.getBusinessDetail().getPhones(), contactPartyGV, "PRIMARY_PHONE", "BusinessTelephoneNumber"); + buildPhone(contact.getPersonalDetail().getPhones(),contactPartyGV, "PHONE_HOME", "HomeTelephoneNumber"); + buildPhone(contact.getBusinessDetail().getPhones(), contactPartyGV, "FAX_NUMBER", "BusinessFaxTelephoneNumber"); + buildPhone(contact.getPersonalDetail().getPhones(), contactPartyGV, "PHONE_MOBILE", "MobileTelephoneNumber"); + + //address + GenericValue purpose = getPurposeFromParty(contactPartyGV, "GENERAL_LOCATION"); //according to spec should be general corresp. address + //TODO: failing that, look for shipping address? + if(purpose!= null) + { + GenericValue postalAddress = purpose.getRelatedOne("PostalAddress"); + if(postalAddress!=null) + { + String + city = postalAddress.getString("city"), + country = postalAddress.getRelatedOne("CountryGeo").getString("geoName"), + postal = postalAddress.getString("postalCode"), + postOffice = postalAddress.getString("address2"), + street = postalAddress.getString("address1"), + state = postalAddress.getRelatedOne("StateProvinceGeo").getString("geoName"); + + //TODO: this should be done via a converter + Address address = contact.getBusinessDetail().getAddress(); + setIfNotEmpty(address.getCity(), city); + setIfNotEmpty(address.getCountry(), country); + setIfNotEmpty(address.getPostalCode(), postal); + setIfNotEmpty(address.getPostOfficeAddress(), postOffice); + setIfNotEmpty(address.getStreet(), street); + setIfNotEmpty(address.getState(), state); + } + } + + //email + prepareContactMail(getPurposeFromParty(contactPartyGV, "PRIMARY_EMAIL"), contact, "Email1Address"); + prepareContactMail(getPurposeFromParty(contactPartyGV, "OTHER_EMAIL"), contact, "OtherEmail2Address"); + prepareContactMail(getPurposeFromParty(contactPartyGV, "OTHER_EMAIL_SEC"), contact, "OtherEmail3Address"); + + return contact; + } + /** * @return Iterable<partyId> of the Account/Contact/Leads which have been deleted in OT * @@ -125,13 +202,13 @@ * @param key should be partyId of an Account, Contact or Lead * @param source should be a Contact */ - public void updateItem(String partyId, Object source) throws GenericEntityException, GenericServiceException + public void updateItem(String partyId, Contact contact) throws GenericEntityException, GenericServiceException { //1. Update Party + Person/PartyGroup with the appropriate service - EntityPreparer contactParty = new EntityPreparer(_syncSource.findByPrimaryKey("Party", UtilMisc.toMap("partyId", partyId))); - Contact contact = (Contact)source; + GenericValue contactGV = _syncSource.findByPrimaryKey("Party", UtilMisc.toMap("partyId", partyId)); //2. call the appropriate converter + update service, based on first role which matches + boolean partyUpdated = false; for(GenericValue role : findRolesForParty(partyId)) //unfortunately Java does not have a lambda/block mechanism!! { String roleType = role.getString("roleTypeId"); @@ -145,12 +222,41 @@ _syncSource.runSync(service.getServiceName(), serviceParams); //2.2 actually call the service log.info("UPDATE " + partyId + " via " + service); - return; //nothing more to do + partyUpdated = true; + break; //nothing more to do for top-level record } - } + } - //3. if we got to here there is something wrong - throw new GenericServiceException("Could not find any service to UPDATE party " + partyId); + if(!partyUpdated) { throw new GenericServiceException("Could not find any service to UPDATE party " + partyId); } + + //3 postal address + updateOrInsertPostalAddress(contactGV, contact, "GENERAL_LOCATION"); //TODO: also shipping address + + //4. emails + for(Email email : (List<Email>)contact.getPersonalDetail().getEmails()) + { + updateOrInsertTelecomNumberOrEmail(contactGV, contact, email, "PRIMARY_EMAIL", "Email1Address"); + updateOrInsertTelecomNumberOrEmail(contactGV, contact, email, "OTHER_EMAIL", "OtherEmail2Address"); + updateOrInsertTelecomNumberOrEmail(contactGV, contact, email, "OTHER_EMAIL_SEC", "OtherEmail3Address"); + } + + //5. update TelecomNumber TODO: use bean query string to avoid need for loop + for(Phone phone : (List<Phone>)contact.getBusinessDetail().getPhones()) + { + updateOrInsertTelecomNumberOrEmail(contactGV, contact, phone, "PRIMARY_PHONE", "BusinessTelephoneNumber"); + updateOrInsertTelecomNumberOrEmail(contactGV, contact, phone, "FAX_NUMBER", "BusinessFaxTelephoneNumber"); + } + for(Phone phone : (List<Phone>)contact.getPersonalDetail().getPhones()) + { + updateOrInsertTelecomNumberOrEmail(contactGV, contact, phone, "PHONE_HOME", "HomeTelephoneNumber"); + updateOrInsertTelecomNumberOrEmail(contactGV, contact, phone, "PHONE_MOBILE", "MobileTelephoneNumber"); + } + } + + public boolean mergeItem(String key, Contact source) throws GeneralException + { + //TODO: not yet implemented + return false; } //=== private behaviour === @@ -161,5 +267,170 @@ private List<GenericValue> findRolesForParty(String partyId) throws GenericEntityException { return _syncSource.findByAnd("PartyRole", UtilMisc.toMap("partyId", partyId)); - } + } + + private void prepareContactMail(GenericValue purpose, Contact contact, String mailType) throws GenericEntityException + { + if(purpose!= null) + { + String otEmail = ((GenericValue)purpose.getRelatedOne("ContactMech")).getString("infoString"); + otEmail = otEmail==null?"":otEmail; + Email email = new Email(); + email.setPropertyType(mailType); + email.setPropertyValue(otEmail); + contact.getPersonalDetail().getEmails().add(email); + } + } + + //TODO: Eurico put a comment here! CAN WE NOT UNIFY THIS WITH: getActivePartyContactMechPurpose + private GenericValue getPurposeFromParty(GenericValue contactParty, String purposeType) throws GenericEntityException + { + List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", purposeType, "thruDate", null)); + + if(activePCMPs.size() > 1){log.warn("Could not find a unique active PCMP for " + purposeType + " and " + contactParty);} + + if(activePCMPs.size() == 0){return null;} + + return activePCMPs.get(0); + } + + /** + * Insert (or updated if already exists) a TelecomNumber for given contactParty and contactMechPurposeType. + * + * @param contactParty + * @param contact + * @param purpose - A ContactMechPurposeType value + * @throws GenericEntityException + * @throws SyncSourceException + */ + private void updateOrInsertPostalAddress(GenericValue contactParty, Contact contact, String purpose) throws GenericEntityException + { + GenericValue postalAddrGV = getActivePartyContactMechPurpose(contactParty, purpose); + if(postalAddrGV == null) + { + _syncSource.insertContactMech((String)contactParty.get("partyId"), contact, purpose, "POSTAL_ADDRESS"); + } + else + { + _converters.toEntityAndStore(contact, postalAddrGV.getRelatedOne("PostalAddress"), purpose); + } + } + + private GenericValue getActivePartyContactMechPurpose(GenericValue contactParty, String purpose) throws GenericEntityException + { + List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", purpose, "thruDate", null)); + if(activePCMPs.size() > 1) + { + log.warn("Could not find a unique active PCMP for " + purpose + " and " + contactParty); + throw new GenericEntityException("Could not find a unique active PCMP for purpose " + purpose + " and party id " + contactParty.get("partyId")); + } + else if(UtilValidate.isEmpty(activePCMPs)) + { + return null; + } + //it must be olny one element + return activePCMPs.get(0); + } + + /** + * Insert (or updated if already exists) a TelecomNumber for given contactParty and contactMechPurposeType. + * + * TODO: pull phone out via a query string/jxpath + * + * @param phoneType - FBol type string - this method does NOTHING if <code>phone</code> does not match this type + * @throws SyncSourceException + */ + private void updateOrInsertTelecomNumberOrEmail(GenericValue contactParty, Contact contact, TypifiedProperty phoneOrEmail, String phoneOrEmailPurpose, String phoneOrEmailType) + throws GenericEntityException + { + if(phoneOrEmail.getPropertyType().equals(phoneOrEmailType)) + { + GenericValue phoneGV = getActivePartyContactMechPurpose(contactParty, phoneOrEmailPurpose); + if(phoneGV == null) //it is NEW + { + if(phoneOrEmail instanceof Phone) + { + _syncSource.insertContactMech((String)contactParty.get("partyId"), contact, phoneOrEmailPurpose, "TELECOM_NUMBER"); + } + else if(phoneOrEmail instanceof Email) + { + _syncSource.insertContactMech((String)contactParty.get("partyId"), contact, phoneOrEmailPurpose, "EMAIL_ADDRESS"); + } + log.warn("created TelecomNumber as part of update"); + } + else //it is UPDATED TODO: use services instead of direct storage + { + if(phoneOrEmail instanceof Phone) + { + _converters.toEntityAndStore(contact, phoneGV.getRelatedOne("TelecomNumber"), phoneOrEmailPurpose); + } + else if(phoneOrEmail instanceof Email) + { + _converters.toEntityAndStore(contact, phoneGV.getRelatedOne("ContactMech"), phoneOrEmailPurpose); + } + } + } + } + + /** + * Build an FBol Phone bean for the given contactParty's data contactMechPurposeType and FBol phone type + * + * TODO: factor out + * TODO: cc-ac-num parsing + * + * @return null if the contactParty has no such data + */ + private void buildPhone(List<Phone> phones, GenericValue contactParty, String phonePurpose, String phoneType) throws GenericEntityException + { + Phone phone = null; + //TODO: does CRMSFA have a service to get only active contact mech shite for a certain Party + //GenericValue phoneGV = contactParty.getRelated("PartyContactMechPurpose[contactMechPurposeTypeId=" + phonePurpose + "]"); + List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", phonePurpose, "thruDate", null)); + if(activePCMPs.size() != 1) { log.warn("Could not find a unique active PCMP for " + phonePurpose + " and " + contactParty); } + else + { + GenericValue phoneGV = activePCMPs.get(0); + GenericValue phoneNumberGV = phoneGV.getRelatedOne("TelecomNumber"); + if(phoneNumberGV != null) + { + phone = new Phone(); + phone.setPropertyType(phoneType); + + String country = phoneNumberGV.getString("countryCode"); + if(UtilValidate.isEmpty(country)) + { + country=""; + } + + String area = phoneNumberGV.getString("areaCode"); + if(UtilValidate.isEmpty(country)) + { + area=""; + } + else + { + area = " ("+area+") "; + } + String contact = phoneNumberGV.getString("contactNumber"); + if(UtilValidate.isEmpty(country)) + { + contact=""; + } + phone.setPropertyValue(country+area+contact); + phones.add(phone); + } + else { log.warn(phonePurpose + " without TelecomNumber"); } + } + } + + /** + * Set a Funambol PIM property with a certain value, only if the value is not null or empty + */ + private void setIfNotEmpty(Property property, Object value) + { + if(!UtilValidate.isEmpty(value)) + { + property.setPropertyValue(value); + } + } } Modified: versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncHandler.java =================================================================== --- versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncHandler.java 2007-07-23 09:40:22 UTC (rev 3932) +++ versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncHandler.java 2007-07-23 10:59:07 UTC (rev 3933) @@ -23,7 +23,7 @@ import com.funambol.framework.engine.source.SyncSource; /** - * Represents a processor that knows about: + * Represents a processor that knows about syncing records whose remote bean type is <E> * Entitys and Relations * Their mappings to beans * SyncSourceExceptions TODO: does it really have to know this? @@ -35,15 +35,16 @@ * * Each instance will only be used by one thread. * - * @author Cameron Smith - Database, Lda - www.database.co.mz - * - * TODO: parameterize so we can don't have to use Object in method signatures - * + * @author Cameron Smith - Database, Lda - www.database.co.mz * */ -public interface EntitySyncHandler +public interface EntitySyncHandler<E> { + //=== initialization === + public void init(EntitySyncSource syncSource); + //=== methods which map directly to MergeableSyncSource methods === + /** * @see SyncSource.getDeletedItemKeys * @return a sequence of the PKs of Entities which should be deleted in the client @@ -52,10 +53,21 @@ */ public Iterable<String> getDeletedKeys(Timestamp since) throws GeneralException; + public E getItemFromId(String key) throws GeneralException; + public void removeItem(String key) throws GeneralException; /** * Update the OT record with the given PK, from the given source Java Bean */ - public void updateItem(String key, Object source) throws GeneralException; + public void updateItem(String key, E source) throws GeneralException; + + /** + * Merge the remoteItem with the data in the entity represented by the given key. + * + * @param key PK of entity to be merged + * @param source remote bean to be merged + * @return true if source was changed, ie remote client needs to re-update with it + */ + public boolean mergeItem(String key, E source) throws GeneralException; } Modified: versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncSource.java =================================================================== --- versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncSource.java 2007-07-23 09:40:22 UTC (rev 3932) +++ versions/1.0/trunk/hot-deploy/funambol/src/org/opentaps/funambol/sync/EntitySyncSource.java 2007-07-23 10:59:07 UTC (rev 3933) @@ -49,21 +49,19 @@ import org.opentaps.funambol.security.OFBizSync4jUser; import org.springframework.context.ApplicationContext; -import com.funambol.common.pim.common.TypifiedProperty; import com.funambol.common.pim.contact.Address; import com.funambol.common.pim.contact.Contact; import com.funambol.common.pim.contact.Email; -import com.funambol.common.pim.contact.Name; -import com.funambol.common.pim.contact.Phone; import com.funambol.common.pim.converter.ContactToVcard; +import com.funambol.common.pim.vcard.ParseException; import com.funambol.common.pim.vcard.VcardParser; -import com.funambol.foundation.exception.EntityException; 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.AbstractSyncSource; import com.funambol.framework.engine.source.ContentType; +import com.funambol.framework.engine.source.MergeableSyncSource; import com.funambol.framework.engine.source.SyncContext; import com.funambol.framework.engine.source.SyncSource; import com.funambol.framework.engine.source.SyncSourceException; @@ -83,10 +81,9 @@ * * @author Cameron Smith, Eurico da Silva - Database, Lda - www.database.co.mz */ -public class EntitySyncSource extends AbstractSyncSource implements SyncSource, Serializable, LazyInitBean +public class EntitySyncSource extends AbstractSyncSource implements MergeableSyncSource, Serializable, LazyInitBean { FunambolLogger log = null; //TODO: can this be static? - EntitySyncSourceHelper _helper = null; //objects representing the user and principal performing the sync protected Principal _principal = null; @@ -259,143 +256,17 @@ { //1. load up the relevant Party and subrecords String partyId = syncItemKey.getKeyAsString(); - log.warn("Will look for this partyId: " + partyId); - GenericValue contactPartyGV = findByPrimaryKey("Party", UtilMisc.toMap("partyId", partyId)); - EntityPreparer contactParty = new EntityPreparer(contactPartyGV); - Contact contact = new Contact(); //TODO: use EBConverter in this direction too, but first need a bean preparer - - //1.1 Person (for Lead/Contact) or PartyGroup (for Account) - Object partyType = contactParty.get("partyTypeId"); - - GenericValue person = contactParty.getRelated("Person"); - GenericValue pGroup = contactParty.getRelated("PartyGroup"); - GenericValue pSupp = contactParty.getRelated("PartySupplementalData"); - - //2. convert Entity -> Contact - if(person!=null) - { - Name name = new Name(); - name.getFirstName().setPropertyValue(person.get("firstName")); - name.getLastName().setPropertyValue(person.get("lastName")); - contact.setName(name); - } - - //2. convert Entity -> Contact - - String company = ""; - if(pGroup!=null) - { - company = pGroup.getString("groupName"); - } - else if(pSupp!=null) - { - company = pSupp.getString("companyName"); - } - - contact.getBusinessDetail().getCompany().setPropertyValue(company); - - //3. now deal with the contact numbers for this party - buildPhone(contact.getBusinessDetail().getPhones(), contactPartyGV, "PRIMARY_PHONE", "BusinessTelephoneNumber"); - buildPhone(contact.getPersonalDetail().getPhones(),contactPartyGV, "PHONE_HOME", "HomeTelephoneNumber"); - //TODO (after defining purposeType seed data) - // BusinessFaxNumber - buildPhone(contact.getBusinessDetail().getPhones(), contactPartyGV, "FAX_NUMBER", "BusinessFaxTelephoneNumber"); - // MobileTelephoneNumber - buildPhone(contact.getPersonalDetail().getPhones(), contactPartyGV, "PHONE_MOBILE", "MobileTelephoneNumber"); - - //address - GenericValue purpose = getPurposeFromParty(contactPartyGV, "PRIMARY_LOCATION"); - if(purpose!= null) - { - GenericValue postalAddress = purpose.getRelatedOne("PostalAddress"); - if(postalAddress!=null) - { - String - city = postalAddress.getString("city"), - country = postalAddress.getRelatedOne("CountryGeo").getString("geoName"), - postal = postalAddress.getString("postalCode"), - postOffice = postalAddress.getString("address2"), - street = postalAddress.getString("address1"), - state = postalAddress.getRelatedOne("StateProvinceGeo").getString("geoName"); - - Address address = contact.getBusinessDetail().getAddress(); - - if(!UtilValidate.isEmpty(city)) - { - - address.getCity().setPropertyValue(city); - } - - if(!UtilValidate.isEmpty(country)) - { - address.getCountry().setPropertyValue(country); - } - - if(!UtilValidate.isEmpty(postal)) - { - address.getPostalCode().setPropertyValue(postal); - } - - if(!UtilValidate.isEmpty(postOffice)) - { - address.getPostOfficeAddress().setPropertyValue(postOffice); - } - - if(!UtilValidate.isEmpty(street)) - { - address.getStreet().setPropertyValue(street); - } - - if(!UtilValidate.isEmpty(state)) - { - address.getState().setPropertyValue(state); - } - } - - } - - //email - purpose = getPurposeFromParty(contactPartyGV, "PRIMARY_EMAIL"); - prepareContactMail(purpose,contact, "Email1Address"); - - purpose = getPurposeFromParty(contactPartyGV, "OTHER_EMAIL"); - prepareContactMail(purpose,contact, "OtherEmail2Address"); - - purpose = getPurposeFromParty(contactPartyGV, "OTHER_EMAIL_SEC"); - prepareContactMail(purpose,contact, "OtherEmail3Address"); - //4. convert Contact -> vcard and return - return createItem(partyId, contact2vcard(contact), SyncItemState.UNKNOWN); + Contact contact = (Contact)_handler.getItemFromId(partyId); + + //2. convert Contact -> vcard and return + return createItem(partyId, contact2vcard(contact), SyncItemState.UNKNOWN); //TODO: what state should we set here? } - catch (GenericEntityException delegatorX) + catch (GeneralException getX) { - throw new SyncSourceException("Could not load all data for Party " + syncItemKey, delegatorX); + abort("Could not load all data for Party " + syncItemKey, getX); return null; } } - private void prepareContactMail(GenericValue purpose, Contact contact, String mailType) throws GenericEntityException - { - if(purpose!= null) - { - String otEmail = ((GenericValue)purpose.getRelatedOne("ContactMech")).getString("infoString"); - otEmail = otEmail==null?"":otEmail; - Email email = new Email(); - email.setPropertyType(mailType); - email.setPropertyValue(otEmail); - contact.getPersonalDetail().getEmails().add(email); - } - } - - private GenericValue getPurposeFromParty(GenericValue contactParty, String purposeType) throws GenericEntityException, SyncSourceException - { - List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", purposeType, "thruDate", null)); - - if(activePCMPs.size() > 1){log.warn("Could not find a unique active PCMP for " + purposeType + " and " + contactParty);} - - if(activePCMPs.size() == 0){return null;} - - return activePCMPs.get(0); - - } /* * TODO: NOT YET TESTED * @see SyncSource @@ -431,82 +302,19 @@ String partyId = syncItem.getKey().getKeyAsString(); try - { - GenericValue contactPartyGV = findByPrimaryKey("Party", UtilMisc.toMap("partyId", partyId)); - EntityPreparer contactParty = new EntityPreparer(contactPartyGV); - - contact = vcard2Contact(new String(syncItem.getContent())); - addressCompatibilization(contact); + { + contact = vcard2Contact(syncItem); + addressCompatibilization(contact); //TODO: this must also go into handler + //2.1 if we got here we are going to alter OT data so start a transaction begin(); - //2.2 update Person/PartyGroup TODO:complete + //2.2 perform the update specific to this kind of record _handler.updateItem(partyId, contact); - - //2.3 postal address - updateOrInsertPostalAddress(contactPartyGV, contact, "PRIMARY_LOCATION"); - - //emails - for(Email email : (List<Email>)contact.getPersonalDetail().getEmails()) - { - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, email, "PRIMARY_EMAIL", "Email1Address"); - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, email, "OTHER_EMAIL", "OtherEmail2Address"); - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, email, "OTHER_EMAIL_SEC", "OtherEmail3Address"); - } - - //update TelecomNumber TODO: use bean query string to avoid need for loop - //why partyContactMechpurs.get(0)? - a purpose(PHONE_WORK) is related to only one contactMech(TelecomNumber) - for(Phone phone : (List<Phone>)contact.getBusinessDetail().getPhones()) - { - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, phone, "PRIMARY_PHONE", "BusinessTelephoneNumber"); - //TODO (after defining purposeType seed data) - // BusinessFaxNumber - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, phone, "FAX_NUMBER", "BusinessFaxTelephoneNumber"); - } - for(Phone phone : (List<Phone>)contact.getPersonalDetail().getPhones()) - { - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, phone, "PHONE_HOME", "HomeTelephoneNumber"); - //TODO (after defining purposeType seed data) - //MobileTelephoneNumber - updateOrInsertTelecomNumberOrEmail(contactPartyGV, contact, phone, "PHONE_MOBILE", "MobileTelephoneNumber"); - } - - //update Person/PartyGroup TODO:complete - GenericValue partyRelation = contactParty.getRelated("FromPartyRelationship"); - GenericValue person = contactParty.getRelated("Person"); - GenericValue pGroup = contactParty.getRelated("PartyGroup"); - GenericValue pSupp = contactParty.getRelated("PartySupplementalData"); - - - //2. convert Entity -> Contact - if(person!=null && (partyRelation.getString("roleTypeIdFrom").equals("CONTACT") - || partyRelation.getString("roleTypeIdFrom").equals("PROSPECT"))) - { - _converters.toEntityAndStore(contact, person); - - if(partyRelation.getString("roleTypeIdFrom").equals("PROSPECT")) - { - _converters.toEntityAndStore(contact, pSupp); - } - } - else if(pGroup!=null && partyRelation.getString("roleTypeIdFrom").equals("ACCOUNT")) - { - _converters.toEntityAndStore(contact, pGroup); - } - - //_converters.toEntityAndStore(contact, partyGroupOrPerson, partyRelation.getString("roleTypeIdFrom")); - + //2. store everything commit(); } - catch (GenericTransactionException e) //TODO: an abort method which logs, rolls back, then causes FBol to note appropriate error message - { - abort(e); - } - catch (EntityException e) - { - abort(e); - } catch (GeneralException e) { abort(e); @@ -531,8 +339,39 @@ null //timestamp ); } + + /** + * Merge the remoteItem with the data in the record represented by the given key. + * + * @param syncItemKey PK of local item to be merged + * @param remoteItem remote item to be merged + * @return true if remoteItem was changed, ie remote client needs to re-update with it + */ + public boolean mergeSyncItems(SyncItemKey localKey, SyncItem remoteItem) throws SyncSourceException + { + //1. convert record + Object remoteBean = vcard2Contact(remoteItem); + + //2. perform merge + begin(); //TODO: make the handler a Spring @Transactional proxied object + + try + { + boolean remoteBeanChanged = _handler.mergeItem(localKey.getKeyAsString(), remoteBean); //2.1 server-side merge + + if(remoteBeanChanged) //2.2 server changed remote bean so we need to pass changes back to remoteItem + { + remoteItem.setContent(contact2vcard((Contact)remoteBean)); + //TODO: do we also need to set its status to MERGED or something? + } + + commit(); //2.3 all data manipulation was successful so commit our work + + return remoteBeanChanged; //inform DSS + } + catch(GeneralException removeX) { abort(removeX); return false; } + } - /** * @throws SyncSourceException - always */ @@ -543,63 +382,17 @@ try { rollback(); //TODO: only rollback if it wasnt a TransactionException? - }catch (Exception x) { - log.error("Rollback failed:"+x.getMessage()); - } + } + catch (Exception x) { log.error("Rollback failed:"+x.getMessage()); } - throw new SyncSourceException(message, e); - + throw new SyncSourceException(message, e); } private void abort(Exception e) throws SyncSourceException { abort("NO MESSAGE", e); } - - private void insertContactMech(String partyId, Contact contact, String purpose, String contactMechType) throws GenericEntityException, SyncSourceException - { - if(contactMechType.equals("POSTAL_ADDRESS") && !ContactUtils.hasAddress(contact, purpose)){return;} - - //create the basic ContactMech which holds everything together - String contactMechId = _delegator.getNextSeqId("ContactMech"); - Map createCMParams = UtilMisc.toMap("contactMechId", contactMechId, "contactMechTypeId", contactMechType); - GenericValue contactMech = _delegator.create("ContactMech",createCMParams); - - - //now the telecom number - EntityPreparer contactPreparer = new EntityPreparer(contactMech, true); - if(contactMechType.equals("TELECOM_NUMBER")) - { - _converters.toEntityAndStore(contact, contactPreparer.getRelated("TelecomNumber"), purpose); - } - else if(contactMechType.equals("POSTAL_ADDRESS")) - { - _converters.toEntityAndStore(contact, contactPreparer.getRelated("PostalAddress"), purpose); - } - else if(contactMechType.equals("EMAIL_ADDRESS")) - { - Email email = getContacMailFromPurpose(purpose, contact.getPersonalDetail().getEmails()); - if(email!=null) - { - contactMech.set("infoString", email.getPropertyValueAsString()); - contactMech.store(); - } - } - else - { - abort(new IllegalArgumentException("ContactMechType "+ contactMechType + " it's not implemented.")); - } - - //create the PartyContactMech which relates person to mech - _delegator.create("PartyContactMech", - UtilMisc.toMap("partyId", partyId, "contactMechId", contactMechId, "fromDate", now())); - - //finally the purpose - _delegator.create("PartyContactMechPurpose", - UtilMisc.toMap("partyId", partyId, "contactMechId", contactMechId, "contactMechPurposeTypeId", purpose, "fromDate", now())); - log.info("created PartyContactMech"); - } - + private Email getContacMailFromPurpose(String purpose, List<Email> emails) { if(emails!=null) @@ -641,7 +434,7 @@ try { - contact = vcard2Contact(new String(syncItem.getContent())); + contact = vcard2Contact(syncItem); addressCompatibilization(contact); //2.1 if we got here we are going to alter OT data so start a transaction begin(); @@ -651,7 +444,7 @@ Map relParams = new HashMap(); //TODO: Ask cameron, should this method return the map? or it's good void! - _helper.prepateServiceCreateRelationshipMap(contact, relParams); + EntitySyncSourceHelper.prepateServiceCreateRelationshipMap(contact, relParams); Map contactInfo = runSync("crmsfa.create"+ContactUtils.contactType(contact), relParams); String partyId = (String)contactInfo.get("partyId"); GenericValue contactParty = _delegator.findByPrimaryKey("Party", UtilMisc.toMap("partyId", partyId)); @@ -682,12 +475,7 @@ //8. return the original SyncItem as it was not altered in any way return newSyncItem; - } - catch (EntityException e) //TODO: make a standard abort mechanism here - { - log.error("---ContactERROR---: "+e); - abort(e); return null; - } + } catch(GeneralException badOTX) //TODO: does throwing an X here cause whole sync to fail? { abort(badOTX); return null; @@ -703,45 +491,45 @@ /** * @see SyncSource + * + * TODO: not yet implemented */ - public SyncItemKey[] getSyncItemKeysFromTwin(SyncItem syncItem) - throws SyncSourceException { - + public SyncItemKey[] getSyncItemKeysFromTwin(SyncItem syncItem) throws SyncSourceException + { return new SyncItemKey[0]; } /** * @see SyncSource + * TODO: do we need to pay attention to this */ - public void setOperationStatus(String operation, int statusCode, SyncItemKey[] keys) { - + public void setOperationStatus(String operation, int statusCode, SyncItemKey[] keys) + { StringBuffer message = new StringBuffer("Received status code '"); message.append(statusCode).append("' for a '").append(operation).append("'"). append(" for this items: "); - for (int i = 0; i < keys.length; i++) { - message.append("\n- " + keys[i].getKeyAsString()); - } + for (int i = 0; i < keys.length; i++) { message.append("\n- " + keys[i].getKeyAsString()); } log.info(message.toString()); } //=== private behaviour === - private SyncItem createItem(String id, String content, char state) + private SyncItem createItem(String id, byte[] content, char state) { log.info("Creating a SyncItem with: {key= " + id + ", content= " + content + ", state= " + state + "}"); SyncItem item = new SyncItemImpl(this, id, state); - item.setContent(content.getBytes()); + item.setContent(content); item.setType(info.getPreferredType().type); return item; } //TODO:Automatizar no bean - private void addressCompatibilization(Contact contact) throws EntityException, GenericEntityException + private void addressCompatibilization(Contact contact) throws GenericEntityException { Address[] addresses = { contact.getBusinessDetail().getAddress(), @@ -789,16 +577,15 @@ /** * Copied from PIMSyncSource.vcard2Contact * TODO: refactor and make more generic, put in a centralized place + * TODO: verify that vcardItem really has correct mimetype * * @param vcard - * @return - * @throws EntityException + * @return a Contact bean populated from the data in the vcard + * @throws SyncSourceException if conversion was not possible */ - private Contact vcard2Contact(String vcard) throws EntityException + private Contact vcard2Contact(SyncItem vcardItem) throws SyncSourceException { - StringBuilder sb = new StringBuilder("Converting: VCARD => Contact"); - sb.append("\nINPUT = {").append(vcard).append('}'); - log.warn(sb.toString()); + log.info("Converting: VCARD => Contact: " + vcardItem.getKey()); ByteArrayInputStream buffer = null; VcardParser parser = null; @@ -807,20 +594,23 @@ { contact = new Contact(); - buffer = new ByteArrayInputStream(vcard.getBytes()); - if ((vcard.getBytes()).length > 0) { + buffer = new ByteArrayInputStream(vcardItem.getContent()); + if (buffer.available() > 0) + { parser = new VcardParser(buffer); contact = (Contact) parser.vCard(); - } else { - throw new EntityException("No data"); } - } catch (EntityException e){ - throw e; - } catch (Exception e){ - throw new EntityException("Error converting VCARD to Contact. ", e); + else + { + throw new SyncSourceException("VCARD contained no data"); //TODO: error message and code + } } + catch (ParseException parseX) + { + abort("Error converting VCARD to Contact. ", parseX); + } - log.warn("Conversion done."); + log.info("Conversion done."); return contact; } @@ -830,10 +620,10 @@ * TODO: refactor and make more generic, put in a centralized place * * @param vcard - * @return - * @throws EntityException + * @return vcard converted to binary data + * @throws SyncSourceException */ - public String contact2vcard(Contact contact) + public byte[] contact2vcard(Contact contact) throws SyncSourceException { log.info("Converting: Contact => VCARD"); @@ -843,16 +633,14 @@ ContactToVcard c2vc = new ContactToVcard(null,null); vcard = c2vc.convert(contact); - log.warn("OUTPUT = {" + vcard + "}"); - - log.info("Conversion done"); + log.info("Conversion done, OUTPUT = {" + vcard + "}"); } - catch (Exception vcardX) //TODO: throw an exception here + catch (Exception vcardX) { - log.error("Could not convert " + contact + " to vcard", vcardX); + abort("Could not convert " + contact + " to vcard", vcardX); } - return vcard; + return vcard.getBytes(); } //=== behaviour for related objects to use: TODO: any protected methods should be moved to appropriate handler, or to EntitySyncSourceHelper === @@ -873,138 +661,50 @@ return _dispatcher.runSync(service, params); } - - /** - * Build an FBol Phone bean for the given contactParty's data contactMechPurposeType and FBol phone type - * - * TODO: factor out - * TODO: cc-ac-num parsing - * - * @return null if the contactParty has no such data - */ - private void buildPhone(List<Phone> phones, GenericValue contactParty, String phonePurpose, String phoneType) throws GenericEntityException + + protected void insertContactMech(String partyId, Contact contact, String purpose, String contactMechType) throws GenericEntityException { - Phone phone = null; - //TODO: does CRMSFA have a service to get only active contact mech shite for a certain Party - //GenericValue phoneGV = contactParty.getRelated("PartyContactMechPurpose[contactMechPurposeTypeId=" + phonePurpose + "]"); - List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", phonePurpose, "thruDate", null)); - if(activePCMPs.size() != 1) { log.warn("Could not find a unique active PCMP for " + phonePurpose + " and " + contactParty); } - else + if(contactMechType.equals("POSTAL_ADDRESS") && !ContactUtils.hasAddress(contact, purpose)){return;} + + //create the basic ContactMech which holds everything together + String contactMechId = _delegator.getNextSeqId("ContactMech"); + Map createCMParams = UtilMisc.toMap("contactMechId", contactMechId, "contactMechTypeId", contactMechType); + GenericValue contactMech = _delegator.create("ContactMech",createCMParams); + + //now the telecom number + EntityPreparer contactPreparer = new EntityPreparer(contactMech, true); + if(contactMechType.equals("TELECOM_NUMBER")) { - GenericValue phoneGV = activePCMPs.get(0); - GenericValue phoneNumberGV = phoneGV.getRelatedOne("TelecomNumber"); - if(phoneNumberGV != null) - { - phone = new Phone(); - phone.setPropertyType(phoneType); - - String country = phoneNumberGV.getString("countryCode"); - if(UtilValidate.isEmpty(country)) - { - country=""; - } - - String area = phoneNumberGV.getString("areaCode"); - if(UtilValidate.isEmpty(country)) - { - area=""; - } - else - { - area = " ("+area+") "; - } - String contact = phoneNumberGV.getString("contactNumber"); - if(UtilValidate.isEmpty(country)) - { - contact=""; - } - phone.setPropertyValue(country+area+contact); - phones.add(phone); - } - else { log.warn(phonePurpose + " without TelecomNumber"); } + _converters.toEntityAndStore(contact, contactPreparer.getRelated("TelecomNumber"), purpose); } - } - - private GenericValue getActivePartyContactMechPurpose(GenericValue contactParty, String purpose) throws GenericEntityException, SyncSourceException - { - List<GenericValue> activePCMPs = contactParty.getRelatedByAnd("PartyContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", purpose, "thruDate", null)); - if(activePCMPs.size() > 1) + else if(contactMechType.equals("POSTAL_ADDRESS")) { - log.warn("Could not find a unique active PCMP for " + purpose + " and " + contactParty); - throw new SyncSourceException("Could not find a unique active PCMP for purpose " + purpose + " and party id " + contactParty.get("partyId")); + _converters.toEntityAndStore(contact, contactPreparer.getRelated("PostalAddress"), purpose); } - else if(UtilValidate.isEmpty(activePCMPs)) + else if(contactMechType.equals("EMAIL_ADDRESS")) { - return null; + Email email = getContacMailFromPurpose(purpose, contact.getPersonalDetail().getEmails()); + if(email!=null) + { + contactMech.set("infoString", email.getPropertyValueAsString()); + contactMech.store(); + } } - //it must be olny one element - return activePCMPs.get(0); - } - /** - * Insert (or updated if already exists) a TelecomNumber for given contactParty and contactMechPurposeType. - * - * TODO: pull phone out via a query string/jxpath - * - * @param phoneType - FBol type string - this method does NOTHING if <code>phone</code> does not match this type - * @throws SyncSourceException - */ - private void updateOrInsertTelecomNumberOrEmail(GenericValue contactParty, Contact contact, TypifiedProperty phoneOrEmail, String phoneOrEmailPurpose, String phoneOrEmailType) - throws GenericEntityException, SyncSourceException - { - if(phoneOrEmail.getPropertyType().equals(phoneOrEmailType)) - { - GenericValue phoneGV = getActivePartyContactMechPurpose(contactParty, phoneOrEmailPurpose); - if(phoneGV == null) //it is NEW - { - if(phoneOrEmail instanceof Phone) - { - insertContactMech((String)contactParty.get("partyId"), contact, phoneOrEmailPurpose, "TELECOM_NUMBER"); - } - else if(phoneOrEmail instanceof Email) - { - insertContactMech((String)contactParty.get("partyId"), contact, phoneOrEmailPurpose, "EMAIL_ADDRESS"); - } - log.warn("created TelecomNumber as part of update"); - } - else //it is UPDATED - { - //Map updateParams = UtilMisc.toMap("contactNumber", phone.getPropertyValueAsString(), "contactMechId", busPhoneGV.get("contactMechId")); - //runSync("updateTelecomNumber", updateParams); //TODO: this service does not appear to work - if(phoneOrEmail instanceof Phone) - { - _converters.toEntityAndStore(contact, phoneGV.getRelatedOne("TelecomNumber"), phoneOrEmailPurpose); - } - else if(phoneOrEmail instanceof Email) - { - _converters.toEntityAndStore(contact, phoneGV.getRelatedOne("ContactMech"), phoneOrEmailPurpose); - } - } - - } - } - - /** - * - * Insert (or updated if already exists) a TelecomNumber for given contactParty and contactMechPurposeType. - * - * @param contactParty - * @param contact - * @param purpose - A ContactMechPurposeType value - * @throws GenericEntityException - * @throws SyncSourceException - */ - private void updateOrInsertPostalAddress(GenericValue contactParty, Contact contact, String purpose) throws GenericEntityException, SyncSourceException - { - GenericValue postalAddrGV = getActivePartyContactMechPurpose(contactParty, purpose); - if(postalAddrGV == null) - { - insertContactMech((String)contactParty.get("partyId"), contact, purpose, "POSTAL_ADDRESS"); - } else { - _converters.toEntityAndStore(contact, postalAddrGV.getRelatedOne("PostalAddress"), purpose); + throw new IllegalArgumentException("ContactMechType "+ contactMechType + " it's not implemented."); } - } + + //create the PartyContactMech which relates person to mech + _delegator.create("PartyContactMech", + UtilMisc.toMap("partyId", partyId, "contactMechId", contactMechId, "fromDate", now())); + + //finally the purpose + _delegator.create("PartyContactMechPurpose", + UtilMisc.toMap("partyId", partyId, "contactMechId", contactMechId, "contactMechPurposeTypeId", purpose, "fromDate", now())); + log.info("created PartyContactMech"); + } + /** * Convenience method for calling findByPK * Modified: versions/1.0/trunk/hot-deploy/funambol/webapp/WEB-INF/opentaps-sync-config.xml =================================================================== --- versions/1.0/trunk/hot-deploy/funambol/webapp/WEB-INF/opentaps-sync-config.xml 2007-07-23 09:40:22 UTC (rev 3932) +++ versions/1.0/trunk/hot-deploy/funambol/webapp/WEB-INF/opentaps-sync-config.xml 2007-07-23 10:59:07 UTC (rev 3933) @@ -26,7 +26,7 @@ <bean class="mz.co.dbl.siga.framework.util.SystemPropertiesSetter"> <property name="mappings"> <map> - <entry key="funambol.ds.home" value="D:/Development/workspace/opentaps2/hot-deploy/funambol"/> + <entry key="funambol.ds.home" value="/home/cameron/workspace/opentaps/hot-deploy/funambol"/> </map> </property> </bean> @@ -98,7 +98,7 @@ stateProvinceGeoId = personalDetail.address.state </value> </entry> - <entry key="PostalAddress:com.funambol.common.pim.contact.Contact:PRIMARY_LOCATION"> + <entry key="PostalAddress:com.funambol.common.pim.contact.Contact:GENERAL_LOCATION"> <value> address1 = businessDetail.address.street address2 = businessDetail.address.postOfficeAddress @@ -138,13 +138,6 @@ groupNameLocal=businessDetail.company </value> </entry> - <entry key="PartyGroup:com.funambol.common.pim.contact.Contact"> <!-- TODO: above --> - <value> - groupName=businessDetail.company - groupNameLocal=businessDetail.company - </value> - </entry> - <entry key="Party:com.funambol.common.pim.contact.Contact:CONTACT"> <!-- service: crmsfa.updateContact --> <value> firstName=name.firstName @@ -153,15 +146,6 @@ lastNameLocal=name.lastName </value> </entry> - <entry key="Person:com.funambol.common.pim.contact.Contact"> <!-- TODO: above --> - <value> - firstName=name.firstName - lastName=name.lastName - firstNameLocal=name.firstName - lastNameLocal=name.lastName - </value> - </entry> - <entry key="Party:com.funambol.common.pim.contact.Contact:PROSPECT"> <!-- service: crmsfa.updateLead --> <value> firstName=name.firstName @@ -170,13 +154,7 @@ lastNameLocal=name.lastName companyName=businessDetail.company </value> - </entry> - <entry key="PartySupplementalData:com.funambol.common.pim.contact.Contact"> <!-- TODO: above --> - <value> - companyName=businessDetail.company - </value> - </entry> - + </entry> </map> </property> </bean> |