Apex Logging Using Static Resources

Define the Error messages detail in json format as static resource

[{
	"id": "REQUESTED_OPERATION_NOT_PERMITTED",
	"code": 1,
	"clientTemplate": "the current rest operation is not permitted for this endpoint",
	"systemTemplate": "the current rest operation is not permitted for this endpoint"
}, {
	"id": "REQUESTED_ID_MISSING_OR_INVALID",
	"code": 2,
	"clientTemplate": "id is missing or invalid for the specific request",
	"systemTemplate": "id is missing or invalid for the specific request Id {0}"
}, {
	"id": "URL_INVALID",
	"code": 3,
	"clientTemplate": "url invalid and does not exist",
	"systemTemplate": "url invalid and does not exist UserId: {0} | Type: {1} | Path: {2} | Body: {3}"
}, {
	"id": "INPUT_JSON_INVALID",
	"code": 4,
	"clientTemplate": "the input json provided is invalid",
	"systemTemplate": "the input json provided is invalid"
}]

Define a ResponseCode class to parse the json into a List of ResponseCodes

global with sharing class ResponseCode {

  public String id { get; set; }
  public Integer code { get; set; }
  public String clientTemplate { private get; set { clientTemplate = value; } }
  public String systemTemplate { private get; set { systemTemplate = value; } }

  public String formattedClientMessage { get; set; }
  public String formattedSystemMessage { get; set; }

  global void formatMessages(List<String> args, Exception ex) {
    if (args != null && args.size() != 0) {
      formattedClientMessage = String.format(clientTemplate, args);
      if (systemTemplate != null) {
        formattedSystemMessage = String.format(systemTemplate, args);
      }
    } else {
      formattedClientMessage = clientTemplate;
      formattedSystemMessage = systemTemplate;
    }
    if (ex != null) {
      if (formattedSystemMessage == null) {
        formattedSystemMessage = formattedClientMessage;
      }
      formattedSystemMessage += ' -> Cause by: ' + ex.getTypeName() + ' - ' + ex.getMessage() + '. Cause trace: ' + ex.getStackTraceString();
    }
  }

  global ResponseCode copy() {
    ResponseCode theNewObj = new ResponseCode();
    theNewObj.id = this.id;
    theNewObj.code = this.code;
    theNewObj.clientTemplate = this.clientTemplate;
    theNewObj.systemTemplate = this.systemTemplate;
    return theNewObj;
  }

	global ResponseCode(){
	}

  global override String toString() {
    return 'ResponseCode(id=' + id + ';code=' + code + ';clientTemplate=' + clientTemplate + ';systemTemplate=' + systemTemplate + ';formattedClientMessage=' + formattedClientMessage + ';formattedSystemMessage=' + formattedSystemMessage + ')';
  }
}

Define a ResponseCode Manager to translate the Codes into a map to get by id

public with sharing class ResponseCodes_Mgr{

	private static Map<String, ResponseCode> itsResponseCodes = new Map<String, ResponseCode>();

	static {
    try {
      StaticResource theCodesSrc = [SELECT Id, Body FROM StaticResource WHERE Name = 'ResponseCode' LIMIT 1];
      if (theCodesSrc != null) {
    		String theCodesJSON = theCodesSrc.Body.toString();
        List<ResponseCode> theCodesList = (List<ResponseCode>) JSON.deserialize(theCodesJSON, List<ResponseCode>.class);
        for (ResponseCode theCode: theCodesList) {
          itsResponseCodes.put(theCode.id, theCode);
        }
        System.debug('Loaded ' + itsResponseCodes.size() + ' response codes into static map');
      } else {
        System.debug(LoggingLevel.ERROR, 'Cannot query ResponseCode static resource from DB');
      }
    } catch (Exception ex) {
      System.debug(LoggingLevel.ERROR, 'ERROR loading response codes: ' + ex.getMessage());
    }

	}

  public static ResponseCode getCode(String anID) {
    return getCode(anID, null, null);
  }

  public static ResponseCode getCode(String anID, Exception anExp) {
    return getCode(anID, anExp, null);
  }

  public static ResponseCode getCode(String anID, List<String> args) {
    return getCode(anID, null, args);
  }

  public static ResponseCode getCode(String anID, Exception anExp, List<String> args) {
    // make a copy so that the caller can do whatever with the object
    // multiple callers might use same ID with different args list
    ResponseCode theCode = itsResponseCodes.get(anID);
    if (theCode == null) {
      theCode = itsResponseCodes.get('UNEXPECTED_RESPONSE_CODE').copy();
      List<String> theArgs = new List<String>();
      theArgs.add(anID);
      if (args != null) {
        theArgs.add(args + '');
      }
      theCode.formatMessages(theArgs, anExp);
      System.debug(LoggingLevel.ERROR, 'Unknown response code ' + anID + '. Returning UNEXPECTED_RESPONSE_CODE and ignoring args list ' + args);
    } else {
      theCode = theCode.copy();
      theCode.formatMessages(args, anExp);
    }
    return theCode;
  }
}

Catch exceptions and log exceptions to logging provide

}catch (JSONException jsonEx){
  responseCode = ResponseCodes_Mgr.getCode('JSON_SERIALIZATION_FAILED', jsonEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
} catch (SearchException searchEx){
  responseCode = ResponseCodes_Mgr.getCode('SEARCH_FAILED', searchEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
} catch (CalloutException calloutEx){
  responseCode = ResponseCodes_Mgr.getCode('CALLOUT_FAILED', calloutEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);  
} catch (DmlException dmlEx){
  responseCode = ResponseCodes_Mgr.getCode('DATABASE_OPERATION_FAILED', dmlEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
} catch (SObjectException sobjectEx){
  responseCode = ResponseCodes_Mgr.getCode('SOBJECT_OPERATION_FAILED', sobjectEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
  String serializeJson = Rest_Global_Json.instance.serialize(json);
  res.responseBody = Blob.valueof(serializeJson);
} catch (QueryException queryEx){
  responseCode = ResponseCodes_Mgr.getCode('QUERY_OPERATION_FAILED', queryEx, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), requestHeaders.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
} catch (Exception ex){
  responseCode = ResponseCodes_Mgr.getCode('REST_DISPATCHER_FAILED', ex, new List<String>{UserInfo.getUserId(), requestHeaders.get(TRACE_ID_KEY), request.headers.get('timestamp'), requestType.name(), request.requestURI, SHOW_BODY && request.requestBody!= null ? request.requestBody.toString() : ''});
  json.setResponseCode(responseCode);
}

Apex HttpCalloutMock DML workaround

When doing a callout it has to happen before the DML action. If it happens after the DML it will throw an error or will not execute. Workaround I used is to manually keep track of the mock response, and call the respond method to intercept Http.send when running a test. By checking for Test.isRunningTest() && (mock!=null) we can send the mock response.

Checking for test running and returning mock response

	public static HttpCalloutMock mock = null;
	public HttpResponse callApiEndpoint(String apiEndpoint, String method, Object aPayload) {
		HttpRequest req = new HttpRequest();
    try {
      if (apiEndpoint != null) {
        req.setTimeout(120000);
        req.setMethod(method);
        setAuthHeader(req);
        req.setEndpoint(WebCustomSettings.Heroku_API_URL__c + apiEndpoint);
				if (aPayload!=null)
        	req.setBody(String.valueOf(aPayload));
        System.debug('Sending api request to endpoint' + apiEndpoint);
				if (Test.isRunningTest() && (mock!=null)) {
					 return mock.respond(req);
			 	} else {
					Http http = new Http();
	        return http.send(req);
				}

      } else {
        System.debug('Service apiEndpoint and payload must not be null');
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED'));
      }
    } catch (Exception ex) {
      System.debug('ERROR sending sync request to Heroku: ' + ex);
      List<String> theArgs = new List<String>();
      theArgs.add('Api');
      theArgs.add(req.getEndpoint());
      throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', ex, theArgs));
    }
  }

Test class

  Test.startTest();
        System.runAs(new User(Id=userWithAccountContact.get('UserId'))){
          //Setting the mock variable and it's also a test so will return the mock response
          Heroku_Services_Api.mock = new Heroku_Services_Api_Mock(200, 'Complete', '{}',null);
          req.requestBody = Blob.valueOf('{"id":"'+ account.Id +'", "clientId":"' + userWithAccountContact.get('ContactId') + '", "amount":122233,"endDate":"2022-01-01"}');
          req.requestURI = '/v1/account';
          req.httpMethod = 'PATCH';
          RestContext.request = req;
          RestContext.response = res;

          try{
            Rest_Dispatcher.doPatch();
          } catch(Rest_Exception ex){
            System.assertEquals(ex.itsRespCode.formattedSystemMessage, 'blah');
          }

          System.assert(res.responseBody!=null);
          System.assertEquals(res.statusCode, 200);
      }
      Test.stopTest();

Apex Dynamic Object Mapping for REST endpoints

Writing rest endpoints in Apex can be hard and exposing Salesforce fields to external application may not make allot of sense as custom fields end with __c and some fields are not writable or systems fields. I have worked on creating a Serializer that could serialize a Salesforce object mapping and translate that back to a Salesforce SObject.

Let say you can provide an endpoint to an app that could return the metadata of that object as well as the list of fields it expects. The mapping shows a map of keys and values. The keys are the Salesforce SObject fields and the values are the fields exposed to external application.

{
  "serializeNulls": true,
  "mapKeysFlag": true,
  "mapKeys": {
    "responseCode": "responseCode",
    "result": "result",
    "userId": "userId",
    "size": "size",
    "status": "status"
  },
  "userId": "00536000000erTFAAY",
  "result": {
    "external_id__c": "externalid",
    "sicdesc": "sicdesc",
    "accountsource": "accountsource",
    "site": "site",
    "rating": "rating",
    "description": "description",
    "tickersymbol": "tickersymbol",
    "ownership": "ownership",
    "numberofemployees": "numberofemployees",
    "billingcountry": "billingcountry",
    "billingpostalcode": "billingpostalcode",
    "billingstate": "billingstate",
    "billingcity": "billingcity",
    "billingstreet": "billingstreet",
    "parentid": "parentid",
    "type": "type",
    "name": "name",
    "id": "id"
  }
}

An application can send us data in the following way and we can serialize it to the Account sObject without doing any mapping.

{"externalid":"2333", "photourl": "http://blah.com", "website": "http://web.com", "accountnumber": "3243232", "fax": "2343432", "phone": "32222"}

Define a serializer to swap the key from application with the fields of the Salesforce object

public abstract class Rest_Serializer {

		public Rest_Serializer(){}

		private Map<String,String> mapKeys;
		private boolean serializeNulls = true;
		private boolean mapKeysFlag = true;

		public Rest_Serializer(Map<String,String> mapping){
			this.mapKeys = mapping;
		}

		public void setSerializeNulls(Boolean serializeNulls){
			this.serializeNulls = serializeNulls;
		}

		public void setMapKeysFlag(Boolean mapKeysFlag){
			this.mapKeysFlag = mapKeysFlag;
		}

		public String serialize(Object obj){
			String retString = JSON.serialize(obj);
			if (retString.length() < 50000){
				retString = transformStringForSerilization(retString);
				if(serializeNulls){
					retString = removeNullsFromJSON(retString);
				}
			} else {
		//throw exception
			}
			return retString;
		}

		public Object deserialize(String jsonString, System.Type type){
			jsonString = transformStringForDeserilization(jsonString);
			Object obj = JSON.deserialize(jsonString, type);
			return obj;
		}

		private String transformStringForSerilization(String s){
			if (mapKeys!=null && mapKeysFlag){
				return replaceAll(s, mapKeys);
			}
			return s;
		}

		private String transformStringForDeserilization(String s){
			Map<String,String> flippedMap = new Map<String,String>();
			if (mapKeys!=null && mapKeysFlag){
				for(String key : mapKeys.keySet()){
					flippedMap.put(mapKeys.get(key), key);
				}
			}
			return replaceAll(s, flippedMap);
		}

		private String removeNullsFromJSON(String s){
				return s.replaceAll('("[\\w]*":null,)|(,?"[\\w]*":null)','');
		}

		private String replaceAll(String s, Map<String,String> toFromMap){
			if (mapKeys!=null && mapKeysFlag){
				for(String key : toFromMap.keySet()){
						s = s.replaceAll('"'+key+'":', '"'+toFromMap.get(key)+'":');
				}
			}
			return s;
		}

		public Boolean getSerializeNulls(){
			return serializeNulls;
		}
}

Extend the Rest Serializer to create a DynamicModel that will take all fields and serialize them to an SObject

public class DynamicModel extends Rest_Serializer {
    public DynamicModel(Map<String,String> fieldMapping){
       super(fieldMapping);
    }
}

Get model name and return SObject Fieldsfields

	public static Map<String,String> getModelByObject(String sObjectName){
		Map<String,String> replaceFieldNames = new Map<String,String>();
		SObjectType accountType = Schema.getGlobalDescribe().get(sObjectName);
		Map<String,Schema.SObjectField> mfields = accountType.getDescribe().fields.getMap();
		for (String mfield : mfields.keySet()){
			//String uField = mfield.replace('[__c,_]','');
			replaceFieldNames.put(mfield, Rest_Util.toCamelCase(mfield));
		}
		return replaceFieldNames;
	}

	public static Map<String, Schema.DescribeFieldResult> getFieldMetaData(
	  Schema.DescribeSObjectResult dsor, Set<String> fields) {
		Map<String,Schema.DescribeFieldResult> finalMap = new Map<String, Schema.DescribeFieldResult>();

	  Map<String, Schema.SObjectField> objectFields = dsor.fields.getMap();

	  for(String field : fields){
	    if (objectFields.containsKey(field)) {
	      Schema.DescribeFieldResult dr = objectFields.get(field).getDescribe();
	      finalMap.put(field, dr);
	    }
	  }
	  return finalMap;
	}

Get SObject fields for an object and translate application fields to SObject fields and return object

    global with sharing class GetModel implements Rest_Dispatcher.Dispatchable {
			private Rest_Global_Request_Json requestBody;

			global String getURIMapping(){
					return Rest_Constants.URI_SETTINGS + '/model/{objectName}';
			}

			global void setRequestBody(Rest_Global_Request_Json requestBody){
					this.requestBody = requestBody;
			}

			global Rest_Global_Json execute(Map<String, String> parameters){
					Rest_Global_Json globalJson = new Rest_Global_Json();
					String sObjectName = parameters.get('objectName');
					Map<String,String> uiFieldNamesForObject = getModelByObject(sObjectName);
					BP_JSON.DynamicModel dynamicModel = new BP_JSON.DynamicModel(uiFieldNamesForObject);
					Schema.SObjectType convertType = Schema.getGlobalDescribe().get(sObjectName);
					Type classType = Type.forName(sObjectName);
					Object transformedObject = dynamicModel.deserialize(requestBody.getRequest(), classType);
					globalJson.setResult(transformedObject);
					return globalJson;
			}

			global override String toString(){
				Rest_Endpoint.Path path = new Rest_Endpoint.Path();
				path.setPath(getURIMapping());
				Rest_Endpoint endpoint = new Rest_Endpoint(path);
				return JSON.serialize(endpoint);
			}
	}
}

Apex callout to RabbitMQ Queue

Create AMQP HTTPRequest calling POST /api/exchanges/{username}/{exchangeName}/publish

	public void callAmqpEndpoint(String exchangeName, String method, String aPayload) {
  	HttpRequest req = new HttpRequest();
    try {
      if (exchangeName != null) {
        req.setTimeout(120000);
        req.setMethod(method);
				setAmqpAuthHeader(req);
				System.debug(String.valueOf(aPayload));
        req.setEndpoint(WebCustomSettings.AMQP_Url__c + '/api/exchanges/' + WebCustomSettings.AMQP_Credentials__c.split(':')[0] + '/' + exchangeName + '/publish');
				if (aPayload!=null)
        	req.setBody(aPayload);
        System.debug('Sending api request to endpoint' + req.getEndpoint());
        Http http = new Http();
        http.send(req);
      } else {
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('AMQP_REQUEST_FAILED'));
      }
    } catch (Exception ex) {
      System.debug('Error sending amqp request ' + ex);
      List<String> theArgs = new List<String>();
      theArgs.add('AMQP');
      theArgs.add(req.getEndpoint());
      throw new Rest_Exception(ResponseCodes_Mgr.getCode('AMQP_REQUEST_FAILED', ex, theArgs));
    }
  }

Setup AMQP headers

private void setAmqpAuthHeader(HttpRequest aReq) {
    Blob headerValue = Blob.valueOf(WebCustomSettings.AMQP_Credentials__c);
    String authorizationHeader = 'Basic ' + EncodingUtil.base64Encode(headerValue);
    aReq.setHeader('Authorization', authorizationHeader);
		aReq.setHeader('Content-Type', 'application/json');
		aReq.setHeader('X-AMQP-Tracer', requestJson!=null && requestJson.getTraceId()!=null ? requestJson.getTraceId() : '');
  }

Serialize AMQP Request JSON

	private String serializeAmqpRequests(String payload) {
		JSONGenerator generator = JSON.createGenerator(false);
		generator.writeStartObject();
		generator.writeStringField('routing_key','amqp-events');
		generator.writeFieldName('properties');
		generator.writeStartObject();
		generator.writeEndObject();
		generator.writeStringField('payload', payload);
		generator.writeStringField('payload_encoding', 'string');
		generator.writeEndObject();
		return generator.getAsString();
	}

Callout RabbitMQ

public void sendAmqpRequest(String payload){
		 String amqpPayload = serializeAmqpRequests(payload);
		 callAmqpEndpoint('event-exchange', 'POST', amqpPayload);
	}

Apex Regex Validation Factory

Validation Factory

public with sharing class Regex_Validator {

    public static Boolean isValidEmail(String emailAddress) {
        String emailRegex = '^[a-zA-Z0-9._}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
        return String.isBlank(emailAddress) ? false : Pattern.compile(emailRegex).matcher(emailAddress).matches();
    }

    public static Boolean isValidPassword(String password){
        String passwordRegex = '^.*(?=.{8,})(?=.*\\d)(?=.*[a-zA-Z])|(?=.{8,})(?=.*\\d)(?=.*[!@#$%^&])|(?=.{8,})(?=.*[a-zA-Z])(?=.*[!@#$%^&]).*$';
        return String.isBlank(password) ? false : Pattern.compile(passwordRegex).matcher(password).matches();
    }

    public static Boolean isValidDate(String dateString){
        String dateRegex = '^\\d{4}-\\d{2}-\\d{2}$';
        return String.isBlank(dateString) ? false : Pattern.compile(dateRegex).matcher(dateString).matches();
    }

    public static Boolean isValidSSN(String ssn){
        String ssnRegex = '[0-9]{3}[-]?[0-9]{2}[-]?[0-9]{4}';
        return String.isBlank(ssn) ? false : Pattern.compile(ssnRegex).matcher(ssn).matches();
    }

    public static Boolean isValidPhone(String phone){
        String phoneRegex='^([0-9\\(\\)\\/\\+ \\-]*)$';
        return String.isBlank(phone) ? false : Pattern.compile(phoneRegex).matcher(phone).matches();
    }

    public static Boolean isValidZipCode(String zipCode){
        String zipRegex='^\\d{5}(?:[-\\s]?\\d{4})?$';
        return String.isBlank(zipCode) ? false : Pattern.compile(zipRegex).matcher(zipCode).matches();
    }

    public static Boolean isValidNumber(String num) {
         String numberRegex = '^[0-9]\\d*(\\.\\d+)?$';
     return String.isBlank(num) ? false : Pattern.compile(numberRegex).matcher(num).matches();
    }

    public static Boolean isValidSfId(String id){
       String idRegex = '[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}';
       return String.isBlank(id) ? false : Pattern.compile(idRegex).matcher(id).matches();
    }
}

Validation Factory Test Class

@isTest
private class Regex_Validator_Test {

    public static User setupUser(){
        User user = (User)SmartFactory.createSObject('User');
        return user;
    }

    @isTest static void test_isValidEmail() {
        test.startTest();
            System.assertEquals(Regex_Validator.isValidEmail('test@mail.com'),true);
            System.assertEquals(Regex_Validator.isValidEmail('tmichels+5@mygmail.com'),true);
            System.assertEquals(Regex_Validator.isValidEmail('testmail.com'),false);
            System.assertEquals(Regex_Validator.isValidEmail('tes13_?//@tmail.com'),false);
            System.assertEquals(Regex_Validator.isValidEmail('testmail'),false);
            System.assertEquals(Regex_Validator.isValidEmail('!@#$%%^&*()'),false);
            System.assertEquals(Regex_Validator.isValidEmail(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidPassword() {
        test.startTest();
            System.assertEquals(Regex_Validator.isValidPassword('Password123!'),true);
            System.assertEquals(Regex_Validator.isValidPassword('password123!'),true);
            System.assertEquals(Regex_Validator.isValidPassword('password!'),true);
            System.assertEquals(Regex_Validator.isValidPassword('Password123'),false);
            System.assertEquals(Regex_Validator.isValidPassword('blah'),false);
            System.assertEquals(Regex_Validator.isValidPassword('!@#$%%^&*()'),false);
            System.assertEquals(Regex_Validator.isValidPassword('!@#$%%^&*()'),false);
            System.assertEquals(Regex_Validator.isValidPassword(null),false);
      test.stopTest();
    }

    @isTest static void test_isValidDate() {
        test.startTest();
                System.assertEquals(Regex_Validator.isValidDate('01/01/2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('1/1/2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('01-01-2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('2016-01-01'),true);
                System.assertEquals(Regex_Validator.isValidDate('01.01.2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('1.1.2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('33/01/2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('01/32/2016'),false);
                System.assertEquals(Regex_Validator.isValidDate('01/01/3016'),false);
                System.assertEquals(Regex_Validator.isValidDate('adasdsad'),false);
                System.assertEquals(Regex_Validator.isValidDate('!@#$%%^&*()'),false);
                System.assertEquals(Regex_Validator.isValidDate(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidSSN() {
        test.startTest();
            System.assertEquals(Regex_Validator.isValidSSN('222-22-2222'),true);
            System.assertEquals(Regex_Validator.isValidSSN('222-2-2222'),false);
            System.assertEquals(Regex_Validator.isValidSSN('22-22-2222'),false);
            System.assertEquals(Regex_Validator.isValidSSN('22-22-222'),false);
            System.assertEquals(Regex_Validator.isValidSSN('222222222'),true);
            System.assertEquals(Regex_Validator.isValidSSN('2222222'),false);
            System.assertEquals(Regex_Validator.isValidSSN('asdsad'),false);
            System.assertEquals(Regex_Validator.isValidSSN('!@#$%%^&*()'),false);
            System.assertEquals(Regex_Validator.isValidSSN(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidPhone() {
        test.startTest();
                System.assertEquals(Regex_Validator.isValidPhone('(408)421-3722'),true);
                System.assertEquals(Regex_Validator.isValidPhone('4084213722'),true);
                System.assertEquals(Regex_Validator.isValidPhone('+1(408)421-3722'),true);
                System.assertEquals(Regex_Validator.isValidPhone('(333)222-999'),true);
                System.assertEquals(Regex_Validator.isValidPhone('3332229999'),true);
                System.assertEquals(Regex_Validator.isValidPhone('asdsadsad'),false);
                System.assertEquals(Regex_Validator.isValidPhone('!@#$$%%@'),false);
                System.assertEquals(Regex_Validator.isValidPhone(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidZipCode() {
        test.startTest();
            System.assertEquals(Regex_Validator.isValidZipCode('95070'),true);
            System.assertEquals(Regex_Validator.isValidZipCode('95070-2344'),true);
            System.assertEquals(Regex_Validator.isValidZipCode('95070-234'),false);
            System.assertEquals(Regex_Validator.isValidZipCode('950702344'),true);
            System.assertEquals(Regex_Validator.isValidZipCode('99999'),true);
            System.assertEquals(Regex_Validator.isValidZipCode('999'),false);
            System.assertEquals(Regex_Validator.isValidZipCode('9a9n9'),false);
            System.assertEquals(Regex_Validator.isValidZipCode('asdasd'),false);
            System.assertEquals(Regex_Validator.isValidZipCode('!@#$$%%@'),false);
            System.assertEquals(Regex_Validator.isValidZipCode(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidNumber() {
        test.startTest();
            System.assertEquals(Regex_Validator.isValidNumber('95070'),true);
            System.assertEquals(Regex_Validator.isValidNumber('-99'),false);
            System.assertEquals(Regex_Validator.isValidNumber('asdasd'),false);
            System.assertEquals(Regex_Validator.isValidNumber('!@#$$%%@'),false);
            System.assertEquals(Regex_Validator.isValidNumber(null),false);
        test.stopTest();
    }

    @isTest static void test_isValidSfId(){
      test.startTest();
          System.assertEquals(Regex_Validator.isValidSfId('32423324'),false);
          System.assertEquals(Regex_Validator.isValidSfId('00363000005eIbz'),true);
          System.assertEquals(Regex_Validator.isValidSfId('00363000005eIbzAlz'),true);
          System.assertEquals(Regex_Validator.isValidSfId(null),false);
      test.stopTest();
    }
}

Apex Get Enum from String

Get Enum from String
  public Enum EventType { USER_LOGIN, USER_LOGOUT, USER_COMPLETE_FORM, USER_CLICK }

  static Map<String, EventType> nameToEvenType {
    get {
        Map<String, EventType> eventTypeValues = new Map<String, EventType>();
        if (nameToEvenType == null){
          for (EventType enumValue : EventType.values()){
              eventTypeValues.put(String.valueOf(enumValue).toUpperCase(), enumValue);
          }
        }
        return eventTypeValues;
    }
    private set;
}

public static EventType getEventType(String name){
    return String.isBlank(name) ? null : nameToEvenType.get(name.toUpperCase());
}

Test Class
@IsTest
public static void testNameToEvenType(){
 	Test.startTest();
      System.assertEquals(Events.EventType.USER_LOGIN, Events.getEventType('USER_LOGIN'));
      System.assertEquals(Events.EventType.USER_LOGOUT, Events.getEventType('USER_LOGOUT'));
      System.assertEquals(Events.EventType.USER_COMPLETE_FORM, Events.getEventType('USER_COMPLETE_FORM'));
      System.assertEquals(Events.EventType.USER_CLICK, Events.getEventType('USER_CLICK'));
    Test.stopTest(); 
}

Apex Advanced Selector Queries

Sub Queries

public Contact getRelatedDetails(Id userId){
       fflib_QueryFactory qf = new fflib_QueryFactory(Contact.sObjectType);
       qf.selectField('name').selectField('Id').setCondition('Id In (Select ContactId from User where Id=:userId)');
       qf.subselectQuery('Goals__r').selectField('Id');
       qf.subselectQuery('Account_Client__r').selectField('Id').selectField('External_Id__c').selectField('Institution_Account__c').setCondition('Status__c=\'Active\'');
       Contact bpContact = Database.query(qf.toSOQL());
       return bpContact;
     }

Pagination with Ordering

  public List<Goal__c> selectByUserIdAndPageNumber(Id userId, Integer pageNumber){
     Integer calcOffset = pageNumber * 200;
     fflib_QueryFactory goalQueryFactory = newQueryFactory();
     fflib_QueryFactory.Ordering ordering = new fflib_QueryFactory.Ordering(Goal__c.CreatedDate, fflib_QueryFactory.SortOrder.DESCENDING, false);
     goalQueryFactory.addOrdering(ordering);
     List<fflib_QueryFactory.Ordering> orderings = goalQueryFactory.getOrderings();
     orderings.remove(0);
     String queryWithOffSet = goalQueryFactory.setCondition('Client__c in (Select ContactId from User where Id=:userId)').setLimit(maxGoalsPerPage).toSOQL() + ' offset ' + calcOffset;
     return (List<Goal__c>) Database.query(queryWithOffSet);
}

Aggregate Query

public List<AggregateResult> getAggregateDetails(List<Id> securityIds){
    String selectedACLevel = 'Level_1__c';

    String formatQuery = String.format('Select {0} sumOfAllocPercent, {1} from Classification__c where Security__c IN :securityIds Group By {2}', new String[]{'sum(Asset_Percent__c)', selectedACLevel, selectedACLevel});
    List<AggregateResult> aggregateResultForAC= (List<AggregateResult>) Database.query(formatQuery);

    return aggregateResultForAC;
}

Implementing Mocking for Apex Tests

Trying to mock a actual database query can be hard. Using the fflib_Mocks jar you can generator Selector Mocks: More info here:

https://github.com/financialforcedev/fflib-apex-mocks

1. Create interface class that extends fflib_ISObjectSelector to mock the Selector 

2. Create interfacemocks.properties file in root of your project

3. Run java -jar apex-mocks-generator-4.0.0.jar “{path}/src/classes” “{path}/interfacemocks.properties” “fflib_Mocks” “{path}/src/classes” “30.0” to generate mocking files

4. Used mocked selector to write stub selector

5. Run mock generator as maven job

1. Create interface class that extends fflib_ISObjectSelector to mock the Selector 
public interface System_Selector extends fflib_ISObjectSelector {
    List<Object> selectByUserId(Id userId);
}

2. Create interfacemocks.properties file in root of your project

System_Selector=Contact:fflib_SObjectMocks.SObjectSelector
3. Run java -jar apex-mocks-generator-4.0.0.jar "{path}/src/classes" "{path}/interfacemocks.properties" "fflib_Mocks" "{path}/src/classes" "30.0" to generate mocking files
/* Generated by apex-mocks-generator version 4.0.1 */
@isTest
public class Selector_Mocks{
    public class Contact extends fflib_SObjectMocks.SObjectSelector implements System_Selector{
        private fflib_ApexMocks mocks;

        public Contact(fflib_ApexMocks mocks){
            super(mocks);
            this.mocks = mocks;
        }
        
        public List<Object> selectByUserId(Id userId){
            return (List<Object>) mocks.mockNonVoidMethod(this, 'selectByUserId', new List<Type> {System.Type.forName('Id')}, new List<Object> {userId});
        }
    }
}

4. Used mocked selector to write stub selector

  @isTest static void testContactSelector(){
        // Create mocks
  		fflib_ApexMocks mocks = new fflib_ApexMocks();
  		fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks);

  		System_Selector selectorMock = new System_Mocks.Contact(mocks);

  		// Given
  		mocks.startStubbing();
        List<Contact> testContactList = new List<Contact> {
  			new Contact(
  				Id = fflib_IDGenerator.generate(Contact.SObjectType),
  				FirstName = 'Test FirstName',
  				LastName = 'Test LastName',
  				Email = 'test@mail.com') };

        Set<Id> testContactSet = new Map<Id, Contact>(testContactList).keySet();
        User testUserId = new User(	Id = fflib_IDGenerator.generate(User.SObjectType),
  				FirstName = 'Test FirstName',
  				LastName = 'Test LastName',
  				Email = 'test@mail.com' );
        mocks.when(selectorMock.sObjectType()).thenReturn(Contact.SObjectType);
    		mocks.when(selectorMock.selectByUserId(testUserId.Id)).thenReturn(testContactList);
  		mocks.stopStubbing();

      System_Rest_App.Selector.setMock(selectorMock);

      // When
      new System_Mocks.Contact(mocks).selectByUserId(testUserId.Id);

      // Then
      ((System_Selector)
        mocks.verify(selectorMock)).selectByUserId(testUserId.Id);
    }

5. Run mock generator as maven job – ant generate.mocks

  <target name="generate.mocks">
		<java classname="com.financialforce.apexmocks.ApexMockGenerator">
			<classpath>
				<pathelement location="${basedir}/lib/apex-mocks-generator-4.0.1.jar"/>
			</classpath>
			<arg value="${basedir}/src/classes"/>
			<arg value="${basedir}/interfacemocks.properties"/>
			<arg value="Selector_Mocks"/>
			<arg value="${basedir}/src/classes"/>
		</java>
	</target>

Salesforce Chatter Attaching Files to SObject

There are different ways you can use salesforce to attach a file to an SObject using Chatter.

  • Apex Code
  • Chatter REST Api
    public static void createAttachmentFeed(List<Attachment> attachments){
      for (Attachment attachment : attachments){
        // ContentVersion is how you upload a file!
       ContentVersion version = new ContentVersion();
       version.Title=attachment.Name;
       version.PathOnClient = '\\' + attachment.Name;
       version.VersionData = attachment.Body;
       version.NetWorkId = brightPlanWebCustomSettings.Community_Id__c;
       insert version;

       // After you insert the ContentVersion object, a base 'ContentDocument' is established
       // The ID of the ContentDocument is what you need to attach the file to the Chatter post.
       version = [SELECT ID,ContentDocumentId FROM ContentVersion WHERE ID=:version.id];

       // Form a basic post attached to our own feed.
       ConnectApi.FeedItemInput feedItem = new ConnectApi.FeedItemInput();
       feedItem.subjectId = attachment.ParentId; // This can also be an objectID to post the file to.

       // Now connect the feeditem to our already uploaded file.
       feedItem.capabilities = new ConnectAPI.FeedElementCapabilitiesInput();
       feedItem.capabilities.files = new ConnectAPI.FilesCapabilityInput();
       feedItem.capabilities.files.items = new List<ConnectAPI.FileIdInput>();
       ConnectAPI.FileIdInput attachFile = new ConnectAPI.FileIDInput();

       //**** Here is where we attach the specific file to the post!
       attachFile.id = version.contentDocumentid;
       feedItem.capabilities.files.items.add(attachFile);

       // Execute the posting
       ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(brightPlanWebCustomSettings.Community_Id__c, feedItem);
      }
    }

Some limitations using this is using Blob for version.VersionData = attachment.Body; will run into Apex String length exceeds maximum: 6000000.

Chatter Rest API

POST https://cs43.salesforce.com/services/data/v35.0/connect/communities/0DB63000000003jGAA/chatter/feed-elements/batch

HEADER

Authorization         Bearer {sessionToken}

Content-Type          multipart/form-data; boundary=a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq

Format is as follows:

{salesforceOrgUrl}/services/data/v35.0/connect/communities/{salesforceCommunityId}chatter/feed-elements/batch

The subjectId is the SObject Id to which you want to link the file, is this case we use our Case Id.

--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Content-Type: application/json; charset=UTF-8
Content-Disposition: form-data; name="json"
{
"inputs": [
{
"binaryPartNames": [
"file1"
],
"richInput": {
"subjectId": "50063000003YlSe",
"capabilities": {
"content": {
"title": "file1.pdf"
}
},
"feedElementType": "FeedItem"
}
},
{
"binaryPartNames": [
"file2"
],
"richInput": {
"subjectId": "50063000003YlSe",
"capabilities": {
"content": {
"title": "file2.pdf"
}
},
"feedElementType": "FeedItem"
}
}
]
}
--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Content-Type: application/octet-stream; charset=ISO-8859-1
Content-Disposition: form-data; name="file1"; filename="file1.pdf"

...contents of file1.pdf...

--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Content-Disposition: form-data; name="file2"; filename="file2.pdf"
Content-Type: application/octet-stream; charset=ISO-8859-1
...contents of file2.pdf...
--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq--

Benefits you can batch upload files without hitting an Apex limits.

Both the Chatter and Apex solutions can be used by Community Users to upload files.

Run Spring Boot Scheduler as Heroku Worker

1. Setup Application
2. Setup Scheduler
3. Add Spring Boot dependency
4. Add Procfile
5. Scale Heroku worker

1. Setup Application

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * Created by tmichels on 9/20/15.
 */

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class);
    }
}

2. Setup Scheduler

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;

/**
 * Created by tmichels on 9/18/15.
 */

@Component
public class Scheduler {

    private static final Logger logger = LoggerFactory.getLogger(Scheduler.class);

    @Scheduled(cron = "0 0 13 * * *")
    public void buyHighPercentageReturnNotes(){
       System.out.println("Scheduler is running");
    }
}

3. Add Spring Boot dependency

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.6.RELEASE</version>
    </parent>
  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
      </dependency>
  </dependencies>

4. Add Procfile

worker: java $JAVA_OPTS -jar target/*.jar

5. Scale Heroku worker

heroku ps:scale worker=1
%d bloggers like this: