Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2017-present, salesforce.com, inc.
* All rights reserved.
* Redistribution and use of this software in source and binary forms, with or
* without modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of salesforce.com, inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission of salesforce.com, inc.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.salesforce.androidsdk.mobilesync.target
import com.salesforce.androidsdk.mobilesync.app.Features
import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager.MobileSyncException
import com.salesforce.androidsdk.mobilesync.target.CompositeRequestHelper.RecordRequest
import com.salesforce.androidsdk.mobilesync.target.CompositeRequestHelper.RecordResponse
import com.salesforce.androidsdk.mobilesync.target.ParentChildrenSyncTargetHelper.RelationshipType
import com.salesforce.androidsdk.mobilesync.util.ChildrenInfo
import com.salesforce.androidsdk.mobilesync.util.Constants
import com.salesforce.androidsdk.mobilesync.util.MobileSyncLogger
import com.salesforce.androidsdk.mobilesync.util.ParentInfo
import com.salesforce.androidsdk.mobilesync.util.SOQLBuilder
import com.salesforce.androidsdk.mobilesync.util.SyncState.MergeMode
import com.salesforce.androidsdk.rest.RestRequest
import com.salesforce.androidsdk.util.JSONObjectHelper
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.util.LinkedList
/**
* Target for sync that uploads parent with children records
*/
open class ParentChildrenSyncUpTarget(
private val parentInfo: ParentInfo,
parentCreateFieldlist: List?,
parentUpdateFieldlist: List?,
private val childrenInfo: ChildrenInfo,
private val childrenCreateFieldlist: List,
private val childrenUpdateFieldlist: List,
private val relationshipType: RelationshipType
) : SyncUpTarget(parentCreateFieldlist, parentUpdateFieldlist), AdvancedSyncUpTarget {
constructor(target: JSONObject) : this(
ParentInfo(target.getJSONObject(ParentChildrenSyncTargetHelper.PARENT)),
JSONObjectHelper.toList(target.optJSONArray(CREATE_FIELDLIST)),
JSONObjectHelper.toList(target.optJSONArray(UPDATE_FIELDLIST)),
ChildrenInfo(target.getJSONObject(ParentChildrenSyncTargetHelper.CHILDREN)),
JSONObjectHelper.toList(target.optJSONArray(CHILDREN_CREATE_FIELDLIST)),
JSONObjectHelper.toList(target.optJSONArray(CHILDREN_UPDATE_FIELDLIST)),
RelationshipType.valueOf(target.getString(ParentChildrenSyncTargetHelper.RELATIONSHIP_TYPE))
)
init {
MobileSyncSDKManager.getInstance()
.registerUsedAppFeature(Features.FEATURE_RELATED_RECORDS)
}
@Throws(JSONException::class)
override fun asJSON(): JSONObject {
return with(super.asJSON()) {
put(ParentChildrenSyncTargetHelper.PARENT, parentInfo.asJSON())
put(ParentChildrenSyncTargetHelper.CHILDREN, childrenInfo.asJSON())
put(CHILDREN_CREATE_FIELDLIST, JSONArray(childrenCreateFieldlist))
put(CHILDREN_UPDATE_FIELDLIST, JSONArray(childrenUpdateFieldlist))
put(ParentChildrenSyncTargetHelper.RELATIONSHIP_TYPE, relationshipType.name)
}
}
override fun getDirtyRecordIdsSql(soupName: String, idField: String): String {
return ParentChildrenSyncTargetHelper.getDirtyRecordIdsSql(
parentInfo,
childrenInfo,
idField
)
}
override fun createOnServer(
syncManager: SyncManager,
record: JSONObject,
fieldlist: List?
): String {
throw UnsupportedOperationException("For advanced sync up target, call syncUpOneRecord")
}
override fun deleteOnServer(syncManager: SyncManager, record: JSONObject): Int {
throw UnsupportedOperationException("For advanced sync up target, call syncUpOneRecord")
}
override fun updateOnServer(
syncManager: SyncManager,
record: JSONObject,
fieldlist: List?
): Int {
throw UnsupportedOperationException("For advanced sync up target, call syncUpOneRecord")
}
override val maxBatchSize: Int
get() = 1
@Throws(JSONException::class, IOException::class, MobileSyncException::class)
override fun syncUpRecords(
syncManager: SyncManager,
records: List,
fieldlist: List?,
mergeMode: MergeMode,
syncSoupName: String
) {
if (records.size > 1) {
throw MobileSyncException(javaClass.simpleName + ":syncUpRecords can handle only 1 record at a time")
}
if (records.isNotEmpty()) {
syncUpRecord(syncManager, records[0], fieldlist, mergeMode)
}
}
@Throws(JSONException::class, IOException::class)
private fun syncUpRecord(
syncManager: SyncManager,
record: JSONObject,
fieldlist: List?,
mergeMode: MergeMode?
) {
val isCreate = isLocallyCreated(record)
val isDelete = isLocallyDeleted(record)
// Getting children
val children =
if (relationshipType == RelationshipType.MASTER_DETAIL && isDelete && !isCreate // deleting master in a master-detail relationship will delete the children
// so no need to actually do any work on the children
) JSONArray() else ParentChildrenSyncTargetHelper.getChildrenFromLocalStore(
syncManager.smartStore,
parentInfo,
childrenInfo,
record
)
syncUpRecord(syncManager, record, children, fieldlist, mergeMode)
}
@Throws(JSONException::class, IOException::class)
private fun syncUpRecord(
syncManager: SyncManager,
record: JSONObject,
children: JSONArray,
fieldlist: List?,
mergeMode: MergeMode?
) {
val isCreate = isLocallyCreated(record)
val isDelete = isLocallyDeleted(record)
// Preparing request for parent
val recordRequests: MutableList = LinkedList()
val parentId = record.getString(idFieldName)
val parentRequest = buildRequestForParentRecord(record, fieldlist)
// Parent request goes first unless it's a delete
if (parentRequest != null && !isDelete) {
parentRequest.referenceId = parentId
recordRequests.add(parentRequest)
}
// Preparing requests for children
for (i in 0 until children.length()) {
val childRecord = children.getJSONObject(i)
val childId = childRecord.getString(childrenInfo.idFieldName)
// Parent will get a server id
// Children need to be updated
if (isCreate) {
childRecord.put(LOCAL, true)
childRecord.put(LOCALLY_UPDATED, true)
}
val childRequest = buildRequestForChildRecord(
childRecord, isCreate,
if (isDelete) null else parentId
)
if (childRequest != null) {
childRequest.referenceId = childId
recordRequests.add(childRequest)
}
}
// Parent request goes last when it's a delete
if (parentRequest != null && isDelete) {
parentRequest.referenceId = parentId
recordRequests.add(parentRequest)
}
// Sending composite request
val refIdToRecordResponses =
CompositeRequestHelper.sendAsCompositeBatchRequest(syncManager, false, recordRequests)
// Build refId to server id map
val refIdToServerId = CompositeRequestHelper.parseIdsFromResponses(refIdToRecordResponses)
// Will a re-run be required?
var needReRun = false
// Update parent in local store
if (isDirty(record)) {
needReRun = updateParentRecordInLocalStore(
syncManager, record, children, mergeMode, refIdToServerId,
refIdToRecordResponses[record.getString(idFieldName)]
)
}
// Update children local store
for (i in 0 until children.length()) {
val childRecord = children.getJSONObject(i)
if (isDirty(childRecord) || isCreate) {
needReRun = needReRun || updateChildRecordInLocalStore(
syncManager, childRecord, record, mergeMode, refIdToServerId,
refIdToRecordResponses[childRecord.getString(childrenInfo.idFieldName)]
)
}
}
// Re-run if required
if (needReRun) {
MobileSyncLogger.d(TAG, "syncUpOneRecord", record)
syncUpRecord(syncManager, record, children, fieldlist, mergeMode)
}
}
@Throws(JSONException::class, IOException::class)
protected fun updateParentRecordInLocalStore(
syncManager: SyncManager,
record: JSONObject,
children: JSONArray,
mergeMode: MergeMode?,
refIdToServerId: Map,
response: RecordResponse?
): Boolean {
var needReRun = false
val soupName = parentInfo.soupName
val lastError = response?.errorJson?.toString()
// Delete case
if (isLocallyDeleted(record)) {
if (isLocallyCreated(record) // we didn't go to the server
|| response?.success == true // or we successfully deleted on the server
|| response?.recordDoesNotExist == true // or the record was already deleted on the server
) {
if (relationshipType == RelationshipType.MASTER_DETAIL) {
ParentChildrenSyncTargetHelper.deleteChildrenFromLocalStore(
syncManager.smartStore, parentInfo, childrenInfo, record.getString(
idFieldName
)
)
}
deleteFromLocalStore(syncManager, soupName, record)
} else {
saveRecordToLocalStoreWithError(syncManager, soupName, record, lastError)
}
} else {
// Success case
if (response?.success == true) {
// Plugging server id in id field
CompositeRequestHelper.updateReferences(record, idFieldName, refIdToServerId)
// Clean and save
cleanAndSaveInLocalStore(syncManager, soupName, record)
} else if (response?.recordDoesNotExist == true) {
// Record needs to be recreated
if (mergeMode == MergeMode.OVERWRITE) {
record.put(LOCAL, true)
record.put(LOCALLY_CREATED, true)
// Children need to be updated or recreated as well (since the parent will get a new server id)
for (i in 0 until children.length()) {
val childRecord = children.getJSONObject(i)
childRecord.put(LOCAL, true)
childRecord.put(
if (relationshipType == RelationshipType.MASTER_DETAIL) LOCALLY_CREATED else LOCALLY_UPDATED,
true
)
}
needReRun = true
}
} else {
saveRecordToLocalStoreWithError(syncManager, soupName, record, lastError)
}
}
return needReRun
}
@Throws(JSONException::class)
protected fun updateChildRecordInLocalStore(
syncManager: SyncManager,
record: JSONObject,
parentRecord: JSONObject,
mergeMode: MergeMode?,
refIdToServerId: Map,
response: RecordResponse?
): Boolean {
var needReRun = false
val soupName = childrenInfo.soupName
val lastError = response?.errorJson?.toString()
// Delete case
if (isLocallyDeleted(record)) {
if (isLocallyCreated(record) // we didn't go to the server
|| response?.success == true // or we successfully deleted on the server
|| response?.recordDoesNotExist == true // or the record was already deleted on the server
) {
deleteFromLocalStore(syncManager, soupName, record)
} else {
saveRecordToLocalStoreWithError(syncManager, soupName, record, lastError)
}
} else {
// Success case
if (response?.success == true) {
// Plugging server id in id field
CompositeRequestHelper.updateReferences(
record,
childrenInfo.idFieldName,
refIdToServerId
)
// Plugging server id in parent id field
CompositeRequestHelper.updateReferences(
record,
childrenInfo.parentIdFieldName,
refIdToServerId
)
// Clean and save
cleanAndSaveInLocalStore(syncManager, soupName, record)
} else if (response?.recordDoesNotExist == true) {
// Record needs to be recreated
if (mergeMode == MergeMode.OVERWRITE) {
record.put(LOCAL, true)
record.put(LOCALLY_CREATED, true)
// We need a re-run
needReRun = true
}
} else if (response?.relatedRecordDoesNotExist == true) {
// Parent record needs to be recreated
if (mergeMode == MergeMode.OVERWRITE) {
parentRecord.put(LOCAL, true)
parentRecord.put(LOCALLY_CREATED, true)
// We need a re-run
needReRun = true
}
} else {
saveRecordToLocalStoreWithError(syncManager, soupName, record, lastError)
}
}
return needReRun
}
@Throws(IOException::class, JSONException::class)
protected fun buildRequestForParentRecord(
record: JSONObject,
fieldlist: List?
): RecordRequest? {
return buildRequestForRecord(
record, fieldlist,
isParent = true,
useParentIdReference = false,
parentId = null
)
}
@Throws(IOException::class, JSONException::class)
protected fun buildRequestForChildRecord(
record: JSONObject,
useParentIdReference: Boolean,
parentId: String?
): RecordRequest? {
return buildRequestForRecord(
record,
null,
false,
useParentIdReference,
parentId
)
}
@Throws(IOException::class, JSONException::class, MobileSyncException::class)
protected fun buildRequestForRecord(
record: JSONObject,
fieldlist: List?,
isParent: Boolean,
useParentIdReference: Boolean,
parentId: String?
): RecordRequest? {
if (!isDirty(record)) {
return null // nothing to do
}
val info = if (isParent) parentInfo else childrenInfo
val id = record.getString(info.idFieldName)
// Delete case
val isDelete = isLocallyDeleted(record)
val isCreate = isLocallyCreated(record)
return if (isDelete) {
if (isCreate) {
null // no need to go to server
} else {
RecordRequest.requestForDelete(info.sobjectType, id)
}
} else {
val fieldlistToUse = if (isParent) {
if (isCreate) {
createFieldlist ?: fieldlist
} else {
updateFieldlist ?: fieldlist
}
} else {
if (isCreate) {
childrenCreateFieldlist
} else {
childrenUpdateFieldlist
}
} ?: throw MobileSyncException("No fields specified")
val fields =
buildFieldsMap(
record,
fieldlistToUse,
info.idFieldName,
info.modificationDateFieldName
)
if (parentId != null) {
fields[(info as ChildrenInfo).parentIdFieldName] =
if (useParentIdReference) String.format(
"@{%s.%s}",
parentId,
Constants.LID
) else parentId
}
if (isCreate) {
val externalId = if (info.externalIdFieldName != null) JSONObjectHelper.optString(
record,
info.externalIdFieldName
) else null
if (externalId != null // the following check is there for the case
// where the the external id field is the id field
// and the field is populated by a local id
&& !isLocalId(externalId)
) {
RecordRequest.requestForUpsert(
info.sobjectType,
info.externalIdFieldName,
externalId,
fields
)
} else {
RecordRequest.requestForCreate(info.sobjectType, fields)
}
} else {
RecordRequest.requestForUpdate(
info.sobjectType,
id,
fields
)
}
}
}
@Throws(JSONException::class, IOException::class)
override fun isNewerThanServer(syncManager: SyncManager, record: JSONObject): Boolean {
if (isLocallyCreated(record)) {
return true
}
val idToLocalTimestamps = getLocalLastModifiedDates(syncManager, record)
val idToRemoteTimestamps = fetchLastModifiedDates(syncManager, record)
for (id in idToLocalTimestamps.keys) {
val localModDate = idToLocalTimestamps[id]
val remoteTimestamp = idToRemoteTimestamps[id]
val remoteModDate = RecordModDate(
remoteTimestamp,
remoteTimestamp == null // if it wasn't returned by fetchLastModifiedDates, then the record must have been deleted
)
if (!super.isNewerThanServer(localModDate, remoteModDate)) {
return false // no need to go further
}
}
return true
}
/**
* Get local last modified dates for a given record and its children
* @param syncManager
* @param record
* @return
*/
@Throws(JSONException::class)
protected fun getLocalLastModifiedDates(
syncManager: SyncManager,
record: JSONObject
): Map {
val idToLocalTimestamps: MutableMap = HashMap()
val isParentDeleted = isLocallyDeleted(record)
val parentModDate = RecordModDate(
JSONObjectHelper.optString(record, modificationDateFieldName),
isParentDeleted
)
idToLocalTimestamps[record.getString(idFieldName)] = parentModDate
val children = ParentChildrenSyncTargetHelper.getChildrenFromLocalStore(
syncManager.smartStore,
parentInfo,
childrenInfo,
record
)
for (i in 0 until children.length()) {
val childRecord = children.getJSONObject(i)
val childModDate = RecordModDate(
JSONObjectHelper.optString(childRecord, childrenInfo.modificationDateFieldName),
isLocallyDeleted(childRecord) || isParentDeleted && relationshipType == RelationshipType.MASTER_DETAIL
)
idToLocalTimestamps[childRecord.getString(childrenInfo.idFieldName)] = childModDate
}
return idToLocalTimestamps
}
/**
* Fetch last modified dates for a given record and its chidlren
* @param syncManager
* @param record
* @return
*/
@Throws(JSONException::class, IOException::class)
protected fun fetchLastModifiedDates(
syncManager: SyncManager,
record: JSONObject
): Map {
val idToRemoteTimestamps: MutableMap = HashMap()
if (!isLocallyCreated(record)) {
val parentId = record.getString(idFieldName)
val lastModRequest = getRequestForTimestamps(syncManager.apiVersion, parentId)
val lastModResponse = syncManager.sendSyncWithMobileSyncUserAgent(lastModRequest)
val rows = if (lastModResponse.isSuccess) lastModResponse.asJSONObject().getJSONArray(
Constants.RECORDS
) else null
if (rows != null && rows.length() > 0) {
val row = rows.getJSONObject(0)
idToRemoteTimestamps[row.getString(idFieldName)] = row.getString(
modificationDateFieldName
)
if (row.has(childrenInfo.sobjectTypePlural) && !row.isNull(childrenInfo.sobjectTypePlural)) {
val childrenRows =
row.getJSONObject(childrenInfo.sobjectTypePlural).getJSONArray(
Constants.RECORDS
)
for (i in 0 until childrenRows.length()) {
val childRow = childrenRows.getJSONObject(i)
idToRemoteTimestamps[childRow.getString(childrenInfo.idFieldName)] =
childRow.getString(
childrenInfo.modificationDateFieldName
)
}
}
}
}
return idToRemoteTimestamps
}
/**
* Build SOQL request to get current time stamps
*
* @param apiVersion
* @param parentId
* @return
* @throws UnsupportedEncodingException
*/
@Throws(UnsupportedEncodingException::class)
protected fun getRequestForTimestamps(apiVersion: String, parentId: String): RestRequest {
val builderNested: SOQLBuilder = SOQLBuilder.getInstanceWithFields(
childrenInfo.idFieldName,
childrenInfo.modificationDateFieldName
)
builderNested.from(childrenInfo.sobjectTypePlural)
val builder: SOQLBuilder = SOQLBuilder.getInstanceWithFields(
idFieldName,
modificationDateFieldName,
String.format("(%s)", builderNested.build())
)
builder.from(parentInfo.sobjectType)
builder.where(String.format("%s = '%s'", idFieldName, parentId))
return RestRequest.getRequestForQuery(apiVersion, builder.build())
}
companion object {
// Constants
const val CHILDREN_CREATE_FIELDLIST = "childrenCreateFieldlist"
const val CHILDREN_UPDATE_FIELDLIST = "childrenUpdateFieldlist"
}
}