diff --git a/.liquibase/Data_alias/basic/2021.0.0/EwsContactSync/create_ab_synccontact.xml b/.liquibase/Data_alias/basic/2021.0.0/EwsContactSync/create_ab_synccontact.xml new file mode 100644 index 0000000000000000000000000000000000000000..9de2f01cc35fba70afd6992a3995da9c8e1b7431 --- /dev/null +++ b/.liquibase/Data_alias/basic/2021.0.0/EwsContactSync/create_ab_synccontact.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd"> + <changeSet author="A.Riedl" id="9820d74b-1b58-4ffd-9f5d-8c4d670bda47"> + <createTable tableName="AB_SYNCCONTACT"> + <column name="SYNCCONTACTID" type="char(36)"> + <constraints primaryKey="true" primaryKeyName="PK_SYNCCONTACTID"/> + </column> + <column name="ASYS_FAVORITEID" type="char(36)"/> + <column name="EXCHANGEID" type="varchar(254)"/> + <column name="USER_ID" type="char(46)"/> + <column name="CONTACT_ID" type="char(36)"/> + <column name="DATE_DEL" type="datetime"/> + <column name="DATE_EDIT" type="datetime"/> + <column name="DATE_NEW" type="datetime"/> + </createTable> + <createIndex indexName="idx_exchangeId" tableName="AB_SYNCCONTACT"> + <column name="EXCHANGEID"/> + </createIndex> + <createIndex tableName="AB_SYNCCONTACT" indexName="idx_UserId"> + <column name="USER_ID"/> + </createIndex> + <createIndex tableName="AB_SYNCCONTACT" indexName="idx_ContactId"> + <column name="CONTACT_ID"/> + </createIndex> + <addUniqueConstraint + columnNames="ASYS_FAVORITEID" + constraintName="const_uniqueFavoriteId" + tableName="AB_SYNCCONTACT"/> + </changeSet> +</databaseChangeLog> diff --git a/.liquibase/Data_alias/basic/2021.0.0/changelog.xml b/.liquibase/Data_alias/basic/2021.0.0/changelog.xml index 489408fcecc12e61ad7e9f689e82a9aa995e70dd..610a807e257c9da7317a73c9ec0852d04af3956d 100644 --- a/.liquibase/Data_alias/basic/2021.0.0/changelog.xml +++ b/.liquibase/Data_alias/basic/2021.0.0/changelog.xml @@ -12,4 +12,5 @@ <include relativeToChangelogFile="true" file="SalesprojectConversion/changelog.xml"/> <include relativeToChangelogFile="true" file="Checklists/changelog.xml"/> <include relativeToChangelogFile="true" file="addDateNewToSalesproject.xml"/> + <include relativeToChangelogFile="true" file="EwsContactSync/create_ab_synccontact.xml"/> </databaseChangeLog> \ No newline at end of file diff --git a/entity/EwsSyncAddContacts_entity/EwsSyncAddContacts_entity.aod b/entity/EwsSyncAddContacts_entity/EwsSyncAddContacts_entity.aod new file mode 100644 index 0000000000000000000000000000000000000000..332169d840fba95e4b9a7507ede82835e4fda87e --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/EwsSyncAddContacts_entity.aod @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entity xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.3.18" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/entity/1.3.18"> + <name>EwsSyncAddContacts_entity</name> + <title>add contact to ewssync</title> + <majorModelMode>DISTRIBUTED</majorModelMode> + <titlePlural></titlePlural> + <recordContainer>datalessConfig</recordContainer> + <entityFields> + <entityProvider> + <name>#PROVIDER</name> + </entityProvider> + <entityProvider> + <name>#PROVIDER_AGGREGATES</name> + <useAggregates v="true" /> + </entityProvider> + <entityField> + <name>contactIds</name> + <state>EDITABLE</state> + <valueProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/contactids/valueProcess.js</valueProcess> + </entityField> + <entityField> + <name>countForSync</name> + <title>count</title> + <state>EDITABLE</state> + <valueProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/countforsync/valueProcess.js</valueProcess> + </entityField> + <entityParameter> + <name>ContactFilter_param</name> + <expose v="true" /> + </entityParameter> + <entityParameter> + <name>ContactIds_param</name> + <expose v="true" /> + </entityParameter> + <entityActionField> + <name>syncContacts</name> + <title>add to sync</title> + <onActionProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/onActionProcess.js</onActionProcess> + <iconId>VAADIN:REFRESH</iconId> + <stateProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/stateProcess.js</stateProcess> + <tooltip></tooltip> + </entityActionField> + <entityParameter> + <name>Mode_param</name> + <expose v="true" /> + </entityParameter> + <entityActionField> + <name>removeSyncContacts</name> + <title>remove from sync</title> + <onActionProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/onActionProcess.js</onActionProcess> + <isObjectAction v="true" /> + <iconId>NEON:RECURRING_APPOINTMENT_MOVED</iconId> + <stateProcess>%aditoprj%/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/stateProcess.js</stateProcess> + </entityActionField> + </entityFields> + <recordContainers> + <datalessRecordContainer> + <name>datalessConfig</name> + <alias>Data_alias</alias> + </datalessRecordContainer> + </recordContainers> +</entity> diff --git a/entity/EwsSyncAddContacts_entity/entityfields/contactids/valueProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/contactids/valueProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..373f61e18faccaead7724225f920d46437148b77 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/contactids/valueProcess.js @@ -0,0 +1,10 @@ +import("system.result"); +import("system.vars"); +import("FilterViewAction_lib"); + +var contactIds = JSON.parse(vars.get("$param.ContactIds_param")); +var contactFilter = vars.get("$param.ContactFilter_param"); + +contactIds = FilterViewActionUtils.getUidsBySelectionOrFilter("Person", contactIds, contactFilter); + +result.string(JSON.stringify(contactIds)); \ No newline at end of file diff --git a/entity/EwsSyncAddContacts_entity/entityfields/countforsync/valueProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/countforsync/valueProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..4d01818ca409c8b5746f74b408c7b4456931abe8 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/countforsync/valueProcess.js @@ -0,0 +1,10 @@ +import("system.vars"); +import("system.result"); + +var res = ""; +if (vars.get("$field.contactIds")) +{ + res = JSON.parse(vars.getString("$field.contactIds")).length; +} + +result.string(res); diff --git a/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/onActionProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/onActionProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..b0a2ce611876352836e323dcd13a38cc97f0a718 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/onActionProcess.js @@ -0,0 +1,8 @@ +import("system.neon"); +import("system.tools"); +import("EwsClientSync_lib"); +import("system.vars"); + +var contactIds = JSON.parse(vars.get("$field.contactIds")); +EwsClientSyncUtils.removeFromFavorite(contactIds, tools.getCurrentUser()[tools.NAME]); +neon.closeImage(vars.get("$sys.currentimage"), true); \ No newline at end of file diff --git a/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/stateProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/stateProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..7ae3eef9a8407cd8e4edaee71f77b14a6c233d03 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/removesynccontacts/stateProcess.js @@ -0,0 +1,11 @@ +import("system.neon"); +import("system.vars"); +import("system.result"); + +var mode = vars.get("$param.Mode_param"); +var ret = neon.COMPONENTSTATE_INVISIBLE; + +if(mode == "REMOVE") + ret = neon.COMPONENTSTATE_EDITABLE; + +result.string(ret); diff --git a/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/onActionProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/onActionProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..34578d4b676914a1e2aa7b8ec83e9277bd82aea1 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/onActionProcess.js @@ -0,0 +1,9 @@ +import("system.neon"); +import("EwsClientSync_lib"); +import("system.vars"); + +var toSncContacts = JSON.parse(vars.get("$field.contactIds")); + +//the handling of already Contacts in the Exchange is handeled by the function itself +EwsClientSyncUtils.addToEwsFavorite(toSncContacts); +neon.closeImage(vars.get("$sys.currentimage"), true); \ No newline at end of file diff --git a/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/stateProcess.js b/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/stateProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..c10e581fee4f5e13e064bbbb4a8faa11c9470aa3 --- /dev/null +++ b/entity/EwsSyncAddContacts_entity/entityfields/synccontacts/stateProcess.js @@ -0,0 +1,11 @@ +import("system.neon"); +import("system.vars"); +import("system.result"); + +var mode = vars.get("$param.Mode_param"); +var ret = neon.COMPONENTSTATE_INVISIBLE; + +if(mode == "ADD") + ret = neon.COMPONENTSTATE_EDITABLE; + +result.string(ret); diff --git a/entity/Person_entity/Person_entity.aod b/entity/Person_entity/Person_entity.aod index ac6d2e755a3059859fd1adb11bc724f80856ea89..b5d6a079e7970a57ac684887b51d6c2edda3b287 100644 --- a/entity/Person_entity/Person_entity.aod +++ b/entity/Person_entity/Person_entity.aod @@ -1333,6 +1333,20 @@ <stateProcess>%aditoprj%/entity/Person_entity/entityfields/filterviewactiongroup/children/cancelobservation/stateProcess.js</stateProcess> <titleProcess>%aditoprj%/entity/Person_entity/entityfields/filterviewactiongroup/children/cancelobservation/titleProcess.js</titleProcess> </entityActionField> + <entityActionField> + <name>addToContactSync</name> + <title>add Contact to Sync</title> + <onActionProcess>%aditoprj%/entity/Person_entity/entityfields/filterviewactiongroup/children/addtocontactsync/onActionProcess.js</onActionProcess> + <iconId>NEON:RECURRING_APPOINTMENT</iconId> + <state>EDITABLE</state> + </entityActionField> + <entityActionField> + <name>removeFromContactSync</name> + <title>remove Contact from Sync</title> + <onActionProcess>%aditoprj%/entity/Person_entity/entityfields/filterviewactiongroup/children/removefromcontactsync/onActionProcess.js</onActionProcess> + <iconId>NEON:RECURRING_APPOINTMENT_MOVED</iconId> + <state>AUTO</state> + </entityActionField> </children> </entityActionGroup> <entityActionGroup> diff --git a/entity/Person_entity/entityfields/filterviewactiongroup/children/addtocontactsync/onActionProcess.js b/entity/Person_entity/entityfields/filterviewactiongroup/children/addtocontactsync/onActionProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..3ba5646790e532ee17f5d19d6fd57fbaece4a4d1 --- /dev/null +++ b/entity/Person_entity/entityfields/filterviewactiongroup/children/addtocontactsync/onActionProcess.js @@ -0,0 +1,4 @@ +import("EwsClientSync_lib"); +import("system.vars"); + +EwsClientSyncUtils.openEwsSyncAddContactView(vars.get("$sys.selection"), vars.get("$sys.filter"), "ADD"); \ No newline at end of file diff --git a/entity/Person_entity/entityfields/filterviewactiongroup/children/removefromcontactsync/onActionProcess.js b/entity/Person_entity/entityfields/filterviewactiongroup/children/removefromcontactsync/onActionProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..1c6e00c85cdc5ec2423caf9c9f046f3fb7305168 --- /dev/null +++ b/entity/Person_entity/entityfields/filterviewactiongroup/children/removefromcontactsync/onActionProcess.js @@ -0,0 +1,4 @@ +import("EwsClientSync_lib"); +import("system.vars"); + +EwsClientSyncUtils.openEwsSyncAddContactView(vars.get("$sys.selection"), vars.get("$sys.filter"), "REMOVE"); \ No newline at end of file diff --git a/neonContext/EwsSyncAddContacts/EwsSyncAddContacts.aod b/neonContext/EwsSyncAddContacts/EwsSyncAddContacts.aod new file mode 100644 index 0000000000000000000000000000000000000000..9f9809ee4ab68bc9abc7fd254e23aba11fb0b2c3 --- /dev/null +++ b/neonContext/EwsSyncAddContacts/EwsSyncAddContacts.aod @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<neonContext xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonContext/1.1.1"> + <name>EwsSyncAddContacts</name> + <majorModelMode>DISTRIBUTED</majorModelMode> + <editView>EwsSyncAddContactsEdit_view</editView> + <entity>EwsSyncAddContacts_entity</entity> + <references> + <neonViewReference> + <name>c4d8f240-388e-4a1d-9455-e100a33c4819</name> + <view>EwsSyncAddContactsEdit_view</view> + </neonViewReference> + </references> +</neonContext> diff --git a/neonView/EwsSyncAddContactsEdit_view/EwsSyncAddContactsEdit_view.aod b/neonView/EwsSyncAddContactsEdit_view/EwsSyncAddContactsEdit_view.aod new file mode 100644 index 0000000000000000000000000000000000000000..74bbe50bf1c6529cfb43c1c895441220675f604e --- /dev/null +++ b/neonView/EwsSyncAddContactsEdit_view/EwsSyncAddContactsEdit_view.aod @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.8" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.8"> + <name>EwsSyncAddContactsEdit_view</name> + <majorModelMode>DISTRIBUTED</majorModelMode> + <size>SMALL</size> + <layout> + <boxLayout> + <name>layout</name> + </boxLayout> + </layout> + <children> + <scoreCardViewTemplate> + <name>ewsSyncScorecard</name> + <fields> + <entityFieldLink> + <name>bb922a4f-c170-4158-9241-8ba04ae92370</name> + <entityField>countForSync</entityField> + </entityFieldLink> + </fields> + </scoreCardViewTemplate> + <actionsViewTemplate> + <name>syncAction</name> + <actions> + <element>syncContacts</element> + <element>removeSyncContacts</element> + </actions> + </actionsViewTemplate> + </children> +</neonView> diff --git a/process/Communication_lib/process.js b/process/Communication_lib/process.js index 055813cf362081fd583c1d8d2cd33fb0b4c0c4c4..f9e30868d8e9c816f642e8a383504605b2ba4990 100644 --- a/process/Communication_lib/process.js +++ b/process/Communication_lib/process.js @@ -31,11 +31,11 @@ CommUtil.getMediumIdsByCategory = function (pCategory) var keywordAttr = new KeywordAttribute($KeywordRegistry.communicationMedium(), "category"); //TODO: use getRows() via JDito var mediumIds = newSelect("AB_KEYWORD_ENTRY.KEYID") - .from("AB_KEYWORD_ATTRIBUTERELATION") - .join("AB_KEYWORD_ENTRY", "AB_KEYWORD_ENTRY.AB_KEYWORD_ENTRYID = AB_KEYWORD_ATTRIBUTERELATION.AB_KEYWORD_ENTRY_ID") - .where("AB_KEYWORD_ATTRIBUTERELATION.AB_KEYWORD_ATTRIBUTE_ID", keywordAttr.id) - .and("AB_KEYWORD_ATTRIBUTERELATION." + keywordAttr.dbField, pCategory) - .arrayColumn(); + .from("AB_KEYWORD_ATTRIBUTERELATION") + .join("AB_KEYWORD_ENTRY", "AB_KEYWORD_ENTRY.AB_KEYWORD_ENTRYID = AB_KEYWORD_ATTRIBUTERELATION.AB_KEYWORD_ENTRY_ID") + .where("AB_KEYWORD_ATTRIBUTERELATION.AB_KEYWORD_ATTRIBUTE_ID", keywordAttr.id) + .and("AB_KEYWORD_ATTRIBUTERELATION." + keywordAttr.dbField, pCategory) + .arrayColumn(); return mediumIds; }; @@ -73,19 +73,19 @@ CommUtil.setStandardForCategory = function(pAffectedRowId, pNewStandardCommId, p //set current standard comm-record as non-standard var cond = newWhere("ISSTANDARD = 1") - .and("COMMUNICATION.CONTACT_ID", pAffectedRowId) - .and("COMMUNICATION.MEDIUM_ID", mediumIds, SqlBuilder.IN()); + .and("COMMUNICATION.CONTACT_ID", pAffectedRowId) + .and("COMMUNICATION.MEDIUM_ID", mediumIds, SqlBuilder.IN()); - statements.push(["COMMUNICATION", cols, null, ["0"], cond.build()]); + statements.push(["COMMUNICATION", cols, null, ["0"], cond.build()]); //pNewStandardCommId can be an empty string if the standard has to only be removed if (pNewStandardCommId != "") { //set the new standard comm-record cond = newWhere("COMMUNICATION.COMMUNICATIONID", pNewStandardCommId) - //check communicationid, contactId and medium to prevent data-inconsistency when bad function params are passed by (e.g communicationid of a different category) - .and("COMMUNICATION.CONTACT_ID", pAffectedRowId) - .and("COMMUNICATION.MEDIUM_ID", mediumIds, SqlBuilder.IN()); + //check communicationid, contactId and medium to prevent data-inconsistency when bad function params are passed by (e.g communicationid of a different category) + .and("COMMUNICATION.CONTACT_ID", pAffectedRowId) + .and("COMMUNICATION.MEDIUM_ID", mediumIds, SqlBuilder.IN()); statements.push(["COMMUNICATION", cols, null, ["1"], cond.build()]); } @@ -148,8 +148,8 @@ CommUtil.getStandardSubSqlForCategory = function(pCategory, pContactField) return "''"; var selectStandardAddr = newSelect("max(COMMUNICATION.ADDR)") - .from("COMMUNICATION") - .where(); + .from("COMMUNICATION") + .where(); if (pContactField == undefined) selectStandardAddr.and("COMMUNICATION.CONTACT_ID = CONTACT.CONTACTID"); @@ -215,6 +215,41 @@ CommUtil.getStandardMail = function (pContactId) return db.cell(query); } +/** + * Returns a sub sql-string (without brackets) for getting the address of a COMMUNICATION.<br> + * + * @param {String} pMediumKey <p> + * Key of the keyword "COMMUNICATION.MEDIUM".<br> + * (custom.category; e.g. "PHONE")<br> + * @param {String|Object} pContactField=CONTACT.CONTACTID (optional) <p> + * SQL-fieldname that shall be used for filtering the CONTACT_ID, <br> + * this can be a string(fieldname) or an SqlBuilder object.<br> + * @return {String} <p> + * Sub-sql.<br> + */ +CommUtil.getMediumAddrSubSqlByKey = function(pMediumKey, pContactField) +{ + var selectAddr = newSelect("max(COMMUNICATION.ADDR)") + .from("COMMUNICATION") + .where(); + + if (pContactField == undefined) + selectAddr.and("COMMUNICATION.CONTACT_ID = CONTACT.CONTACTID"); + else if (typeof(pContactField) == "string") + selectAddr.and("COMMUNICATION.CONTACT_ID", pContactField); + else if (typeof(pContactField) == "object") + { + //you may want to sepcify a concrete value + selectAddr.and(pContactField); + } + else + return "''"; + + selectAddr.andIfSet("COMMUNICATION.MEDIUM_ID", pMediumKey); + + return selectAddr.toString(); +} + /** * Provides static methods for validation of communication data.<br> * <b>Do not create an instance of this!</b> diff --git a/process/EwsClientSync_lib/EwsClientSync_lib.aod b/process/EwsClientSync_lib/EwsClientSync_lib.aod new file mode 100644 index 0000000000000000000000000000000000000000..a53260007b9ecc486117433576e3c03b8723ff43 --- /dev/null +++ b/process/EwsClientSync_lib/EwsClientSync_lib.aod @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2"> + <name>EwsClientSync_lib</name> + <majorModelMode>DISTRIBUTED</majorModelMode> + <process>%aditoprj%/process/EwsClientSync_lib/process.js</process> + <alias>Data_alias</alias> + <variants> + <element>LIBRARY</element> + </variants> +</process> diff --git a/process/EwsClientSync_lib/process.js b/process/EwsClientSync_lib/process.js new file mode 100644 index 0000000000000000000000000000000000000000..2366e6b5af77b5ba73399379f2f2d2c14fe88c1b --- /dev/null +++ b/process/EwsClientSync_lib/process.js @@ -0,0 +1,357 @@ +import("system.neon"); +import("Util_lib"); +import("system.translate"); +import("Communication_lib"); +import("Placeholder_lib"); +import("system.SQLTYPES"); +import("system.vars"); +import("system.datetime"); +import("system.util"); +import("system.tools"); +import("system.favorite"); +import("system.logging"); +import("Sql_lib"); +import("system.db"); +import("system.entities") + +/** + * Provides methods for handling and interacting with the EWS Plugin + * + * @class + */ +function EwsClientSyncUtils() {} + +/* + * Constant which contains the ewsSync Tag + * if the tag has to be changed a constant is the better way + */ +EwsClientSyncUtils.EWSSYNCTAG = function(){ + return "ewssync"; +} + +/* + * prepares the synctable, insert new entries and mark for deletion + * + */ +EwsClientSyncUtils.prepareContactSyncTable = function(){ + + var favoriteDataArray = EwsClientSyncUtils.getEwsFavorites(); + var ewsSyncData = newSelect("SYNCCONTACTID, ASYS_FAVORITEID, USER_ID, CONTACT_ID") + .from("ab_synccontact") + .table(); + + var dataIndex = { + index: new Map(), + add: function (pA, pB) + { + var idxMap = this.index; + if (!idxMap.has(pA)) + idxMap.set(pA, new Set()); + idxMap.get(pA).add(pB); + }, + has: function (pA, pB) + { + return this.index.has(pA) && this.index.get(pA).has(pB); + } + }; + + //preparing the value for inserting in ewsSync Table + var toInsertFavs = []; + for (let i = 0, l = ewsSyncData.length; i < l; i++) + { + //3 - contactID 2 userID + dataIndex.add(ewsSyncData[i][3], ewsSyncData[i][2]); + } + ​ + //we want to insert those which aren't in the sync table yet but which are tagged for sync + toInsertFavs = favoriteDataArray.filter(function (row) { + return !dataIndex.has(row[1], row[2]); + }); + + + //preparing values for updating entrys in the ewsSync Table + dataIndex.index.clear(); + + var toUpdate = []; + + for (let i = 0, l = favoriteDataArray.length; i < l; i++) + { + dataIndex.add(favoriteDataArray[i][1], favoriteDataArray[i][2]); + } + + ​//dataset which are in the synctable but aren't tagged for sync (favorites ewssync) has to be updated for deletion + toUpdate = ewsSyncData.filter(function (row){ + ​ + return !dataIndex.has(row[3], row[2]); + } + ​); + + + let statements = []; + let toInsert = []; + let cols = ["SYNCCONTACTID", "ASYS_FAVORITEID", "CONTACT_ID", "USER_ID"]; + let vals = []; + let table = "AB_SYNCCONTACT"; + let updStatements = []; + + for(let i = 0; i < toInsertFavs.length; i++) + { + vals = []; + vals = [util.getNewUUID(), toInsertFavs[i][0], toInsertFavs[i][1], toInsertFavs[i][2]] + + statements.push([table, cols, null, vals]); + } + for(let i = 0; i < toUpdate.length; i++) + { + updStatements.push([table, ["DATE_DEL"], null, [vars.get("$sys.date")], "ASYS_FAVORITEID = '" + toUpdate[i][1] + "'" ]); + } + + try{ + let count = db.inserts(statements, "Data_alias"); + let updCount = db.updates(updStatements, "Data_alias"); + } + catch(exc){ + logging.log(exc) + } +} + +/* + * get the contact which are marked with the ewssync favorite Tag + * + * @return [Array] favorite Data [[]] + */ +EwsClientSyncUtils.getEwsFavorites = function(){ + + + //get Favorites which are tagged with the Sync Tag + // var conf = favorite.createGetFavoritesConfig(); + // conf.setFavoriteGroupTitle(EwsClientSyncUtils.EWSSYNCTAG()); + // conf.setObjectType("Person"); + // + // var favoriteData = favorite.getFavorites(conf); + // var favoriteDataArray = []; + // + // for (var temp in favoriteData) + // { + // let favId = favoriteData[temp]["id"]; + // let rowId = favoriteData[temp]["rowid"]; + // let userID = favoriteData[temp]["group"]["groupuser"]; + // + // favoriteDataArray.push([favId, rowId, userID]); + // } + + + //in the current version it isn't possible to call the favorite API within a Serverprocess this will be implemented in the next RC + + var favoriteDataArray = new SqlBuilder("_____SYSTEMALIAS") + .selectDistinct("ASYS_RECORD.ID, ASYS_RECORD.ROW_ID, ASYS_RECORDGROUP.USER_ID") + .from("ASYS_RECORD") + .join("ASYS_RECORDGROUP", "ASYS_RECORDGROUP.ID = ASYS_RECORD.RECORDGROUP_ID") + .where("ASYS_RECORD.OBJECT_TYPE", "Person") + .and("ASYS_RECORDGROUP.TITLE", EwsClientSyncUtils.EWSSYNCTAG(), "LOWER(#) = ?") + .groupBy("ASYS_RECORDGROUP.USER_ID, ASYS_RECORD.ROW_ID, ASYS_RECORD.ID") + .table(); + + return favoriteDataArray; +} + +/* + * sets the editdate for the entrys in the synctable, in reason to get synced with exchange + * + * @param {String} pTableName + * @param {String} pDataSetID req + * @param {String} pDate req + * + * @return {void} + + */ +EwsClientSyncUtils.setContactToSync = function(pTableName, pDataSetID, pDate){ + + var affectedTables = ["ORGANISATION" , "PERSON", "CONTACT", "ADDRESS", "COMMUNICATION"]; + var affectedIDs = []; + + if (affectedTables.indexOf(pTableName) != -1) + { + switch(pTableName) + { + case "ORGANISATION": + affectedIDs = newSelect("CONTACT.CONTACTID").from("CONTACT").where("CONTACT.ORGANISATION_ID", pDataSetID).arrayColumn(); + break; + case "PERSON": + affectedIDs = newSelect("CONTACT.CONTACTID").from("CONTACT").where("CONTACT.PERSON_ID", pDataSetID).arrayColumn(); + break; + case "CONTACT": + affectedIDs = [pDataSetID]; + break; + case "ADDRESS": + affectedIDs = newSelect("ADDRESS.CONTACT_ID").from("ADDRESS") + .where("ADDRESS.ADDRESSID", pDataSetID) + .union(newSelect("CONTACT.CONTACT_ID").from("CONTACT").where("CONTACT.ADDRESS_ID", pDataSetID)).arrayColumn(); + break; + case "COMMUNICATION": + affectedIDs = newSelect("COMMUNICATION.CONTACT_ID").from("COMMUNICATION").where("COMMUNICATION.COMMUNICATIONID", pDataSetID).arrayColumn(); + break; + } + + if (affectedIDs.length > 0) + { + var upd = db.updateData("AB_SYNCCONTACT", ["DATE_EDIT"], null, [pDate], + "EXCHANGEID is not null and CONTACT_ID in ('" + affectedIDs.join("','") + "')"); + } + } +} + + +/* + * prepare Placholder for Exchange contacts + * + * @param {String} pUserId + */ +EwsClientSyncUtils.getPlaceholders = function(pUserId){ + var ewsPlaceholders = []; + + //EWS Sync Placeholder + //See EWS API Doc for other placeholder + //new Placeholder that has to be synced probably has also to be added in addDataToValueObjects() in the serverprocess + _addSqlPart("CONTACTID", "CONTACT.CONTACTID"); + _addSqlPart("EXCHANGEID", "select EXCHANGEID from ab_synccontact where Ab_synccontact.contact_id = contact.contactid and user_id = '"+ pUserId +"'", null, null) + + _addSqlPart("Givenname", "PERSON.FIRSTNAME"); + _addSqlPart("Surname", "PERSON.LASTNAME"); + _addSqlPart("Department", "CONTACT.DEPARTMENT"); + _addSqlPart("JobTitle", "CONTACT.CONTACTROLE"); + _addSqlPart("CompanyName", "ORGANISATION.NAME"); + + + _addAddressFormat("Business_street", "{street} {buildingno}"); + _addAddressFormat("Business_postalcode", "{zip}"); + _addAddressFormat("Business_city", "{city}"); + _addAddressFormat("Business_state", "{district}"); + _addAddressFormat("Business_countryorregion", "{country}"); + + _addSqlPart("EmailAddress1", CommUtil.getStandardSubSqlMail()); + + _addSqlPart("BusinessPhone", CommUtil.getStandardSubSqlPhone()); + _addSqlPart("BusinessHomepage", CommUtil.getMediumAddrSubSqlByKey("COMMINTERNET")); + + _addSqlPart("OtherFax", CommUtil.getMediumAddrSubSqlByKey("COMMFAX")); + _addSqlPart("MobilePhone", CommUtil.getMediumAddrSubSqlByKey("COMMMOBIL")); + _addSqlPart("FileAsMapping","case when PERSON_ID is not null then 'SurnameCommaGivenName' else 'Company' end" , null, null); + + + // _addSqlPart("HomePhone", "''"); + // _addSqlPart("HomeFax", "''"); + // _addSqlPart("EmailAddress2", "''"); + + + + return ewsPlaceholders; + + function _addSqlPart (pName, pSqlPart, pTarget, pTitle) + { + ewsPlaceholders.push(new Placeholder(pName, Placeholder.types.SQLPART, "(" + pSqlPart + ")", pTarget, pTitle)); + } + /** + * add an address format placeholder to placeholders + */ + function _addAddressFormat (pName, pFormat, pTarget, pTitle) + { + ewsPlaceholders.push(new Placeholder(pName, Placeholder.types.ADDRESSFORMAT, pFormat, pTarget, pTitle)); + } +} + +/* + * removes Person favorites with the EWS Tag + * + * @param {Array} pToDelete contains contact_ids for which the favorite should be removed + * @param {String} pUserId - User for which the favorites should be removed + * + * @return {Boolean} true if successfull + * + */ +EwsClientSyncUtils.removeFromFavorite = function(pToDelete, pUserId ){ + + //maybe in a future version there will be a way to remove favorites on a better way + + //all ews related Favorites for the user + var config = favorite.createGetFavoritesConfig() + .setFavoriteGroupTitle(EwsClientSyncUtils.EWSSYNCTAG()) + .setGroupType(favorite.FAVORITE_GROUP).setObjectType("Person") + .setUserId(pUserId); + + var ewsFavorite = favorite.getFavorites(config); + var favsToDelete = []; + + var dataIndex = { + index: new Map(), + add: function (pA) + { + var idxMap = this.index; + if (!idxMap.has(pA)) + idxMap.set(pA, new Set()); + }, + has: function (pA) + { + return this.index.has(pA); + } + }; + + for each(let row in pToDelete) + { + dataIndex.add(row) + } + + // delete those which are in the selection and are also in ewsFavorite + for each (let row in ewsFavorite) + { + if(dataIndex.has(row["rowid"])) + favsToDelete.push(row["id"]); + } + + var delConfig = favorite.createRemoveMultipleByIdConfig().setFavoriteRecordIds(favsToDelete); + + return favorite.remove(delConfig); +} + +/* + * Add contacts to favorites as ewssync group + * + * @param {Array} pToInsert Array with contactIds which should be added to the sync + */ +EwsClientSyncUtils.addToEwsFavorite = function(pToInsert){ + var userID = tools.getCurrentUser()[tools.NAME] + + var config = favorite.createAddFavoriteConfig(); + config.setFavoriteGroupTitle(EwsClientSyncUtils.EWSSYNCTAG()); + config.setObjectType("Person"); + config.setUserId(userID); + config.setGroupType(favorite.FAVORITE_GROUP) + + for(let i = 0, l = pToInsert.length; i < l; i++){ + config.setRowId(pToInsert[i]); + favorite.add(config); + } +} + +/** + * Opens a context to add or remove contacts from the ewssync<br> + * + * @param {String[]} pContactIds Contacts that should be added.<br> + * @param {String|Object} pFilter the filter for the contacts that should be used if no contact is selected + * @param {String} pMode the mode which should be used ("ADD" or "REMOVE") default ADD + */ +EwsClientSyncUtils.openEwsSyncAddContactView = function(pContactIds, pFilter, pMode) +{ + if (!Utils.isString(pContactIds)) + pContactIds = JSON.stringify(pContactIds); + if (!Utils.isString(pFilter)) + pFilter = JSON.stringify(pFilter); + + var params = { + "ContactIds_param": pContactIds, + "ContactFilter_param": pFilter, + "Mode_param": pMode + } + neon.openContext("EwsSyncAddContacts", "EwsSyncAddContactsEdit_view", null, neon.OPERATINGSTATE_VIEW, params); +} + \ No newline at end of file diff --git a/process/EwsClient_lib/process.js b/process/EwsClient_lib/process.js index 2de3addde44d76ac3ad80c4f264e837fc6d90e45..51dd088565b42911b3354abcf3b823cf6c1152f1 100644 --- a/process/EwsClient_lib/process.js +++ b/process/EwsClient_lib/process.js @@ -62,7 +62,10 @@ $EwsClientTaskDescriptions.GET_CONTACTS_BY_FOLDERS = function(){retur $EwsClientTaskDescriptions.GET_CONTACTSDEFAULTFOLDER = function(){return "GET_CONTACTSDEFAULTFOLDER"}; $EwsClientTaskDescriptions.GET_CONTACTFOLDERS = function(){return "GET_CONTACTFOLDERS"}; $EwsClientTaskDescriptions.UPDATE_CONTACT = function(){return "UPDATE_CONTACT"}; - +$EwsClientTaskDescriptions.INSERT_CONTACT_LIST_TO_FOLDER = function(){return "INSERT_CONTACT_LIST_TO_FOLDER"}; +$EwsClientTaskDescriptions.INSERT_CONTACT_LIST = function(){return "INSERT_CONTACT_LIST"}; +$EwsClientTaskDescriptions.UPDATE_CONTACT_LIST = function(){return "UPDATE_CONTACT_LIST"}; +$EwsClientTaskDescriptions.DELETE_CONTACT_LIST_BY_IDS = function(){return "DELETE_CONTACT_LIST_BY_IDS"}; function $EwsClientImpersonationModes(){} /** * Impersonation mode NONE should be set if no impersonation user is used and every user is authenticated per user-data @@ -400,4 +403,394 @@ EwsClientCalendarPermissionUtils._getCalendarPermissionFromResult = function (pP + "there exists a write but no read permission"); return retVal; +} + + + +/* + * provides static methods for the use of the exchange Contact Sync + * @class + */ +function EwsSyncContactUtils(){} + +EwsSyncContactUtils.getEwsSyncTag = function(){ + return "ewssync"; +} + + +/* + * returns the contact Folder for the passed mailbox + * requires the EWSClientPlugin + * + * @param {String} pAliasName name of the exchange alias + * @param {String} pMailbox mailbox of which the folder should be read + * + * @return {Array} array with maps [["id"], ["name"]] + */ +EwsSyncContactUtils.getContactFolders = function(pAliasName, pMailbox){ + + let taskDescription = $EwsClientTaskDescriptions.GET_CONTACTFOLDERS(); + + let task = <mailbox>{ + pMailbox + }</mailbox>; + + let pluginInput = EwsClientXMLUtils.getRequestXMLforAlias(taskDescription, task, $EwsClientImpersonationModes.SINGLE(), pAliasName); + let xmlOutput = EwsClientUtils.callPlugin(pluginInput); + let retVal = []; + + for each (folder in xmlOutput.folders.folder) + { + let map = {}; + map["id"] = folder.id; + map["name"] = folder.name; + retVal.push(map); + } + return retVal; +} + + +EwsSyncContactUtils.updateContacts = function(pAliasName, pMailbox, pObjects, doDebug ){ + let taskDescription = $EwsClientTaskDescriptions.UPDATE_CONTACT_LIST(); + + let tasks = <mailbox>{ + pMailbox + } + </mailbox>; + + let contactsXml = <contacts/> + + for each(let [pUniqueId, pData, pAddresses] in pObjects) + { + if(pUniqueId) + contactsXml.appendChild(EwsSyncContactXMLUtils.mapContactToXML(pData, pAddresses, pUniqueId, true)); + } + tasks += contactsXml; + + let pluginInput = EwsClientXMLUtils.getRequestXMLforAlias(taskDescription, tasks, $EwsClientImpersonationModes.SINGLE(), pAliasName); + + let xmlOutput = EwsClientUtils.callPlugin(pluginInput); + + return EwsSyncContactUtils.proceedPluginXmlOutput(xmlOutput, 2, doDebug); +} + + +/** + * Adds a contact in a specific folder + * requires EWSClientPlugin + * + * @param {String} pAliasName name of the Alias which contains connectiondata + * @param {String} pMailbox owner of the contact + * @param {String} pUniqueId defines folder + * @param {[[dataArray, adressArray]]} pObjects + * @param {boolean} doDebug + * + * @return {String} new unique ID of the contact (ExchangeID) + * + * pData array with maps with following structure: + * ["key"] + * ["value"] + * + * pAddresses array with following Maps + * ["addressKey"] + * ["value"] -> array with Maps with following structure ["key"] + * ["value] + * + * existing Key for a exchange Contact: + * title, givenname, middlename, surname + * jobtitle, officelocation, department, companyname, manager, assistentname, businesshomepage + * fileasmapping + * messagebody, categories + * id, changekey, lastmodifiedtime, datetimecreated (read only) + * + * - possible phone numbers: + * AssistantPhone, BusinessFax, BusinessPhone, BusinessPhone2, Callback, CarPhone, CompanyMainPhone, HomeFax, HomePhone, + * HomePhone2, Isdn, MobilePhone, OtherFax, OtherTelephone, Pager, PrimaryPhone, RadioPhone, Telex, TtyTddPhone + * + * - possible Mailing: + * EmailAddress1, EmailAddress2, EmailAddress3 + * + * - possible Instant Messaging: + * ImAddress1, ImAddress2, ImAddress3 + * + * - possible Adressen: + * Business, Home, Other + * - possible values of a adress: + * street, postalcode, city, state, countryorregion + * + * postaladdressindex + * - possible value for "postaladdressindex" + * None, Business, Home, Other + */ +EwsSyncContactUtils.insertContactsToFolder = function(pAliasName, pMailbox, pUniqueId, pObjects, doDebug) +{ + + let taskDescription = $EwsClientTaskDescriptions.INSERT_CONTACT_LIST_TO_FOLDER(); + + let task = <uniqueId>{ + pUniqueId + }</uniqueId> + + task += <mailbox>{ + pMailbox + }</mailbox> + + let contactsXml = <contacts/> + + for each(let [pData, pAddresses] in pObjects) + { + contactsXml.appendChild(EwsSyncContactXMLUtils.mapContactToXML(pData, pAddresses)); + } + task += contactsXml; + + let pluginInput = EwsClientXMLUtils.getRequestXMLforAlias(taskDescription, task, $EwsClientImpersonationModes.SINGLE(), pAliasName); + let xmlOutput = EwsClientUtils.callPlugin(pluginInput); + + return EwsSyncContactUtils.proceedPluginXmlOutput(xmlOutput, 1, doDebug); +} + +/** + * deletes unique Contacts out of EWS + * Voraussetzung ist EWSClientPlugin + * + * @param {String} pAliasName Name des Alias aus dem die Verbindungsdaten ausgelesen werden können + * @param {String} pUniqueIds Definiert Die Kontakte die + * @param {String} pMailBox opt falls angegen wird der ImpersonatedUser verwendet + * @param {boolean} doDebug + * + * @return {String[]} Array with results for Structure see Example + * @example + * Structure of the result from plugin + * [ + * [exchangeIdsWithNoError, n], + * [[id, "Error", "errorMsg", "Type"], [idn, "Error", "errorMsg", "Type"] ] + * ] + */ +EwsSyncContactUtils.deleteContactByID = function(pAliasName, pUniqueIds, pMailBox, doDebug) +{ + let taskDescription = $EwsClientTaskDescriptions.DELETE_CONTACT_LIST_BY_IDS(); + + let task = <uniqueIds/>; + + for each(let pUniqueId in pUniqueIds) + { + if(pUniqueId) + task.appendChild(<uniqueId>{ + pUniqueId + }</uniqueId>) + } + + let pluginInput; + + if ( pMailBox == undefined ) + { + pluginInput = EwsClientXMLUtils.getRequestXMLforAlias(taskDescription, task, $EwsClientImpersonationModes.NONE(), pAliasName); + } + else + { + task += <mailbox>{ + pMailBox + }</mailbox>; + pluginInput = EwsClientXMLUtils.getRequestXMLforAlias(taskDescription, task, $EwsClientImpersonationModes.SINGLE(), pAliasName); + } + + var xmlOutput = EwsClientUtils.callPlugin(pluginInput); + + return EwsSyncContactUtils.proceedPluginXmlOutput(xmlOutput, 3, doDebug); +} + +/* + * proceeds the plugin outputXMl and returns contactId and ExchangeID, + * + * @param {XML} pXmlOutput output of the plugin call + * @param {int} pMode controlls proceeding 1 - insert, 2 - update, 3 - delete + * @param {boolean} doDebug + * + * @return {Array} 2d Array with contactIds, unigueID(ExchangeID) and ErrorArray + * + */ +EwsSyncContactUtils.proceedPluginXmlOutput = function(pXmlOutput, pMode, doDebug){ + + var contactRes = []; + var failedIds = []; + var status, uniqueId, resultMessage, resultCode; + + //insert and Update + if(pMode < 3) + { + //for every contactResult in the contactResults list + for each( contactResult in pXmlOutput.contactResults.contactResult ) + { + var keyValues = contactResult.contact.keyValues.keyValue; + var contactid = ""; + + for each( keyValue in keyValues ) + { + if(keyValue.key.toString() == "CONTACTID")//we only need the contactid value + { + contactid = keyValue.value.toString(); + break; + } + } + + status = contactResult.result.toString(); + resultMessage = contactResult.resultMessage.toString(); + + //during update the xml structure is slightly different + uniqueId = (Number(pMode) == 2) ? contactResult.contact.@uniqueId.toString() : contactResult.uniqueId.toString(); + + if(status == "Success" && contactid != "" && uniqueId != "")//only if the action was successfull + { + if(pMode == 2)//update + contactRes.push(uniqueId); + else + contactRes.push([contactid, uniqueId]);//push for update/insert in AB_SYNCCONTACT + } + else + { + resultCode = contactResult.resultCode.toString(); + failedIds.push([contactid, status, resultMessage, resultCode, contactResult.contact.toString()]) + } + } + if(doDebug) logging.log("failedIds: " + failedIds.toSource()); + }//pMode < 3 + else + //the delete result xml differs from insert and update + if (pMode == 3) + { + for each( contactResult in pXmlOutput.resultBundles.resultBundle ) + { + //keyvalues + status = contactResult.result.toString(); + uniqueId = contactResult.uniqueId.toString(); + + if(status == "Success" && uniqueId != "") + { + contactRes.push(uniqueId);//push for deleting in AB_SYNCCONTACT + } + else + { + //get Keyvalues + resultMessage = contactResult.resultMessage.toString(); + resultCode = contactResult.resultCode.toString(); + + failedIds.push([uniqueId, status, resultMessage, resultCode]); + } + } + } + + return [contactRes, failedIds]; +} + +/* + *Class for handling ews XML + * + * @class + */ +function EwsSyncContactXMLUtils(){} + + +EwsSyncContactXMLUtils.mapContactToXML = function( pData, pAddresses, pExchangeId, isUpdate) +{ + var content = <contact> + <keyValues/> + <physicalAddresses/> + </contact>; + + if(isUpdate) + { + content = <contact uniqueId={ + pExchangeId + }> + <keyValues/> + <physicalAddresses/> + </contact>; + } + + //add contactData e.g. Name,Lastname, department etc. + for each (let map in pData) + { + let keyXml = <keyValue/> + let mapKey = map["key"]; + let mapVal = map["value"]; + + keyXml.appendChild(<key>{ + mapKey + }</key>); + keyXml.appendChild(<value>{ + mapVal + }</value>); + + //append contactdata to parent xml + content.keyValues.appendChild(keyXml); + } + + //TODO: refactor when for of Loop is available + for each( let outerMap in pAddresses ) + { + var adressXml = <physicalAddresse/> + var pAdressKey = outerMap["addressKey"]; + + adressXml.appendChild(<addressKey>{ + pAdressKey + }</addressKey>) + + var list = outerMap["value"]; + + adressXml.appendChild(<keyValues/>) + + for each( let innerMap in list ) + { + let keyXml = <keyValue/> + let innerMapKey = innerMap["key"]; + let innerMapValue = innerMap["value"]; + + + keyXml.appendChild(<key>{ + innerMapKey + }</key>); + keyXml.appendChild(<value>{ + innerMapValue + }</value>); + + adressXml.keyValues.appendChild(keyXml); + } + //append adressxml to parent + content.physicalAddresses.appendChild(adressXml); + } + return content; + } + +EwsSyncContactXMLUtils.mapContactFromXML = function( pContactXML ) +{ + let retVal = {}; + let shallow = []; + + for each (keyValue in pContactXML.keyValues.keyValue) + { + let map = {}; + map["key"] = keyValue.key; + map["value"] = keyValue.value; + shallow.push(map); + } + retVal["shallow"] = shallow; + + let addresses = []; + for each (physicalAddresse in pContactXML.physicalAddresses.physicalAddresse) + { + + let outerMap = {}; + outerMap["addressKey"] = physicalAddresse.addressKey; + let list = []; + for each(keyValue in physicalAddresse.keyValues) + { + let innerMap = {}; + innerMap["key"] = keyValue.key; + outerMap["value"] = keyValue.value; + list.push(innerMap); + } + outerMap["value"] = list; + } + retVal["addresses"] = addresses; + return retVal; } \ No newline at end of file diff --git a/process/ewsSyncContacts_serverProcess/ewsSyncContacts_serverProcess.aod b/process/ewsSyncContacts_serverProcess/ewsSyncContacts_serverProcess.aod new file mode 100644 index 0000000000000000000000000000000000000000..fbbe02fc2fff34c845acc1f03e22f34836da4ab6 --- /dev/null +++ b/process/ewsSyncContacts_serverProcess/ewsSyncContacts_serverProcess.aod @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2"> + <name>ewsSyncContacts_serverProcess</name> + <title>Sync Exchange Contacts</title> + <majorModelMode>DISTRIBUTED</majorModelMode> + <process>%aditoprj%/process/ewsSyncContacts_serverProcess/process.js</process> + <alias>Data_alias</alias> + <variants> + <element>EXECUTABLE</element> + <element>LIBRARY</element> + </variants> +</process> diff --git a/process/ewsSyncContacts_serverProcess/process.js b/process/ewsSyncContacts_serverProcess/process.js new file mode 100644 index 0000000000000000000000000000000000000000..fd420916d9172146ce5f60943ae629bf8578cd82 --- /dev/null +++ b/process/ewsSyncContacts_serverProcess/process.js @@ -0,0 +1,588 @@ +import("Util_lib"); +import("system.project"); +import("system.favorite"); +import("system.db"); +import("system.result"); +import("EwsClient_lib"); +import("system.text"); +import("Address_lib"); +import("Communication_lib"); +import("system.vars"); +import("Sql_lib"); +import("system.tools"); +import("system.logging"); +import("EwsClientSync_lib"); + +//inserting new Data in the asys_synccontact table and deleting those which aren't anymore in the favorite context +EwsClientSyncUtils.prepareContactSyncTable(); + +//start Sync with Exchange +var msg = runSync(); + +if(msg.length){ + result.string(msg.join("\n")); +} + +function runSync(){ + var doDebug = false; + var retMsg = []; + + var syncUser = newSelect("distinct USER_ID").from("AB_SYNCCONTACT").arrayColumn(); + + for (var i= 0 ; i < syncUser.length; i++) + { + var user = tools.getUserByAttribute(tools.NAME, [syncUser[i]], tools.PROFILE_DEFAULT); + if (user) + { + var mailBox = user[tools.PARAMS][tools.EXCHANGE_EMAIL]; + + if(!mailBox)//use the mail of the user as backup exchangeMail adress + mailBox = user[tools.PARAMS][tools.EMAIL]; + + + var exAlias = user[tools.PARAMS][tools.CALENDAR_ALIAS]; + + if (!exAlias) + exAlias = project.getInstanceConfigValue("calendarAlias", null); + + var login = user[tools.TITLE]; + var lastRunDate = user[tools.PARAMS]["lastEwsSyncContact"]; + var runDate = vars.get("$sys.date"); + user[tools.PARAMS]["lastEwsSyncContact"] = runDate; + + if (mailBox != "" && exAlias != "") + { + if(doDebug) logging.log("ContactSync for User: " + login + " started") + + let delContacts = EWSdeleteContactsForUser(syncUser[i], mailBox, exAlias, login, doDebug); + let updateContacts = EWSupdateContactsForUser(syncUser[i], mailBox, exAlias, lastRunDate, login, doDebug); + let insertContacts = EWSinsertContactsForUser(syncUser[i], mailBox, exAlias, runDate, login, doDebug); + + if(delContacts.length) + retMsg.push(delContacts); + if(updateContacts.length) + retMsg.push(updateContacts); + if(insertContacts.length) + retMsg.push(insertContacts); + + tools.updateUser(user); + + if(doDebug) logging.log("ContactSync for User: " + login + " completed") + } + else + { + retMsg.push(["No exchange configuration exists for user " + login]); + } + } + } + if(doDebug) logging.log("EWSContactSync completed"); + + return retMsg; +} + +/**************************************************************************************************/ + +/* + * deletes the Synced Contacts for the passed User + * + * @param {String} pUserID ID of the User for which Contact should be removed from sync + * @param {String} pMailBox Exchange Mail + * @param {String} pAlias name of the Exchange Alias + * @param {String} pUser Login of the User + * @param {boolean} doDebug + * +*/ +function EWSdeleteContactsForUser(pUserID, pMailBox, pAlias, pUser, doDebug) +{ + //delete all Entrys with a DATE_DEL tag, those were removed between the last import and yet + var cond = newWhere("AB_SYNCCONTACT.DATE_DEL is not null").and("AB_SYNCCONTACT.USER_ID", pUserID); + + var delContacts = newSelect("AB_SYNCCONTACT.EXCHANGEID") + .from("AB_SYNCCONTACT") + .where(cond) + .and("AB_SYNCCONTACT.EXCHANGEID is not null") + .arrayColumn(); + + var deletions = Utils.clone(delContacts); + + var exchangeids = []; + while(deletions.length > 0) + { + let deletionPart = deletions.splice(0, 200); + try{ + exchangeids = exchangeids.concat(EwsSyncContactUtils.deleteContactByID(pAlias, deletionPart, pMailBox, doDebug)); + } + catch(ex) + { + let errMsg = "Error while calling the plugin: " + logging.toLogString(ex.rhinoException != undefined ? ex.rhinoException : ex, true) + "\r\n"; + logging.log(errMsg); + return [errMsg]; + } + } + + var errorItems = []; + var notFoundItems = []; + var deletedItems = []; + + if(exchangeids.length > 0) + { + //no errors during deletion in Exchange + while (exchangeids[0].length > 0) + { + let exchangeidsPart = exchangeids[0].splice(0, 30);//max limit 30 + + cond.and("AB_SYNCCONTACT.EXCHANGEID", exchangeidsPart, SqlBuilder.IN()).deleteData(); + deletedItems = deletedItems.concat(exchangeidsPart); + } + + //exchange Entrys with errors for deleting + while (exchangeids[1].length > 0) + { + if(doDebug) logging.log("Exchange Entrys with Errors: " + exchangeids[1].length); + var deletes = []; + + let exchangeidsPart = exchangeids[1].splice(0, 30);//max limit 30 + + for (let i = 0; i < exchangeidsPart.length; i++) + { + if(exchangeidsPart[i][3] == "ErrorItemNotFound") + { + deletes.push(exchangeidsPart[i][0]); + notFoundItems.push(exchangeidsPart[i][0] , pUserID); + } + else + { + errorItems.push(exchangeidsPart[i][0] , pUserID); + } + } + + if(deletes.length > 0) + { + let affectedRows = cond.and("AB_SYNCCONTACT.EXCHANGEID", deletes, SqlBuilder.IN()).deleteData(); + if(doDebug) logging.log("Anzahl der gelöschten Einträge: " + affectedRows); + deletedItems = deletedItems.concat(deletes); + } + + //clearing the where condition and preset it to it's origin condition + cond.clearWhere(); + cond.where("AB_SYNCCONTACT.DATE_DEL is not null").and("AB_SYNCCONTACT.USER_ID", pUserID); + } + + if(doDebug && notFoundItems.length > 0) + logging.log("EWSSYnc not found items: " + notFoundItems.toSource()); + + if(errorItems.length > 0) + logging.log("EWSSYnc error items: " + errorItems.toSource()); + + if(doDebug && deletedItems.length > 0) + logging.log("EWSSYnc deleted items: " + deletedItems.toSource()); + } + return errorItems; +} + + + +/* + * updates the Synced Contacts for the passed User + * + * @param {String} pUserID ID of the User for which Contact should be updated + * @param {String} pMailBox Exchange Mail + * @param {String} pAlias name of the Exchange Alias + * @param {String} pRunDate lastRunDate of the sync + * @param {String} pUser Login of the User + * @param {boolean} doDebug + * +*/ +function EWSupdateContactsForUser(pUserID, pMailBox, pAlias, pRunDate, pUser, doDebug) +{ + var runDate = (!pRunDate) ? 0 : pRunDate; + + //get Contacts for update + var cond = newWhere("AB_SYNCCONTACT.EXCHANGEID is not null") + .and("AB_SYNCCONTACT.USER_ID", pUserID) + .and("AB_SYNCCONTACT.DATE_EDIT", runDate, SqlBuilder.GREATER()); + + var contactsData = newSelect("AB_SYNCCONTACT.CONTACT_ID") + .from("AB_SYNCCONTACT") + .where(cond) + .arrayColumn(); + + var retMsg = []; + + if (contactsData.length > 0) + { + //get data with config + var config = EwsClientSyncUtils.getPlaceholders(pUserID); + var data = getAddressData(contactsData, config); + + //data pos 0 contains column Name - use them as key for the User object + var header = []; + for (let i = 0; i < data[0].length; i++) + { + header.push(data[0][i].replace(new RegExp("[{@}]","g"), "")); //replace start and end delimeter which are set in the getAdressData Method + } + + var dataObj, addrObj, addrValueObjBusiness, addrValueObjHome, addrValueObjOther, exchangeId; + var objects = []; + //loop through array at pos 1, header has been already collected + for (let i = 1; i < data.length; i++) + { + //dataObj at pos data[i][j] will match to the header[j] + dataObj = new Object(); + addrObj = new Object(); + addrValueObjBusiness = {}; + addrValueObjHome = {}; + addrValueObjOther = {}; + + //addressdata will allocated with tags (Business_, Home_, Other_) to each object + for (let j = 0; j < header.length; j++) + { + addDataToValueObjects(header[j], data[i][j], dataObj, addrValueObjBusiness, addrValueObjHome, addrValueObjOther); + } + + //build addr object out of the objects generated above + buildAddrObject(addrObj, addrValueObjHome, addrValueObjBusiness, addrValueObjOther); + + // Get the exchangeID from the data-object for update + let exchangeId = dataObj["EXCHANGEID"].value; + objects.push([exchangeId, dataObj, addrObj]); + } + + var errorItems = []; + var notFoundItems = []; + var deletedItems = []; + + while(objects.length > 0) + { + let objectsPart = objects.splice(0, 200);//currently max limit: 200 + try + { + exchangeIds = EwsSyncContactUtils.updateContacts(pAlias, pMailBox, objectsPart, doDebug);//run plugin + } + catch(ex) + { + let errMsg = "Error while calling the plugin: " + logging.toLogString(ex.rhinoException != undefined ? ex.rhinoException : ex, true) + "\r\n"; + logging.log(errMsg); + return [errMsg]; + } + + //exchangeIDs[1] array for the contacts which got an error + while(exchangeIds[1].length > 0) + { + var deletes = []; + + let exchangeIdsPart = exchangeIds[1].splice(0, 30); + + for (let i = 0; i < exchangeIdsPart.length; i++) + { + //contact which aren't in Exchange anymore can be deleted out of the sync + if(exchangeIdsPart[i][3] == "ErrorItemNotFound") + { + deletes.push(exchangeIdsPart[i][0]); + notFoundItems.push(exchangeIdsPart[i][0] , pUserID); + } + else + { + errorItems.push(exchangeIdsPart[i].toSource(), pUserID); + retMsg.push( exchangeIDs[1][0] + " : " + exchangeIDs[1][2] + " " + exchangeIDs[1][3] ); + } + } + + if(deletes.length > 0) + { + //delete out of sync table + db.deleteData("AB_SYNCCONTACT", " CONTACT_ID in ('" + deletes.join("', '") + "') and USER_ID = '" + pUserID + "'", "Data_alias", 300000); + + //remove set favorite for those which are delted - otherwise they would appear in the next run as 'new' Contacts + EwsClientSyncUtils.removeFromFavorite(deletes, pUserID); + deletedItems = deletedItems.concat(deletes); + } + } + }//while object > 0 + + if(doDebug && notFoundItems.length > 0) + logging.log("EWSSYnc not found items: " + notFoundItems.toSource()); + + if(errorItems.length > 0) + logging.log("EWSSYnc error items: " + errorItems.toSource()); + + if(doDebug && deletedItems.length > 0) + logging.log("EWSSYnc deleted items: " + deletedItems.toSource()); + }//contactsData.length > 0 + + return retMsg; +} + +/* + * updates the Synced Contacts for the passed User + * + * @param {String} pUserID ID of the User for which Contact should be updated + * @param {String} pMailBox Exchange Mail + * @param {String} pAlias name of the Exchange Alias + * @param {String} pRunDate lastRunDate of the sync + * @param {String} pUser Login of the User + * @param {boolean} doDebug + * +*/ +function EWSinsertContactsForUser(pUserID, pMailBox, pAlias, pRunDate, pUser, doDebug) +{ + var cond = newWhere("AB_SYNCCONTACT.EXCHANGEID is null") + .and("AB_SYNCCONTACT.USER_ID", pUserID); + + var contactsData = newSelect("AB_SYNCCONTACT.CONTACT_ID") + .from("AB_SYNCCONTACT") + .where(cond) + .arrayColumn(); + var retMsg = []; + if (contactsData.length > 0) + { + var config = EwsClientSyncUtils.getPlaceholders(pUserID) + var data = getAddressData(contactsData, config); + + var header = []; + for (let i = 0; i < data[0].length; i++) + { + header.push(data[0][i].replace(new RegExp("[{@}]","g"), "")) + } + + var dataObj; + var addrObj; + var objects = []; + var addrValueObjHome = {}; + var addrValueObjBusiness = {}; + var addrValueObjOther = {}; + + + //get ID of the ADITO-Ordners + var folderID = getExchangeFolderID(pAlias, pMailBox, "ADITO"); + if (folderID != "") + { + //loop through array at pos 1, header has been already collected + for (var i = 1; i < data.length; i++) + { + dataObj = new Object(); + addrObj = new Object(); + addrValueObjBusiness = {}; + addrValueObjHome = {}; + addrValueObjOther = {}; + + // The address data gets assigned to the respective object + for (let j = 0; j < header.length; j++) + { + addDataToValueObjects(header[j], data[i][j], dataObj, addrValueObjBusiness, addrValueObjHome, addrValueObjOther); + } + + buildAddrObject(addrObj, addrValueObjHome, addrValueObjBusiness, addrValueObjOther); + objects.push([dataObj, addrObj]); + } + + while(objects.length > 0) + { + let objectPart = objects.splice(0, 200); //current limit 200 + + + let exchangeIDs = EwsSyncContactUtils.insertContactsToFolder(pAlias, pMailBox, folderID, objectPart, doDebug); + + while(exchangeIDs[0].length > 0) + { + let exchangeIDsPart = exchangeIDs[0].splice(0, 30);//current limit 30 + updateADITOContactsAfterAction(exchangeIDsPart, pUserID); + } + + //when errors occured + while(exchangeIDs[1].length > 0) + { + retMsg.push( exchangeIDs[1][0] + " : " + exchangeIDs[1][2] + " " + exchangeIDs[1][3] ) + } + }//objects.length > 0 + } + else + return ["No folder 'ADITO' for account '" + pMailBox + "' and user '" + pUser + "' found"]; + } + return retMsg; +} + + +/* + * allocates passed headers/values to their object + * + * @param pHeader {String} fieldname in exchange + * @param pData {String} value for Header + * @param pDataObj {{}} object for contactdata + * @param pAddrValueObjBusiness {{}} adressobject Organisations + * @param pAddrValueObjHome {{}} adressobject homeadress + * @param pAddrValueObjOther {{}} adressobject others + * + */ +function addDataToValueObjects(pHeader, pData, pDataObj, pAddrValueObjBusiness, pAddrValueObjHome, pAddrValueObjOther) +{ + //with used appendChild method the pData values will be escaped automatically + + //differ contactdata and adressdata + //none of the abbreviation defined by the config + var headerNotNull = ["EmailAdress1", "EmailAddress2", "BusinessPhone", "BusinessHomepage", "HomePhone", "HomeFax", "OtherFax", "MobilePhone"]; + + if (pData != "" || headerNotNull.indexOf(pHeader) == -1) + { + if (pHeader.indexOf("Business_") == -1 && pHeader.indexOf("Home_") == -1 && pHeader.indexOf("Other_") == -1 && pData != "") + { + pDataObj[pHeader] = { + "key" : pHeader, + "value" : pData + }; + } + else + { + //Organisation Adress + if (pHeader.indexOf("Business") != -1) + { + pHeader = pHeader.replace("Business_", ""); + pAddrValueObjBusiness[pHeader] = { + "key" : pHeader, + "value" : pData + } + } + //Privatadress + else if (pHeader.indexOf("Home") != -1) + { + pHeader = pHeader.replace("Home_", ""); + pAddrValueObjHome[pHeader] = { + "key" : pHeader, + "value" : pData + } + } + //other Adress + else if (pHeader.indexOf("Other") != -1) + { + pHeader = pHeader.replace("Other_", ""); + pAddrValueObjOther[pHeader] = { + "key" : pHeader, + "value" : pData + } + } + } + } +} + +/* + * Build Address Object for the plugin call + * + * @param pAddrObject {{}} addressobject for call + * @param pAddrValueObjHome {{}} adressobject with Privatadresse + * @param pAddrValueObjBusiness {{}} adressobject for Organisation + * @param pAddrValueObjOther {{}} adressobject for other adresses + */ +function buildAddrObject(pAddrObject, pAddrValueObjHome, pAddrValueObjBusiness, pAddrValueObjOther) +{ + var homeOk = false; + var businessOk = false; + var otherOk = false; + + for each (var obj in pAddrValueObjHome) + { + if (obj["value"] != "") + { + homeOk = true; + break; + } + } + for each (obj in pAddrValueObjBusiness) + { + if (obj["value"] != "") + { + businessOk = true; + break; + } + } + for each (obj in pAddrValueObjOther) + { + if (obj["value"] != "") + { + otherOk = true; + break; + } + } + + if (homeOk) + { + pAddrObject["Home"] = { + "addressKey" : "Home", + "value" : pAddrValueObjHome + }; + } + if (businessOk) + { + pAddrObject["Business"] = { + "addressKey" : "Business", + "value" : pAddrValueObjBusiness + }; + } + if (otherOk) + { + pAddrObject["Other"] = { + "addressKey" : "Other", + "value" : pAddrValueObjOther + }; + } +} + +/* + * returns the exchangeID of the ADITO Folder of an exchange User + * if no folder is passed all folderIDs will be returned + * + * @param {String} pAlias exchange-alias + * @param {String} pMailBox mail-adress of the user + * @param {String} pFolderName name of the folder for which the ID should returned none if all + */ +function getExchangeFolderID(pAlias, pMailBox, pFolderName) +{ + var folders = EwsSyncContactUtils.getContactFolders(pAlias, pMailBox); + + for ( var i = 0; i < folders.length; i++) + { + if( folders[i].name.toUpperCase() == pFolderName.toUpperCase() ) + return folders[i].id; + } + return ""; +} + +function escapeValuesForXML(pValue){ + + var reps = {}; + reps["&"] = "&"; + reps["\""] = """; + reps["'"] = "&apos"; + reps["<"] = "<"; + reps[">"] = ">"; + + pValue = text.replaceAll(pValue, reps).trim(); + + return pValue +} + +/* +* Updates the Edit-Date and the exchangeID into the associated data set of AOSYS_SYNCCONTACT +* If the ExchangeID isn't passed, then it will just update DATE_EDIT +* +* @param {Array<Array>} pUpdateValues 2d-array in the following form: [[CONTACTID, matching new EXCHANGEID]] +* @param {String} pUserID ID of the corresponding user +*/ +function updateADITOContactsAfterAction(pUpdateValues, pUserID) +{ + var cols = ["EXCHANGEID"]; + var types = db.getColumnTypes("AB_SYNCCONTACT", cols); + var vals; + var cond = ""; + var updArr = []; + for (let i = 0; i < pUpdateValues.length; i++) + { + vals = []; + // Get EXCHANGEID + if (pUpdateValues[i][1]) + vals.push(pUpdateValues[i][1]); + + cond = "CONTACT_ID = '" + pUpdateValues[i][0] + "' and USER_ID = '" + pUserID + "'"; + updArr.push(["AB_SYNCCONTACT", cols, types, vals, cond]); + } + db.updates(updArr, "Data_alias", 300000); +}