MobileCaddy offers developers the ability to restrict data as it is refreshed from the Salesforce Platform to a device. We call these ‘Basic’ and ‘Platform Class’ restrictions – these are available from the ‘Change Data Restrictions’ button that sits on the Mobile Table record. The ‘Basic’ restriction offers the user the ability to restrict by a ‘User Id Location’ (a field containing an Id that must match the current user’s Id) and/or a ‘Boolean Restriction Field’ (a checkbox or formula checkbox that must be checked in order for the record to be considered for refresh). The ‘Platform Class’ (also referred to as ‘Restriction Class’) is an alternative method that we describe in this document in detail. The developer writes a class defining their restriction and ‘plugs this in’ to the MobileCaddy configuration. This document describes the restriction.
Versions
- “Restriction Interface 2″ is available from ‘Package Version 1.0.121” onwards.
- “Restriction Interface 1” is available from “Package Version 1.0.1” onwards.
Note that “Restriction Interface 2” is only available when using Data Sync Refresh Version 5.
Concept
Even though MobileCaddy offers the developer a great deal of flexibility when defining data restrictions on a Mobile Table using ‘Basic’ restrictions, there are some situations where a class is needed to refine the restrictions further. This is why the advanced ‘Platform Class’ restriction is available.
How To
Restriction Interface 2 – Data Sync/Refresh 5 (sync 5) and above.
This interface is the only one that supports paging and so may be using for Mobile Tables with or without paging switched on but only when using sync 5 or later. Follow the steps below.
1) Implement the mobilecaddy1.RestrictionInterface002_mc Interface
Your class must be global (as MobileCaddy will call it outside of its own mobilecaddy1 namespace) and it must implement this interface. The first line must be similar to the following.
1 2 3 |
global class CUSTOMER_CLASS_NAME implements mobilecaddy1.RestrictionInterface002_mc { |
The interface has only one method. In fact the entire interface follows.
1 2 3 4 5 6 7 8 |
// Made global so can be used outside of the package global interface RestrictionInterface002_mc { // Returns a json formatted string containing required field 'ids' containing a list of ids String returnRestrictedIds(String jsonParams); } |
The developer needs to implement the single method ‘returnRestrictedIds’. Astute developers who have previously written restriction classes for ‘Restriction Interface 1’ will note that the response from the method is now a ‘String’ an no longer a ‘Set<Id>’. This response is JSON that does contain the restricted Ids as well as additional information.
2) Extract Information from ‘String jsonParams’.
Deserialise Incoming Parameters
The jsonParams must be deserialise prior to use as shown below.
1 2 3 4 5 6 7 |
global String returnRestrictedIds(String jsonParams) { // Deserialise the incoming parameters Map<String,Object> int2ParamMap = (Map<String,Object>)JSON.deserializeUntyped(jsonParams); ... |
Retrieve Mobile Table Record and Extract Field Values
The incoming ‘jsonParams’ contain the entire Mobile Table record as configured in MobileCaddy. Developers can access the configuration in their class allowing them to write complex restrictions (e.g. use the SObject Name to write a class that is dynamic and may be used across several Mobile Tables, use the Paging Configuration to correctly restrict the data returned on first query etc). The Mobile Table Record may be extracted as a map as shown below.
1 2 3 4 5 6 7 |
// Deserialise the incoming parameters Map<String,Object> int2ParamMap = (Map<String,Object>)JSON.deserializeUntyped(jsonParams); // Get Mobile Table as a map Map<String,Object> recNameValuePairsMap = (Map<String,Object>)int2ParamMap.get('mobileTableRec'); |
The Mobile Table map shown will look similar to this:
1 2 3 |
{Id=a0W6E000000CYO2UAO, RecordTypeId=0126E0000004n78QAA, attributes={type=mobilecaddy1__Mobile_Table__c, url=/services/data/v41.0/sobjects/mobilecaddy1__Mobile_Table__c/a0W6E000000CYO2UAO}, mobilecaddy1__Boolean_Restriction_Field__c=mobilecaddy1__MC12001R_0001__c, mobilecaddy1__Conflict_Resolution_Logging__c=On - Log per Record, mobilecaddy1__Create_Failure_Behaviour__c=Per Record Failure (delete device records), mobilecaddy1__Create_Failure_Logging__c=On - Log per Record, mobilecaddy1__Device_Create_Failure_Behaviour__c=Retain/Retry, mobilecaddy1__Device_Hard_Deleted_Behaviour__c=Delete, mobilecaddy1__Device_Soft_Deleted_Behaviour__c=Delete, ...} |
Extracting Ids Already Processed
MobileCaddy refreshes in Batches of Pages designed around Salesforce Governor Limits. A ‘Batch’ contains multiple ‘Pages’. If there are so many records present that MobileCaddy hits the maximum governor limit for query rows, then MobileCaddy ‘assumes’ there is more data available and following all pages of the first batch being sent to the device, the device will perform a refresh for a second batch. When MobileCaddy executes the Platform Class it will pass in all ids that were processed on previous batches – thus enabling the developer to exclude these from the id query in the class. This code shows us extracting the ids and then using them in the query.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Collect set of ids that have already been processed. MobileCaddy provides these if you have already processed these // and a further batch has been requested. This enables them to be missed in this query. Set<Id> idsAlreadyProcessed = new Set<Id>(); // Extract from incoming param List<Object> idsAlreadyProcessedList = (List<Object>)int2ParamMap.get('idsAlreadyProcessed'); // Only extract if there are any ! if (idsAlreadyProcessedList != null) { for (Object idObject : idsAlreadyProcessedList) { idsAlreadyProcessed.add((Id)idObject); } } ... String queryString = 'Select Id From ' + sObjectName + ' Where (Not (Id in :idsAlreadyProcessed)) And .... // Query in records and ignore those already process Map<Id,SObject> restMap = new Map<Id,SObject>(Database.query(queryString)); |
See below how we ‘tell’ MobileCaddy that we need to request another batch.
Extracting ‘isInstall’ Parameter
The ‘isInstall’ parameters tells the class that the user is currently installing data for the first time from this Mobile Table and is not merely refreshing a table that is already on their device. This value is useful when paging – this is because there are different data limits defined in our Paging Configuration that the class must consider. The parameter is extracted as follows.
1 2 3 4 |
// Are we installing MobileCaddy or is this a subsequent refresh? Boolean isInstall = (Boolean)int2ParamMap.get('isInstall'); |
If the developer has created a Mobile Table that implements paging then there are several ‘Best Practice’ steps that must be followed. These are:
- Check that paging is indeed on! You can test the Mobile Table field ‘mobilecaddy1__Use_Paging__c’ that will be ‘true’ if paging is on. This enables you to write code that might (a) throw an exception if you are not happy with the value (see below for throwing an exception) or (b) allow paging / non paging in your class if this meets your business requirement.
- When paging obtain the ‘Refresh Limit’. This differs when installing to subsequent paging. Sample code to obtain this limit follows:
1 2 3 4 5 6 7 8 |
// Obtain the maximum number of records we are allowed to query in this class we must not query more than this. // This value comes through from settings on the Mobile Table which may be tuned according to actual data volumes. Integer refreshRecordLimit = (Integer)recNameValuePairsMap.get('mobilecaddy1__Refresh_Record_Limit__c'); // We have a different config value for refresh limit on an initial install (when MC is doing more work!) if (isInstall) refreshRecordLimit = (Integer)recNameValuePairsMap.get('mobilecaddy1__Refresh_Record_Limit_Install__c'); |
From this point on, the developer should use this value (here ‘refreshRecordLimit’) in place of the ‘getQueryRows’ governor limit. A Platform Class may have several queries with the final query usually being that that extracts the Ids to be returned to MoibleCaddy. The developer must not exceed the limit in ‘refreshRecordLimit’ or the refresh will fail. Where the class contains only a single query then this is relatively straightforward:
1 2 3 4 5 6 |
String queryString = 'Select Id From ' + sObjectName + ' Where (Not (Id in :idsAlreadyProcessed)) And (Not (Name Like 'FQT-%6')) Limit :refreshRecordLimit'; // Query in records and ignore those already process Map<Id,SObject> restMap = new Map<Id,SObject>(Database.query(queryString)); |
This code shows the limit statement on its single query. If the developer needs to make several queries prior to the final ‘id list’ query then it is important that none of them blow our limit.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Query custom field on opps Map<Id,Opportunity> oppMap = new Map<Id,Opportunity>[Select custom_field__c From Opportunity Where ... Limit :refreshRecordLimit]; ... Integer newLimit = refreshRecordLimit - oppMap.size(); ... String queryString = 'Select Id From ' + sObjectName + ' Where (Not (Id in :idsAlreadyProcessed)) And (Not (Name Like 'FQT-%6')) Limit :newLimit'; ... |
Warning: under some circumstances this approach is impractical. Suppose that the Mobile Table is on Account records and prior to issuing the final ‘Primary’ query, the Platform Class issues at least one ‘Secondary’ query in preparation on a custom field on Opportunities (for example to restrict the account query based on the set of results). If the Secondary query returns a huge number of records (10s of thousands) then it leaves so little room to actually query the Accounts that it makes little sense continuing with this approach and the developer should return to the design phase to attempt to overcome this issue. The developer might like to use the MobileCaddy Limits Exception (see below) to throw an exception where secondary queries are returning vast numbers of records.
Depending on how complex the developer’s class is, there could be multiple queries with the limit being reduced each time. The developer must keep track of this final limit for the next item.
- We need to tell MobileCaddy if more batches are required. When we reach our final (perhaps our only) query in the Platform Class as discussed above, we check to see if the number of rows returned from the query have hit the limit that we gave the query. If it does then we assume there are more (better safe than sorry!) and tell MobileCaddy to issue a further batch refresh – in this case MobileCaddy will automatically pass in ids on the device as shown in ‘ids already processed’ above. An example is shown below.
1 2 3 4 5 6 7 8 9 |
Boolean moreBatches; if (restMap.size() == refreshRecordLimit) { moreBatches = true; } else { moreBatches = false; } |
Build Response to MobileCaddy
The response is a serialised JSON string containing the record ids queried and the ‘moreBatches’ flag. This is created as follows.
1 2 3 4 5 6 7 |
// Now create the response map Map<String,Object> responseMap = new Map<String,Object>(); responseMap.put('ids',restMap.keySet()); responseMap.put('moreBatches',moreBatches); return JSON.serialize(responseMap); |
Throw MobileCaddy Exception
From package 1.0.121 we have opened up the MobileCaddy Exception class so that it may be thrown in a customer Platform Class. This means that customer exceptions will be captured in the same way as MobileCaddy’s own exceptions, with a Mobile Log being created against the Connection Session containing the developers error text. The exception class is called MC_001_mcException and it may be thrown as shown.
1 2 3 |
throw new mobilecaddy1.MC_001_mcException(mobilecaddy1.MC_001_mcException.MC_EXT_CLASS_GENERIC,'Customer generic message goes here'); |
These messages will be reported in the Mobile Logs as error number ‘1000’. There is a further exception available (1001) which a developer uses where data volumes from Secondary Queries are so large that it is impractical continuing (see above).
1 2 3 |
throw new mobilecaddy1.MC_001_mcException(mobilecaddy1.MC_001_mcException.MC_EXT_CLASS_LIMITS,'Hit Limits'); |
Entering into Configuration
When a developer creates a brand new class and wishes to use it for the first time, the ‘Create Platform Object Support Entry’ button on the Mobile Table must be pressed. Enter the class name and when MobileCaddy confirms the save, click ‘Cancel’ to return to the Mobile Table record (remember the name of the created record!). Next click ‘Change Data Restrictions’ and choose ‘Platform Class’ and in the lookup choose the Platform Object Support Entry that was created.
Examples
A sample restriction 2 class is shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
global class sync5_generic_restriction implements mobilecaddy1.RestrictionInterface002_mc { global String returnRestrictedIds(String jsonParams) { // Deserialise the incoming parameters Map<String,Object> int2ParamMap = (Map<String,Object>)JSON.deserializeUntyped(jsonParams); // Retrieve the mobile table record. This map will look like this example: // {Id=a0W6E000000CYO2UAO, RecordTypeId=0126E0000004n78QAA, attributes={type=mobilecaddy1__Mobile_Table__c, url=/services/data/v41.0/sobjects/mobilecaddy1__Mobile_Table__c/a0W6E000000CYO2UAO}, // mobilecaddy1__Boolean_Restriction_Field__c=mobilecaddy1__MC12001R_0001__c, mobilecaddy1__Conflict_Resolution_Logging__c=On - Log per Record, // mobilecaddy1__Create_Failure_Behaviour__c=Per Record Failure (delete device records), mobilecaddy1__Create_Failure_Logging__c=On - Log per Record, // mobilecaddy1__Device_Create_Failure_Behaviour__c=Retain/Retry, mobilecaddy1__Device_Hard_Deleted_Behaviour__c=Delete, mobilecaddy1__Device_Soft_Deleted_Behaviour__c=Delete, ...} Map<String,Object> recNameValuePairsMap = (Map<String,Object>)int2ParamMap.get('mobileTableRec'); // Are we installing MobileCaddy or is this a subsequent refresh? Boolean isInstall = (Boolean)int2ParamMap.get('isInstall'); // Obtain the maximum number of records we are allowed to query n this class we must not query more than this. // This value comes through from settings on the Mobile Table which may be tuned according to actual data volumes. Integer refreshRecordLimit = (Integer)recNameValuePairsMap.get('mobilecaddy1__Refresh_Record_Limit__c'); // We have a different config value for refresh limit on an initial install (when MC is doing more work!) if (isInstall) refreshRecordLimit = (Integer)recNameValuePairsMap.get('mobilecaddy1__Refresh_Record_Limit_Install__c'); // Example exception command - throw MobileCaddy exception to get a log against your connection session // throw new mobilecaddy1.MC_001_mcException(mobilecaddy1.MC_001_mcException.MC_EXT_CLASS_GENERIC,'Customer generic message goes here'); // Collect set of ids that have already been processed. MobileCaddy provides these if you have already processed these // and a further batch has been requested. This enables them to be missed in this query. Set<Id> idsAlreadyProcessed = new Set<Id>(); // Extract from incoming param List<Object> idsAlreadyProcessedList = (List<Object>)int2ParamMap.get('idsAlreadyProcessed'); // Only extract if there are any ! if (idsAlreadyProcessedList != null) { for (Object idObject : idsAlreadyProcessedList) { idsAlreadyProcessed.add((Id)idObject); } } // Get the sObject name for our query (could hard code but shows we have it mobile table) and enables // us to reuse this class across multiple objects String sObjectName = (String)recNameValuePairsMap.get('mobilecaddy1__SObject_Name__c'); if (Test.isRunningTest()) sObjectName = 'page_on_medium_data__c'; // Build up the query string. In this example we filter out records with a single digit autonumber name! String queryString = 'Select Id From ' + sObjectName + ' Where (Not (Id in :idsAlreadyProcessed)) And (Not (Name Like 'FQT-%6')) Limit :refreshRecordLimit'; // Query in records and ignore those already process Map<Id,SObject> restMap = new Map<Id,SObject>(Database.query(queryString)); // If we have hit our refresh record limit then we must assume there are more batches equired. Tell MobileCaddy. Boolean moreBatches; if (restMap.size() == refreshRecordLimit) { moreBatches = true; } else { moreBatches = false; } // Now create the response map Map<String,Object> responseMap = new Map<String,Object>(); responseMap.put('ids',restMap.keySet()); responseMap.put('moreBatches',moreBatches); return JSON.serialize(responseMap); } } |
The associated Test Class might look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@isTest private class sync5_generic_restriction_tests { @testSetup static void initData() { page_on_medium_data__c rec = new page_on_medium_data__c(); insert rec; } // end initData public static testMethod void testCreateNewVersion() { mobilecaddy1__Mobile_Table__c mobileTableRec = new mobilecaddy1__Mobile_Table__c( mobilecaddy1__Mobile_Table_Version__c = '001', mobilecaddy1__Mobile_Table_Status__c = 'Inactive', mobilecaddy1__Dev_Status__c = 'Developing', mobilecaddy1__Sync_Priority__c = 1, mobilecaddy1__Build_Order__c = 1, mobilecaddy1__Refresh_Record_Limit__c = 1000, mobilecaddy1__Refresh_Record_Limit_Install__c = 1000 ); // Prepare map of input parameters to interace 2 Map<String,Object> int2ParamMap = new Map<String,Object>(); // Provide entire mobile table record to external class int2ParamMap.put('mobileTableRec',mobileTableRec); // Mark as a new install int2ParamMap.put('isInstall',true); // Provide set of ids already processed so that external class may ignore them on a more batch query int2ParamMap.put('idsAlreadyProcessed',null); // Serialise the map into JSON as a parameter String param = JSON.serialize(int2ParamMap); // Make the call and get the response sync5_generic_restriction restInst = new sync5_generic_restriction(); String supportResponse = restInst.returnRestrictedIds(param); } // end testMethod } // end class |