Apex One Endpoint to Rule them all

Most systems need to do CRUD on some objects and also query some information. Why not have one endpoint to do it all?I have created a endpoint that can do all that using mapping between systems field and Salesforce fields. System field does not contain any underscores or Salesforce specific syntax like (__c). With that a system can send:

{“name”:”John Doe”, “taxRatePerClient”:20}

And it will be translated to:

{“Name”:”John Doe”, “Tax_Rate_Per_Client__c”:20}

Do CRUD and execute any query

global with sharing class App_Rest_Data {

	global with sharing class Post implements App_Rest_Dispatcher.Dispatchable {
		private App_Rest_Global_Request_Json requestBody;

		global String getURIMapping(){
						return App_Rest_Constants.URI_DATA +'/{object}/{operation}';
		}

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

		global App_Rest_Global_Json execute(Map<String, String> parameters){
			App_Rest_Global_Json globalJson = new App_Rest_Global_Json();
			String objectName = parameters.get('object');
			String operation = parameters.get('operation');

			if ('q'.equals(operation)){
				globalJson.setResult(Database.query(String.valueOf(((Map<String, Object>)JSON.deserializeUntyped(requestBody.getRequest())).get('query'))));
			} else {
	            Map<String,String> uiFieldNamesForObject = App_Rest_Settings.getModelByObject(objectName);
	            App_JSON.DynamicModel dynamicModel = new App_JSON.DynamicModel(uiFieldNamesForObject);
	            dynamicModel.setMapKeysFlag(true);
	            Type classType = Type.forName(objectName);
	            SObject sobjectModel = (SObject) classType.newInstance();
	            sobjectModel =  (SObject)dynamicModel.deserialize(requestBody.getRequest(), classType);

	            fflib_ISObjectUnitOfWork uow = App_Rest_App.UnitOfWork.newInstance();

	            if ('c'.equals(operation)){
	              uow.registerNew(sobjectModel);
	            } else if ('u'.equals(operation)){
	              uow.registerDirty(sobjectModel);
	            } else if('d'.equals(operation)){
	              uow.registerDeleted(sobjectModel);
	            }
	            uow.commitWork();

	            Schema.SObjectType sobjectModelType = Schema.getGlobalDescribe().get(objectName);
	            fflib_QueryFactory queryFactory = new fflib_QueryFactory(sobjectModelType);
	            queryFactory.selectFields(uiFieldNamesForObject.keySet());

	            globalJson.setResult((List<Object>) Database.query(queryFactory.toSOQL()));
  			}

			return globalJson;
		}

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

Testing out CRUD and query operations

@isTest
private class App_Rest_Data_Test {

	@isTest(SeeAllData=true) static void testPost() {
		Test.setMock(HttpCalloutMock.class, new App_LogEntries_Api_Mock());
		RestRequest req = new RestRequest();
		RestResponse res = new RestResponse();
		User communityUser = App_Global_Test.setupCommunityUserAndLogin();
		Test.startTest();
			req.requestBody =  Blob.valueOf('{"firstName":"Test FirstName","lastName":"Test LastName", "email":"test@mail.com"}');
			req.requestURI = '/v1/data/contact/c';
			req.httpMethod = 'POST';
			RestContext.request = req;
			RestContext.response = res;
			App_Rest_Dispatcher.doPost();
		Test.stopTest();

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

	@isTest(SeeAllData=true) static void testPatch() {
		Test.setMock(HttpCalloutMock.class, new App_LogEntries_Api_Mock());
		RestRequest req = new RestRequest();
		RestResponse res = new RestResponse();
		User communityUser = App_Global_Test.setupCommunityUserAndLogin();
		Test.startTest();
			Account setupAccount = App_Global_Test.setupAccount();
			req.requestBody =  Blob.valueOf('{"id":"'+setupAccount.Id+'","name":"Test Update"}');
			req.requestURI = '/v1/data/account/u';
			req.httpMethod = 'POST';
			RestContext.request = req;
			RestContext.response = res;
			App_Rest_Dispatcher.doPost();
		Test.stopTest();

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

	@isTest(SeeAllData=true) static void testDelete() {
		Test.setMock(HttpCalloutMock.class, new App_LogEntries_Api_Mock());
		RestRequest req = new RestRequest();
		RestResponse res = new RestResponse();
		User communityUser = App_Global_Test.setupCommunityUserAndLogin();
		Test.startTest();
			Account setupAccount = App_Global_Test.setupAccount();
			req.requestBody =  Blob.valueOf('{"id":"'+setupAccount.Id+'","name":"Test Update"}');
			req.requestURI = '/v1/data/account/d';
			req.httpMethod = 'POST';
			RestContext.request = req;
			RestContext.response = res;
			App_Rest_Dispatcher.doPost();
		Test.stopTest();

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

	@isTest(SeeAllData=true) static void testQuery() {
		Test.setMock(HttpCalloutMock.class, new App_LogEntries_Api_Mock());
		RestRequest req = new RestRequest();
		RestResponse res = new RestResponse();
		User communityUser = App_Global_Test.setupCommunityUserAndLogin();
		Test.startTest();
			Account setupAccount = App_Global_Test.setupAccount();
			req.requestBody =  Blob.valueOf('{"query":"Select id from User"}');
			req.requestURI = '/v1/data/account/q';
			req.httpMethod = 'POST';
			RestContext.request = req;
			RestContext.response = res;
			App_Rest_Dispatcher.doPost();
		Test.stopTest();

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

Apex Convert String To Camel Case

Convert string to camelCase for one underscore

   public static String toCamelCase(String input) {
      Pattern p = Pattern.compile('_([a-zA-Z])');
      Matcher m = p.matcher( input );
      fflib_StringBuilder sb = new fflib_StringBuilder();
      while (m.find()) {
          sb.add(m.replaceAll(m.group(1).toUpperCase()));
      }
      return sb.toString().uncapitalize();
     }

Convert string to camelCase for multiple underscore

 public static String toCamelCase(String input){
      fflib_StringBuilder sb = new fflib_StringBuilder();
      boolean capitalizeNext = false;
      for (String c:input.split('')) {
        if (c == '_') {
          capitalizeNext = true;
        } else {
          if (capitalizeNext) {
            sb.add(c.toUpperCase());
            capitalizeNext = false;
          } else {
            sb.add(c.toLowerCase());
          }
        }
      }
      return sb.toString();
    }

To Camel Case Test Class

@isTest
	static void testToCamelCase(){
		Test.startTest();
			System.assertEquals(BrightPlan_Rest_Util.toCamelCase('loan_id'), 'loanId');
			System.assertEquals(BrightPlan_Rest_Util.toCamelCase('Camel_case_This_word'), 'camelCaseThisWord');
			System.assertEquals(BrightPlan_Rest_Util.toCamelCase('this_IS_A_TEst_for_CAMEL_Case'), 'thisIsATestForCamelCase');
		Test.stopTest();
	}

Query Multi-Select Picklists in SOQL

Create list of values to check for multi picklist

       Set<String> typeSet = new Set<String>();
       String allStr = 'All';
       typeSet.add('\''+ allStr + '\'');
       for (Financial_Goal__c goal  : Contact.FinancialGoals__r){
         typeSet.add('\''+ goal.Type__c + '\'');
       }

Query using INCLUDES and using String.join to create string of List.

List<Content__c> typeMappings = (List<Content__c>) Database.query(
contentMappingQueryFactory.setCondition('Status__c=\'Publish\'
AND Id!=:featureMappingId AND (Goal_Type_Attribute__c INCLUDES(' + String.join(goalTypeList, ',')+ ') 
AND Age_Attribute__c INCLUDES(' + String.join(ageList, ',')+ ') 
AND Employment_Attribute__c IN :employementList AND Gender_Attribute__c IN :genderList 
AND Net_Worth_Attribute__c IN :netWorthList)').setLimit(2).toSOQL());

Use , for OR and ; for AND to match values in picklist

OR:
will match for any value in picklist
String.join(goalTypeList, ',')

AND:
will match for all the value in picklist
String.join(goalTypeList, ';')

Apex download images from RSS feed as attachments

Update Blog Post Job calling from RSS

  @future(callout=true)
    static global void upsertContentMapping() {
        List<Dom.XMLNode> recentPosts = getRSSFeed('https://www.yoursite.com/resources/rss.xml');
        List<BP_Content_Mapping__c> blogEntries = new List<BP_Content_Mapping__c>();
        List<String> titles = new List<String>();
        List<BP_Content_Mapping__c> entriesToAdd = new List<BP_Content_Mapping__c>();

        String listOfBlogs = '';
        System.Debug('# OF POSTS FOUND:' +recentPosts.size());

        for(Dom.XMLNode post : recentPosts) {
           blogEntries.add(convertFeedToBlogPost(post));
        }

        System.Debug('# OF ENTRIES FOUND:' +blogEntries.size());

        for(BP_Content_Mapping__c bp : blogEntries) {
            titles.add(bp.Content_URL__c);
        }

        List<Attachment> blogAttachments = new List<Attachment>();
        List<BP_Content_Mapping__c> blogs = [SELECT Id, GUID__c from BP_Content_Mapping__c WHERE Content_URL__c IN :titles];
        Integer counter = 0 ;
        for(BP_Content_Mapping__c blogEntry : blogEntries) {
            if (counter < 10){
              Boolean added = false;
              for(BP_Content_Mapping__c blog : blogs) {
                  if(blog.GUID__c == blogEntry.GUID__c) { added = true; }
              }
              if(!added) {
                entriesToAdd.add(blogEntry);
                counter++;
              }
            }
        }

        Map<String, Attachment> attachments =  createAttachments(entriesToAdd);

        System.Debug('# OF ENTRIES TO IMPORT:' + entriesToAdd.size() + ' ' + entriesToAdd);
        insert entriesToAdd;

        for (BP_Content_Mapping__c entry : entriesToAdd){
          Attachment attachment = attachments.get(entry.GUID__c);
          attachment.ParentId = entry.Id;
          attachments.put(entry.GUID__c, attachment);
        }

        System.Debug('# OF ATTACHMENTS TO IMPORT:' + attachments.size() + ' ' + attachments);
        insert attachments.values();
   }

Get RSS feed and return XMLNode

static global List<Dom.XMLNode> getRSSFeed(string URL) {
      Http h = new Http();
      HttpRequest req = new HttpRequest();
      // url that returns the XML in the response body

      req.setEndpoint(url);
      req.setMethod('GET');
      HttpResponse res = h.send(req);
      Dom.Document doc = res.getBodyDocument();

      Dom.XMLNode rss = doc.getRootElement();
      System.debug('@@' + rss.getName());

      List<Dom.XMLNode> rssList = new List<Dom.XMLNode>();
      for(Dom.XMLNode child : rss.getChildren()) {
         System.debug('@@@' + child.getName());
         for(Dom.XMLNode channel : child.getChildren()) {
             System.debug('@@@@' + channel.getName());

             if(channel.getName() == 'item') {
                  rssList.add(channel);
             }
         }
      }
      return rssList;
  }

Convert XMLNode to Blog Posts

static global BP_Content_Mapping__c convertFeedToBlogPost(Dom.XMLNode post) {
      BP_Content_Mapping__c bp = new BP_Content_Mapping__c();
      Integer tagIndex = 0;

      for(Dom.XMLNode child : post.getChildren()) {
          if(child.getName() == 'title') { bp.Title__c = child.getText(); }
                    if(child.getName() == 'guid') { bp.GUID__c = child.getText(); }
          if(child.getName() == 'pubDate') { bp.Effective_Date__c = convertRSSDateStringToDate(child.getText()); }
          if(child.getName() == 'link') { bp.Content_URL__c = child.getText(); }
          if(child.getName() == 'description') {
            String[] descriptionPipeSlit = child.getText().split('\\|',-1);
            if (descriptionPipeSlit.size() > 0){
              bp.Summary__c = descriptionPipeSlit[0];
              bp.Author__c = descriptionPipeSlit[1].remove('Author:');
              bp.Blog_Category__c = descriptionPipeSlit[2].remove('Category:');
              bp.Category_Color__c = descriptionPipeSlit[3].remove('Color:');
            }
          }
          if ('content'.equals(child.getName())){
              bp.Image_URL__c = child.getAttributeValue('url', null);
          }
      }
      return bp;
  }

Download blog images to attachment

static Map<String, Attachment> createAttachments(List<BP_Content_Mapping__c> contentMappings){
    Map<String, Attachment> attachments = new Map<String, Attachment>();
    for (BP_Content_Mapping__c contentMapping : contentMappings){
      Attachment attachment = new Attachment();
      attachment.Name = contentMapping.Title__c + '.jpg';
      attachment.ContentType= 'image/jpg';
      attachment.Body = downloadImagesToBlob(contentMapping.Image_URL__c);
      attachments.put(contentMapping.GUID__c, attachment);
    }
    return attachments;
  }

  static Blob downloadImagesToBlob(String imageUrl){
      Http h = new Http();
      HttpRequest req = new HttpRequest();
      req.setEndpoint(imageUrl);
      req.setMethod('GET');
      req.setHeader('Content-Type', 'image/jpg');
      req.setCompressed(true);
      req.setTimeout(60000);
      HttpResponse res = null;
      res = h.send(req);
      Blob image = res.getBodyAsBlob();
      return image;
  }

Catch Apex Callout Loop not Allowed

Getting the following error: Apex Callout Loop not Allowed when you are doing a callout to the same Salesforce org and then calling out to another external service.

In this case I was doing a callout to the same salesforce org and then doing a callout to the my logging service.

RestRequest request = RestContext.request;
Map requestHeaders = request.headers;
if (requestHeaders.containsKey('Sfdc-Stack-Depth') && '1'.equals(requestHeaders.get('Sfdc-Stack-Depth'))){
      System.debug('Do not calllout as it will cause a callout loop error');
} 

Further you can write these values to a custom object and have a scheduler or workflow run to do the callout.

Apex workaround for doing callout after DML

If you are trying to do a callout after a DML operation you would have seen this error message:

You have uncommitted work pending. Please commit or rollback before calling out

A way around doing callouts after a DML is to do the DML as its own internal callout. This is possible by creating a internal endpoint which will be called first to insert the records. After the insert is complete serialize the response and use that for the external callout.

Steps to following:
1. Create internal endpoint to insert records
2. Create internal Httprequest to the endpoint above
3. Combine internal callout and external callout

Create internal endpoint to insert records

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

	global String getURIMapping(){
					return Rest_Constants.URI_ACCOUNT + '/internal';
	}

	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();
			List<Financial_Account__c> financialAccountList = (List<Financial_Account__c>)JSON.deserialize(requestBody.getRequest(), List<Financial_Account__c>.class);
			List<Financial_Account__c> bpFinancialAccounts = App_Service.instance.insertFinancialAccount(financialAccountList);
			globalJson.setResult(Selector_Financial_Account.newInstance().selectByFinAccountAccounts(bpFinancialAccounts).values());
			return globalJson;
	}
}

Create internal Httprequest to the internal endpoint

public static HttpResponse doInternalCalloutToInsertAccount(List<Financial_Account__c> financialAccountList){
	Http h = new Http();
	Httprequest req = new Httprequest();
	req.setMethod('POST');

	req.setHeader('Authorization','Bearer ' + UserInfo.getSessionId());
	req.setHeader('Content-Type','application/json');
	Object finaAccountListObject = (Object)financialAccountList;
	req.setBody(JSON.serialize(finaAccountListObject));
	req.setEndpoint(System.URL.getSalesforceBaseURL().toExternalForm() + '/services/apexrest/v1/account/internal');
	HttpResponse response = h.send(req);
	return response;
}

Combine internal callout and external callout

HttpResponse response = doInternalCalloutToInsertAccount(financialAccountList);

if (response!=null && response.getBody()!=null){
	Map<String,Object> serializeResponse = (Map<String,Object>)JSON.deserializeUntyped(response.getBody());
	bpAccountInserted = (List<Financial_Account__c>)JSON.deserialize(JSON.serialize(serializeResponse.get('result')), List<Financial_Account__c>.class);
	List<Metadata.BPUpdatedAccounts> bpAccountUpdateList = new List<Metadata.BPUpdatedAccounts>();

	for (Financial_Account__c bpAccount : bpAccountInserted){
		...
	}
}

Apex Sorting Objects with Comparable Interface

Sorting objects generic class

public abstract class Comparator {
    public abstract Integer compare(Object o1, Object o2);
    public static void sort(Object[] values, Comparator comp) {
        //  Obtain the list type of values
        Object[] temp = values.clone();
        temp.clear();
        //  Helper class for sorting using Comparable
        Helper[] tempValues = new Helper[0];
        for(Object value: values) {
            tempValues.add(new Helper(comp, value));
        }
        //  Perform sort
        tempValues.sort();
        //  Extract values back into temp list
        for(Helper helper: tempValues) {
            temp.add(helper.value);
        }
        //  And set the list to the new, sorted order
        values.clear();
        values.addAll(temp);
    }
    //  Simply calls Comparator when asked.
    class Helper implements Comparable {
        Comparator method;
        Object value;
        Helper(Comparator comp, Object val) {
            method = comp;
            value = val;
        }
        public Integer compareTo(Object o) {
            return method.compare(value, ((Helper)o).value);
        }
    }
}

Compare Strings

public class AccountNameComparator extends Comparator {
    public override Integer compare(Object a, Object b) {
        return ((Account)a).name.compareTo(((Account)b).name);
    }
}

Compare Dates

public class PriceHistoryPriceDateCompare extends Comparator {
    public override Integer compare(Object a, Object b) {
      return (DateTime.newInstance(((History__c)a).Price_Date__c.year(), ((History__c)a).Price_Date__c.month(),1).getTime().format()).compareTo((DateTime.newInstance(((History__c)b).Price_Date__c.year(), ((History__c)b).Price_Date__c.month(), 1).getTime()).format());
    }
	}

Test Class

@isTest(SeeAllData=true) static void testComparator(){
		List<Account> accountToSort = new List<Account>();
		for (Integer k = 70;k > 64;k--){
			Account acc = (Account)SmartFactory.createSObject('Account');
			acc.Name = String.fromCharArray(new List<integer> {k});
			insert acc;
			accountToSort.add(acc);
		}

		Test.startTest();
    	Comparator.sort(accountToSort, new AccountNameComparator());
			System.assertEquals(accountToSort.get(0).Name, 'A');
			System.assertEquals(accountToSort.get(1).Name, 'B');
			System.assertEquals(accountToSort.get(2).Name, 'C');
			System.assertEquals(accountToSort.get(3).Name, 'D');
			System.assertEquals(accountToSort.get(4).Name, 'E');
			System.assertEquals(accountToSort.get(5).Name, 'F');
		Test.stopTest();
	}

Apex Cache RecordTypes for fast retrieval

Apex class to lookup record types and cache them

  private static Map<Schema.SObjectType,Map<String,Id>> rtypesCache;

static {
   rtypesCache = new Map<Schema.SObjectType,Map<String,Id>>();//convenient map, formatted from r    esults.
}

  public static Map<String, Id> getRecordTypeMapForObjectGeneric(Schema.SObjectType token) {
      Map<String, Id> mapRecordTypes = rtypesCache.get(token);
      if (mapRecordTypes == null) {
          mapRecordTypes = new Map<String, Id>();
          rtypesCache.put(token,mapRecordTypes);
      } else {
           return mapRecordTypes;
      }

      Schema.DescribeSObjectResult obj = token.getDescribe();
      if (results == null || results.isEmpty()) {
          String soql = 'SELECT Id, Name, DeveloperName, sObjectType FROM RecordType WHERE IsActive = TRUE';
          try {
              results = Database.query(soql);
          } catch (Exception ex) {
              results = new List<SObject>();
          }
      }

      Map<Id,Schema.RecordTypeInfo> recordTypeInfos = obj.getRecordTypeInfosByID();
      for (SObject rt : results) {
          if (recordTypeInfos.get(rt.Id) != null) {
              if (recordTypeInfos.get(rt.Id).isAvailable()) {
                  mapRecordTypes.put(String.valueOf(rt.get('DeveloperName')),rt.Id);
              }
              else {
                  System.debug('The record type ' + rt.get('DeveloperName') + ' for object ' + rt.get('sObjectType') + ' is not availiable for the user.');
              }
          }
      }
      return mapRecordTypes;
    }

Test Class

  @isTest(SeeAllData=true) static void testGetRecordTypeMapForObjectGeneric(){
      Test.startTest();
        Map<String, Id> caseRecordTypeId = App_Service.getRecordTypeMapForObjectGeneric(Case.SobjectType);
        System.Assert(caseRecordTypeId.get('Error')!=null);
        System.Assert(caseRecordTypeId.get('Internal')==null);
      Test.stopTest();
    }

Apex Check User Session Still Valid

Checking if a user has a valid session before making a query or call else an INVALID_SESSION error will be returned. The value of time remaining for the session can be cached and referenced so this query only runs a few times. Every time a user makes a request we can check the cache and see if the user has time remaining.

Query session information and returning UserSession model

public static UserSession getUserSessionInfo(User userObj){
      UserSession bpUserSession = new UserSession();
      List<AuthSession> session = [Select LastModifiedDate, NumSecondsValid from AuthSession where UsersId = : userObj.Id] ;
      bpUserSession.setSessionValid(false);

      for(AuthSession sessionObj : session){
          bpUserSession.setSecondsValid(sessionObj.NumSecondsValid);
          bpUserSession.setLastModifiedDate(sessionObj.LastModifiedDate);
      }

      if (bpUserSession.getSecondsValid()!=null)
        bpUserSession.setSessionExpireTime(bpUserSession.getLastModifiedDate().addSeconds(bpUserSession.getSecondsValid()));

      if( bpUserSession.getSessionExpireTime() > System.now())
          bpUserSession.setSessionValid(true);

      List<AggregateResult> loginHistoryObj = [SELECT MAX(LoginTime) FROM LoginHistory WHERE UserId = : userObj.Id GROUP BY UserId];

      DateTime loginDateTime = (DateTime)loginHistoryObj[0].get('expr0');
      bpUserSession.setLoginTime(loginDateTime);
      Date loginDate = loginDatetime.date();
      if( logindate != (DateTime.now()).date())
          bpUserSession.setSessionValid(false);

     return bpUserSession;
    }

User Session model to set session information

    public class UserSession {
      DateTime lastModifiedDate;
      Integer secondsValid;
      DateTime sessionExpireTime;
      Boolean sessionValid;
      DateTime loginTime;

      public void setLastModifiedDate(DateTime lastModifiedDate){
        this.lastModifiedDate = lastModifiedDate;
      }

      public DateTime getLastModifiedDate(){
        return lastModifiedDate;
      }

      public void setSecondsValid(Integer secondsValid){
        this.secondsValid = secondsValid;
      }

      public Integer getSecondsValid(){
        return secondsValid;
      }

      public void setSessionExpireTime(DateTime sessionExpireTime){
        this.sessionExpireTime = sessionExpireTime;
      }

      public DateTime getSessionExpireTime(){
        return sessionExpireTime;
      }

      public void setSessionValid(Boolean sessionValid){
        this.sessionValid = sessionValid;
      }

      public Boolean getSessionValid(){
        return sessionValid;
      }

      public void setLoginTime(DateTime loginTime){
        this.loginTime = loginTime;
      }

      public DateTime getLoginTime(){
        return loginTime;
      }
    }

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);
}
%d bloggers like this: