Apex passing parameters to Batch job during scheduling
Posted on September 12, 2019 Leave a Comment
Here it the use case:
You have 1 job that you want to run weekly and monthly but the monthly job also has to generate a notification. You don’t need to create two classes but can pass a parameter to the schedule to know it is a weekly or monthly job.
The batch Job class that accepts constructor parameter jobRunFrequency
global class AccountBatchJob implements System.Schedulable, Database.Batchable<SObject>, Database.Stateful, Database.AllowsCallouts { private static final String RUN_WEEKLY_JOB = 'RUN_WEEKLY_JOB'; //This is the key that needs to be used to generate notifications for Monthly private static final String RUN_MONTHLY_JOB = 'RUN_MONTHLY_JOB'; private String jobRunFrequency; String query = 'Select Id, FirstName, LastName from Account'; public AccountBatchJob(){ this.jobRunFrequency = RUN_WEEKLY_JOB; } public AccountBatchJob(String jobRunFrequency){ this.jobRunFrequency = jobRunFrequency; } global Database.QueryLocator start(Database.BatchableContext BC) { return Database.getQueryLocator(query); } global void execute(Database.BatchableContext BC, List<Account> accounts) { .... if (RUN_MONTHLY_JOB.equalsIgnoreCase(jobRunFrequency)){ ...... } } }
Test class for batch Job passing parameter check if it is the Monthly job
Test.startTest(); Database.executeBatch(new AccountBatchJob('RUN_MONTHLY_JOB'), 1); Test.stopTest();
Test class Scheduler Job to check it if is the Monthly job
Test.startTest(); String jobId = System.schedule('AccountBatchJob', '0 0 0 15 3 ? 2022', new AccountBatchJob('RUN_MONTHLY_JOB')); CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId]; System.assertEquals('0 0 0 15 3 ? 2022', ct.CronExpression); Test.stopTest();
Apex Cache Tracker to remove session cache
Posted on August 5, 2019 Leave a Comment
Here is the problem that needed to be solved:
1. Use session cache to cache some user information like their profile or user settings
2. When a external systems update the information and try to remove the session cache as it got updated it fails because it does not have access to a users session cache
Solution:
Build a session tracker in Org Cache to track missed removal of session cache
Step 1: When a session cache remove fails add it to the Org level tracker cache. The format of session keys are the following:
key + userId (Salesforce userId)
That way I can keep track of what userId’s I need to remove session cache
String splitStrKey = key.substring(0, key.length()-18);
String splitUserId = key.substring(key.length()-18, key.length());
will split into key and userId
public Boolean remove(String key) { if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){ Boolean removed = false; if (sessionCacheEnabled && sessionPartition!=null) { System.debug('inside session cache remove key: ' + key); removed = sessionPartition.remove(key); if (!removed && key!=null && key.length() > 18){ String splitStrKey = key.substring(0, key.length()-18); String splitUserId = key.substring(key.length()-18, key.length()); System.debug('Session remove failed key ' + splitStrKey + ' for ' + splitUserId + ' adding to tracker'); removeFailedAddToTracker(splitStrKey, splitUserId, null); } } else if (orgPartition!=null){ removed = orgPartition.remove(key); } if (removed) { System.debug(LoggingLevel.DEBUG, 'Removed key ' + key); return true; } else{ System.debug(LoggingLevel.DEBUG, 'Not removed key not found ' + key); return false; } } else { System.debug(LoggingLevel.DEBUG, 'Skipping cache remove in batch ' + key); return false; } }
Step 2: Add session keys to Org level tracker cache (CACHE_USER_CACHE_TRACKER), we will add it as Map<Id, Map> where Map contains UserId as key and Map of all the session keys that needs to be removed
public void removeFailedAddToTracker(String keyPrefix, Id userId, Set<Id> clientIds){ if (App_Service.isIntegrationUser(userId)){ Map<String, String> mapOfContactUserIds = (Map<String, String>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CONTACT_MAP); Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER); if (clientIds!=null && !clientIds.isEmpty()){ for (Id clientId : clientIds){ if (mapOfContactUserIds!=null && mapOfContactUserIds.containsKey(clientId)){ Id userIdToClearCache = mapOfContactUserIds.get(clientId); if (mapOfExistingCacheQueueTracker!=null){ if (mapOfExistingCacheQueueTracker.containsKey(userIdToClearCache)){ Map<String, Id> existingCacheStrings = mapOfExistingCacheQueueTracker.get(userIdToClearCache); existingCacheStrings.put(keyPrefix, userIdToClearCache); mapOfExistingCacheQueueTracker.put(userIdToClearCache, existingCacheStrings); } else { mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache}); } } else { mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>(); mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache}); } } } } else { if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(userId)){ Map<String, Id> existingMap = mapOfExistingCacheQueueTracker.get(userId); existingMap.put(keyPrefix, userId); mapOfExistingCacheQueueTracker.put(userId, existingMap); } else { if (mapOfExistingCacheQueueTracker==null){ mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>(); } mapOfExistingCacheQueueTracker.put(userId, new Map<String, Id>{keyPrefix=>userId}); } } if (mapOfExistingCacheQueueTracker!=null) getOrgPartition().put(App_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker); } }
Step 3: Every time before we check if session cache contains a key we will see if the same key is contained in the CACHE_USER_CACHE_TRACKER cache. If yes remove it from the users session cache so that cache can be removed and new query can be done
public Boolean containsKey(String cacheKey){ Boolean containsKey = false; if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){ if (sessionCacheEnabled==false && orgPartition!=null){ if (orgPartition.get(cacheKey)!=null){ containsKey = true; } } else if (sessionCacheEnabled && sessionPartition!=null) { if (sessionPartition.get(cacheKey)!=null){ containsKey = true; Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER); if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(UserInfo.getUserId())){ Map<String, Id> flagsToClear = mapOfExistingCacheQueueTracker.get(UserInfo.getUserId()); Boolean removeFlag = false; for (String flagToClear : flagsToClear.keySet()){ if (flagToClear.equalsIgnoreCase(cacheKey)){ String keyToRemove = flagToClear + flagsToClear.get(flagToClear); Boolean removeItemFromCache = getSessionPartition().remove(keyToRemove); removeFlag = true; containsKey = false; } } if (removeFlag){ for (String flagToClear : flagsToClear.keySet()){ flagsToClear.remove(cacheKey); if (flagsToClear.isEmpty() && flagsToClear.size()==0){ mapOfExistingCacheQueueTracker.remove(UserInfo.getUserId()); } else { mapOfExistingCacheQueueTracker.put(UserInfo.getUserId(), flagsToClear); } } getOrgPartition().put(App_Rest_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker); } } } } } return containsKey; }
Apex Coding Challenge Find Highest Frequency of Numbers
Posted on June 4, 2019 Leave a Comment
Problem: Find the number that has the highest frequency in a list of integers.
Input: 1,6,2,1,6,1
Output: 1 //because 1 occurs 3 times in the list
Option 1: Use Hashmap to iterate list
List<Integer> nums = new List<Integer>{1,6,2,1,6,1}; Map<Integer, Integer> numMap = new HashMap<>(); for (Integer num : nums){ if (numMap.containsKey(num)){ Integer numFreq = numMap.get(num); numMap.put(num, numFreq+1); } else { numMap.put(num, 1); } } Integer biggestFreq = 0; Integer biggestVal = 0; for (Integer num : numMap.keySet()){ if (numMap.get(num) > biggestFreq){ biggestFreq = numMap.get(num); biggestVal = num; } } System.debug(biggestVal);
Option 2: Use wrapper class with compare to sort wrapper
List<Integer> nums = new List<Integer>{1,6,2,1,6,1}; Map<Integer, NumFrequencyWrapper> numMap = new HashMap<>(); for (Integer num : nums){ if (numMap.containsKey(num)){ NumFrequencyWrapper numFreqWrapper = numMap.get(num); numFreqWrapper.setFrequency(numFreqWrapper.getFrequency()+1); numMap.put(num, numFreqWrapper); } else { NumFrequencyWrapper numFrequencyWrapper = new NumFrequencyWrapper(); numFrequencyWrapper.setNum(num); numFrequencyWrapper.setFrequency(1); numMap.put(num, numFrequencyWrapper); } } List<NumFrequencyWrapper> frequencyWrapperList = new List(numMap.values()); Collections.sort(frequencyWrapperList, new Untitled.NumFrequencyWrapperCompare()); System.debug(frequencyWrapperList.get(0).getNum()); public class NumFrequencyWrapper { private Integer num; private Integer frequency; public void setNum(Integer num){ this.num = num; } public Integer getNum(){ return num; } public void setFrequency(Integer frequency){ this.frequency = frequency; } public Integer getFrequency(){ return this.frequency; } } public class NumFrequencyWrapperCompare implements Comparator<NumFrequencyWrapper>{ public int compare(NumFrequencyWrapper a, NumFrequencyWrapper b) { return b.getFrequency() - a.getFrequency(); } }
Option 3: Using buckets to group index of frequencies together
List<Integer> nums = new List<Integer>{1,6,2,1,6,1}; Integer returnNums = 2; Map<Integer, Integer> numMap = new Map<Integer, Integer>(); for (Integer num : nums){ if (numMap.containsKey(num)){ Integer numFreq = numMap.get(num); numMap.put(num, numFreq+1); } else { numMap.put(num, 1); } } Map<Integer, List<Integer>> mapOfBucketWithValues = new Map<Integer, List<Integer>>(); for (Integer num : numMap.keySet()){ Integer numFrequency = numMap.get(num); if (mapOfBucketWithValues.containsKey(numFrequency)){ List<Integer> existingIndexNum = mapOfBucketWithValues.get(numFrequency); existingIndexNum.add(num); mapOfBucketWithValues.put(numFrequency, existingIndexNum); } else { List<Integer> numList = new ArrayList<>(); numList.add(num); mapOfBucketWithValues.put(numFrequency, numList); } } for (Integer k=nums.size(), returnedNums=0; 1<=k; k--){ if (mapOfBucketWithValues.containsKey(k)){ for (Integer numBucket : mapOfBucketWithValues.get(k)){ if (returnedNums < returnNums){ System.debug(numBucket); returnedNums++; } } } }
Apex Clear all fields for a SObject record
Posted on May 30, 2019 Leave a Comment
The clearOutRecords would iterate all the fields passed as the currentRecord, then:
1. Exclude the fields as part of the fieldsToExcludeForClearOut Set and relationship fields
2. Check if the field is not null and updateable
3. Special logic to set fields to predefined values
4. Set all other fields to null
5. Return the SObject with fields as null
private static Set<String> fieldsToExcludeForClearOut = new Set<String>{'Cases', 'DoNotCall', 'HasOptedOutOfFax', 'HasOptedOutOfEmail', 'LastName', 'FirstName', 'Email', 'AccountId', 'CreatedDate', 'IsDeleted','Interval__c','OwnerId', 'OtherGeocodeAccuracy','MailingGeocodeAccuracy', 'BillingGeocodeAccuracy','ShippingGeocodeAccuracy'}; public SObject clearOutRecords(SObject currentRecord, String sObjectName){ SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName); DescribeSObjectResult objDef = objToken.getDescribe(); Map<String, SObjectField> fieldsSobject = objDef.fields.getMap(); Map<String, Object> fields = currentRecord.getPopulatedFieldsAsMap(); Type classType = Type.forName(sObjectName); SObject mergedRecord = (SObject)JSON.deserialize('{}', classType); for (String field : fields.keySet()){ if (!fieldsToExcludeForClearOut.contains(field) && !field.contains('__r')){ if (currentRecord.get(field)!=null && fieldsSobject.get(field).getDescribe().isUpdateable()){ if ('User_Status__c'.equals(field)){ mergedRecord.put(field, 'Incomplete'); } else if ('Is_Mail_Same_As_Home__c'.equals(field)){ mergedRecord.put(field, false); } else { mergedRecord.put(field, null); } } else if ('Id'.equals(field)){ mergedRecord.put(field, currentRecord.get(field)); } } } return mergedRecord; }
Initializing the clearOutRecords method
1. Query the fields that you would like to clear
2. Pass the Object to the clearOutRecords method
Contact queryContact = [Select Id, FirstName, LastName, Email, Birthdate, MailingState, Age__c from Contact where MailingState!=null limit 1 ]; Contact clearedOutContact = (Contact)App_Service.instance.clearOutRecords(queryContact, 'Contact');
Output
Contact:{Id=0036300000TQZIwAAP, Birthdate=null, MailingState=null}
Salesforce Platform Events Streaming using Spring Boot
Posted on May 29, 2019 2 Comments
Create EmpConnector connection
The first thing is to create a configuration that on startup will connect to the EmpConnector and start listing for incoming events on a specific topic.
1. Set all your Bayeux Parameters (bayeuxParameters)
2. Create EmpConnector connection (empConnector)
3. Start listening to topic (startAndPublishAsyncEventToExchange)
4. Adding listeners to the topic (startAndPublishAsyncEventToExchange)
package com.app.core.events; import com.app.core.service.IncomingRequestHandler; import com.app.utils.AppPlanUtils; import com.salesforce.emp.connector.BayeuxParameters; import com.salesforce.emp.connector.EmpConnector; import com.salesforce.emp.connector.TopicSubscription; import com.salesforce.emp.connector.example.LoggingListener; import org.cometd.bayeux.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import java.net.URL; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static com.salesforce.emp.connector.LoginHelper.login; @Configuration @ConfigurationProperties @Lazy public class SalesforceEventConfig { private static Logger itsLogger = LoggerFactory.getLogger(SalesforceEventConfig.class); @Value("${salesforce.username}") private String username; @Value("${salesforce.password}") private String password; @Value("${salesforce.token}") private String token; @Value("${salesforce.baseurl}") private String url; @Value("${salesforce.version:39.0}") private String version; @Autowired protected IncomingRequestHandler itsReqHandler; private static final Integer WAIT_TIME = 2; private static final Integer START_UP_WAIT_TIME = 5; private static final String EVENT_NAME = "App_Events__e"; @Bean public BayeuxParameters bayeuxParameters() throws Exception { String theUrlEnv = AppUtils.getConfigVar("SALESFORCE_URL"); String passwordAndToken = password + token; if (theUrlEnv != null) { url = getNextToken(0, theUrlEnv); username = getNextToken(url.length() + version.length() + 2, theUrlEnv); passwordAndToken = theUrlEnv.substring(url.length() + version.length() + username.length() + 3); itsLogger.info("Found SALESFORCE_URL to parse. url={}, version={}, username={}", url, version, username); url = url.split("/services/Soap/u/")[0]; } BayeuxParameters bayeuxParams = getBayeuxParamWithSpecifiedAPIVersion(version); BayeuxParameters bayeuxParameters = login(new URL(url),username, passwordAndToken, bayeuxParams); return bayeuxParameters; } @Bean public EmpConnector empConnector(BayeuxParameters bayeuxParameters){ itsLogger.debug("BayeuxParameters url {} version {}", bayeuxParameters.host(), bayeuxParameters.version()); EmpConnector empConnector = new EmpConnector(bayeuxParameters); itsLogger.debug("EmpConnector isConnected {} isHandshook {}", empConnector.isConnected(), empConnector.isHandshook()); return empConnector; } private static BayeuxParameters getBayeuxParamWithSpecifiedAPIVersion(String apiVersion) { BayeuxParameters params = new BayeuxParameters() { @Override public String version() { return apiVersion; } @Override public String bearerToken() { return null; } }; return params; } private String getNextToken(int startIdx, String origStr) { int theIdx = origStr.indexOf("|", startIdx); if (theIdx > 0) { return origStr.substring(startIdx, theIdx); } else { return origStr.substring(startIdx); } } @Bean public TopicSubscription startAndPublishAsyncEventToExchange(EmpConnector empConnector) { TopicSubscription subscription = null; try { long replayFrom = EmpConnector.REPLAY_FROM_TIP; itsLogger.debug("Setup event consumer with replyFrom {}", replayFrom); SalesforceEventPayload eventPayload = new SalesforceEventPayload(); Consumer<Map<String, Object>> consumer = event -> { eventPayload.setPayload(event); itsReqHandler.handleRequest(eventPayload.getPayload()); }; LoggingListener loggingListener = new LoggingListener(true, true); itsLogger.debug("Adding event listeners"); empConnector.addListener(Channel.META_CONNECT, loggingListener) .addListener(Channel.META_HANDSHAKE, loggingListener) .addListener(Channel.META_DISCONNECT, loggingListener) .addListener(Channel.META_SUBSCRIBE, loggingListener) .addListener(Channel.META_UNSUBSCRIBE, loggingListener) .addListener(Channel.META_DISCONNECT, loggingListener) .addListener(Channel.SERVICE, loggingListener); itsLogger.debug("Starting Event Bus"); empConnector.start().get(START_UP_WAIT_TIME, TimeUnit.SECONDS); itsLogger.debug("Subscribing to event {}", EVENT_NAME); subscription = empConnector.subscribe("/event/" + EVENT_NAME, replayFrom, consumer).get(WAIT_TIME, TimeUnit.SECONDS); itsLogger.debug(String.format("Subscribed: %s", subscription)); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } return subscription; } }
Basic POJO to serialize events received for a specific topic
Note: as the ‘payload’ tag has some extra tags I remove them before serializing the payload to POJO.
package com.app.core.events; import com.app.db.model.UserServiceRequestEvent; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Serializable; import java.util.Map; public class SalesforceEventPayload implements Serializable{ private static final String SALESFORCE_EVENT_KEY = "Event_Data__c="; private static Logger itsLogger = LoggerFactory.getLogger(SalesforceEventPayload.class); public SalesforceEventPayload() {} private UserServiceRequestEvent payload; public UserServiceRequestEvent getPayload() { return payload; } public void setPayload(Map<String, Object> fieldMappings) { ObjectMapper theMapper = new ObjectMapper(); theMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); theMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false); theMapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true); theMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); theMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true); if (fieldMappings.containsKey("payload")){ String eventPayload = fieldMappings.get("payload").toString(); String splitEventString = eventPayload.toString().split(SALESFORCE_EVENT_KEY)[1]; String removeLastTwo = splitEventString.substring(1, splitEventString.length() - 2); String unescapeJson = StringEscapeUtils.unescapeJson(removeLastTwo); try{ payload = theMapper.readValue(unescapeJson, UserServiceRequestEvent.class); } catch (JsonParseException e) { itsLogger.error("Event json parse exception {}", e.getMessage()); } catch (JsonMappingException e) { itsLogger.error("Event json mapping exception {}", e.getMessage()); } catch (IOException e) { itsLogger.error("Event io exception {}", e.getMessage()); } } } }
Apex remove sensitive data from json
Posted on May 23, 2019 Leave a Comment
When you need to remove sensitive data from json before logging the following method will remove a predefined list of keywords
String bodyArgs = '{"name":"test", "ssn":"324234234", "email":"test@mail.com"}'; Object bodyObj = (Object)JSON.deserializeUntyped(bodyArgs); Map<String, Object> mapObj = new Map<String, Object>(); if (bodyObj instanceof List<Object>){ List<Object> lstObjs = (List<Object>)JSON.deserializeUntyped(bodyArgs); for (Object lstObj : lstObjs){ Map<String,Object> parseLstObj = (Map<String,Object>)JSON.deserializeUntyped(JSON.serialize(lstObj)); mapObj.putAll(parseLstObj); } } else { mapObj = (Map<String,Object>)JSON.deserializeUntyped(bodyArgs); } Map<String, String> newMappedValues = new Map<String, String> System.debug(removeAttributes(mapObj, newMappedValues)); >>> Output: '{"name":"test"}'
removeAttributes method will iterate all the keys in the payload and remove the sensitive keys from the payload
private Set<String> removeSensitiveKeyValue = new Set<String>{'ssn', 'email', 'dob'}; public Map<String, String> removeAttributes(Map<String,Object> jsonObj, Map<String, String> mappedKeys) { for(String key : jsonObj.keySet()) { if (removeSensitiveKeyValue.contains(key)){ jsonObj.remove(key); } else { if(jsonObj.get(key) instanceof Map<String,Object>) { removeAttributes((Map<String,Object>)jsonObj.get(key), mappedKeys); } else if(jsonObj.get(key) instanceof List<Object>) { for(Object listItem : (List<Object>)jsonObj.get(key)) { if(listItem instanceof Map<String,Object>) { removeAttributes((Map<String,Object>)listItem, mappedKeys); } } } else { mappedKeys.put(key, String.valueOf(jsonObj.get(key))); } } } return mappedKeys; }
Apex Coding Challenge for Iterating Lists
Posted on May 20, 2019 Leave a Comment
Here is the problem statement:
Given an expectedSum value find the sum (n+ (n+1) = expectedValue) from a list that equals the expectedSum. I am using the following list
1,3,4,4,5,9
and looking for the sum to be 8 (3+5, 4+4)
*Note the list is sorted
Solution 1:
Integer expectedSum = 8; List<Integer> listInts = Arrays.asList(1,3,4,4,5,9); for (Integer k=0; k<listInts.size();k++){ for (Integer j=k+1; j<listInts.size();j++){ if (listInts.get(k)+listInts.get(j)==expectedSum){ System.debug(listInts.get(k) + ' + ' + listInts.get(j)); } } }
The above time complexity for a nested for loop is O(n^2)
Solution 2:
Do a binary search to search if the diff is contained in the list
Integer expectedSum = 8; List<Integer> listInts = Arrays.asList(1,3,4,4,5,9); for (Integer listInt : listInts){ Integer diffInt = expectedSum-listInt; if (binarySearch(listInts, diffInt)!=null){ System.debug(listInt + " + " + diffInt); } } public static Integer binarySearch(List<Integer> listInts, Integer searchInt){ Integer startPos = 0; Integer listSize = listInts.size() - 1; Integer mid; while(startPos <= listSize){ mid=(startPos+listSize)/2; if(listInts.get(mid) == searchInt){ return listInts.get(mid); } else if(searchInt < listInts.get(mid)){ listSize = mid - 1; } else{ startPos = mid + 1; } } return null; }
the above time complexity for binary search is for each element in the array O(n log n) Returning null is not a good practice try to return another number or throw an exception.
Solution 3:
Check if the diff is contained in the list by using the .contains method
Integer expectedSum = 8; List<Integer> listInts = Arrays.asList(1,3,4,4,5,9); for (Integer listInt : listInts){ Integer diffInt =expectedSum-listInt; if (listInts.contains(diffInt)){ System.debug(listInt + ' + ' + diffInt); } }
the above time complexity for loop is O(n)
Option 4
Start on either end of the array and move inwards when you find a solution, if the sum of the outer and inner element is bigger that the expectedSum move the maxPointer inwards, if it is smaller move the minPointer inwards.
Integer expectedSum = 1; List<Integer> listInts = Arrays.asList(1,3,4,4,5,9); Integer maxPointer = listInts.size()-1; Integer minPointer = 0; for (Integer k=0; k<listInts.size();k++){ if (expectedSum < listInts.get(maxPointer)){ maxPointer-=1; } else if (minPointer!=maxPointer){ Integer sumPair =listInts.get(minPointer) + listInts.get(maxPointer); if ( sumPair == expectedSum){ System.debug(listInts.get(minPointer) + " + " + listInts.get(maxPointer)); minPointer +=1; maxPointer-=1; } else if (sumPair < expectedSum){ minPointer +=1; } else { maxPointer-=1;; } } }
the above time complexity for loop is O(n)
Output:
4 + 4 5 + 3 3 + 5
Bonus:
How to do it with an unsorted list:
Iterate through the list and add the integer to the list, if the diff of the expected sum and integer is contained in the set then print it
Integer expectedSum = 8; List<Integer> listInts = Arrays.asList(7,4,6,1,5,2,3); Set<Integer> intSetWithDiff = new HashSet<>(); for (Integer listInt : listInts){ Integer sumDiff = expectedSum - listInt; if (intSetWithDiff.contains(sumDiff)){ System.debug(listInt + " + " + sumDiff); } intSetWithDiff.add(listInt); }
Output:
1 + 7 2 + 6 3 + 5
Salesforce Object Level and Field Level Security Architecture
Posted on May 17, 2019 Leave a Comment
The Salesforce Security review require that both:
- Object Level (OLS)
- Field Level (FLS)
Security is applied for the following areas:
- Query of data – Selectors
- Triggers – Domains
- DML – Unit of Work
To check both (OLS) and FLS we can use fflib_SecurityUtils it has all the to methods to check if a user can access an object and also individual fields for that object. An Apex exception is thrown if the user does not have access to the given object and/or fields.
Example is when a community user tries to create an Account the following error occurs if OLS is enabled
fflib_SecurityUtils.CrudException: You do not have permission to insert Account Class.fflib_SecurityUtils.checkObjectIsInsertable: line 309, column 1 Class.fflib_SObjectUnitOfWork.insertDmlByType: line 583, column 1 Class.fflib_SObjectUnitOfWork.doCommitWork: line 531, column 1 Class.fflib_SObjectUnitOfWork.commitWork: line 509, column 1 Class.App_Service_Test.testSecurityUOW: line 81, column 1 |
- Query of data – Selectors
The key purpose of this class is to make building dynamic SOQL queries safer and more robust than traditional string concatenation or String.format approaches. It also has an option to automatically check read security for the objects and fields given to it. Here are some of the methods we use to dynamically build all our queries.
Enable FLS check for queries before they run, by setting setEnforceFLS(true)
OR set the OLS and FLS when constructing a new Selector so it will be implement for all queries
OpportunitiesSelector oppsSelector =Â new OpportunitiesSelector(includeFieldSetFields, enforceObjectSecurity, enforceFLS);
2. Triggers – Domains
Domain classes has minimal functionality in it other than the routing of trigger events to the applicable virtual methods and object security enforcement.
Domain classes by OLS by default as part of it’s Configuration class:
3. DML – Unit of Work
This is not currently implemented to check the following:
1. Insert
if (enforceOLS){ fflib_SecurityUtils.checkObjectIsInsertable(sObjectType); }
2. Update
if (enforceOLS){ fflib_SecurityUtils.checkObjectIsUpdateable(sObjectType); }
3. DeleteÂ
if (enforceOLS){ fflib_SecurityUtils.checkObjectIsDeletable(m_sObjectTypes[objectIdx--]); }
Disadvantages of OLS and FLS implemented across your app
OLS – this is a check that is done to see if the user profile has access to do CRUD on the specific object. If this check is not done and user tries to do any of the CRUD operations it will just result in a Salesforce error. Doing this check on an object level is not too big of an overhead. If these checks are not done, Permission errors will be thrown as the profile does not have the correct permission.
FLS – this is not recommended as we have to iterate through every field of the object and check if the user has the privileges to read/update/insert/delete to that field. This slows down operations down drastically and only needs to be used in a few uses cases.
Possible solutions run security checks on app
Have a way to enable both OLS and FLS in your code then run through all your test and see if any break. If the break GREAT fix your Object Settings/FLS so that they work. When running in Production have a way to disable this as it may/will slow down your operations/services.
Apex Interview Question > FizzBuzz
Posted on May 16, 2019 Leave a Comment
Consider the following problem:
Write a short program that prints each number from 1 to 100 in one line
For each multiple of 3, print “Fizz” instead of the number.
For each multiple of 5, print “Buzz” instead of the number.
For numbers which are multiples of both 3 and 5, print “FizzBuzz” instead of the number.
Write a solution (or reduce an existing one) so it has as few characters as possible.
Solution 1: For loop with multiple if statements
String str = ''; for(Integer i = 1; i&lt;=100; i++){ if(Math.mod(i,3)==0) str+='Fizz'; if(Math.mod(i,5)==0) str+='Buzz'; else if(Math.mod(i, 3) != 0) str+=i; str +=','; } System.debug(str);
Output
1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,
Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,31,32,Fizz,34,Buzz,
Fizz,37,38,Fizz,Buzz,41,Fizz,43,44,FizzBuzz,46,47,Fizz,49,Buzz,
Fizz,52,53,Fizz,Buzz,56,Fizz,58,59,FizzBuzz,61,62,Fizz,64,Buzz,
Fizz,67,68,Fizz,Buzz,71,Fizz,73,74,FizzBuzz,76,77,Fizz,79,Buzz,
Fizz,82,83,Fizz,Buzz,86,Fizz,88,89,FizzBuzz,91,92,Fizz,94,Buzz,
Fizz,97,98,Fizz,Buzz
Apex FeedItem Trigger Share to Community
Posted on April 6, 2019 Leave a Comment
When uploading a new FeedItem we want to share it to a specific community. First thing we need to do is share the FeedItem to a community by sharing it to the User. As the documentation states: Only feed items with a Group or User parent can set a NetworkId or a null value for NetworkScope.
NetworkId—The ID of the community in which the FeedItem is available. If left empty, the feed item is only available in the default community.
FeedItemTrigger Trigger
trigger FeedItemTrigger on FeedItem (after insert, before insert) { fflib_SObjectDomain.triggerHandler(App_Domain_FeedItem.class); }
FeedItemTrigger Domain Class
public class App_Domain_FeedItem extends fflib_SObjectDomain { public override void onBeforeInsert(){ List<ContentDocumentLink> contentDocumentLinksList = new List<ContentDocumentLink>(); for(FeedItem record : (List<FeedItem>) Records){ record.Body = record.ParentId; record.NetworkScope = {NetworkId}; record.ParentId = UserInfo.getUserId(); record.Visibility='AllUsers'; } } }
Now that the file is shared with the user we can give View sharing back to the original object using ContentDocumentLink by using the RelatedRecordId
public class App_Domain_FeedItem extends fflib_SObjectDomain { public override void onAfterInsert(){ List<ContentDocumentLink> contentDocumentLinksList = new List<ContentDocumentLink>(); List<Documents__c> bpDocumentsList = new List<Documents__c>(); Map<Id, Id> feedItemToRelatedRecordIdMap = new Map<Id, Id>(); for(FeedItem record : (List<FeedItem>) Records){ feedItemToRelatedRecordIdMap.put(record.Id, record.RelatedRecordId); } Map<Id, Id> mapOfRelatedRecordContentVersionMap = new Map<Id, Id>(); List<ContentVersion> contentDocumentVersions = [Select Id, ContentDocumentId from ContentVersion where id IN :feedItemToRelatedRecordIdMap.values()]; for (ContentVersion contentDocumentVersion : contentDocumentVersions){ mapOfRelatedRecordContentVersionMap.put(contentDocumentVersion.Id, contentDocumentVersion.ContentDocumentId); } for(FeedItem record : (List<FeedItem>) Records){ String bpDocumentId = record.Body; Id relatedRecordId = feedItemToRelatedRecordIdMap.get(record.Id); Id contentDocumentId = null; if (mapOfRelatedRecordContentVersionMap.containsKey(relatedRecordId)){ contentDocumentId = mapOfRelatedRecordContentVersionMap.get(relatedRecordId); } if (contentDocumentId!=null){ ContentDocumentLink cdl = new ContentDocumentLink(LinkedEntityId = bpDocumentId , ContentDocumentId=contentDocumentId , shareType = 'V'); contentDocumentLinksList.add(cdl); } Documents__c updateDocument = new Documents__c(Id=bpDocumentId); updateDocument.Document_Status__c='Downloaded'; String formatDownloadUrl = '/CommunityApi/sfc/servlet.shepherd/version/download/{0}?asPdf=false&operationContext=CHATTER'; updateDocument.Download_Url__c = String.format(formatDownloadUrl, new List<String>{contentDocumentId}); updateDocument.File_Id__c = contentDocumentId; updateDocument.File_Name__c = record.Title; updateDocument.File_Size__c = record.ContentSize; updateDocument.File_Type__c = record.ContentType!=null ? mimeTypeMap.containsKey(record.ContentType.toLowerCase()) ? mimeTypeMap.get(record.ContentType.toLowerCase()) : record.ContentType : record.ContentType; updateDocument.File_Permission__c='R'; bpDocumentsList.add(updateDocument); } if (!contentDocumentLinksList.isEmpty()){ insert contentDocumentLinksList; } if (!bpDocumentsList.isEmpty()){ update bpDocumentsList; } } }
Now the Feeditem is shared both to the user and the object. It can be downloaded by the logged in user.