Encryption/Decryption in Salesforce

When developing integration between Salesforce and other application we sometimes face issue regarding storing sensitive information like password and security token.

Salesforce encrypted fields wasn't able to provide good enough solution, as users with permission View Encrypted Data can view it, in addition during summer 17' upgrade Salesforce update the encryption field functionality and those values are no longer masked or encrypted when viewed in Salesforce.
However, Salesforce does provide Ecnrypto class that provide some method for encrypt/decrypt. Still, you might face new challenge when you need to use the encryped data in other application (in my case Java).

In this example I demonstrate how we can use such encryption process. It contains the following steps:
1. Page for entering encrypted data.
2. Encryption setup in Salesforce.
3. Utility process that get text and encrypt it based on the setup from 1.
4. Trigger for the specific object that encrypt the data
5. Process for replacing the encrypted data.
6. Java util class that decode the data.

Lets start working:
1. Page for entering encrypted data
This is not required, but without this step, when user will enter his security information it will be visible during typing. It's preferred to mask it, and it isn't complex to solved it.
The component apex:inputSecret does the masking for us.

Assume we have custom object User_Credential__c, with the fields User_Name__c, Password__c, Security_Token__c, then we can create use the following visualforce page.


<apex:page standardController="User_Credential__c" tabStyle="User_Credential__c">  
 <apex:form >  
 
  <apex:sectionHeader title="Enter Credentials" subtitle="Enter Credentials"/>  

  <apex:pageBlock>  

   <apex:pageBlockButtons >  
    <apex:commandButton action="{!save}" value="Save"/>  
    <apex:commandButton action="{!cancel}" value="Cancel"/>  
   </apex:pageBlockButtons>  
  
   <apex:pageBlockSection columns="2">  
    <apex:inputField value="{!User_Credential__c.Name}"/>  
    <apex:inputField value="{!User_Credential__c.User_Name__c}"/>  
    <apex:inputSecret value="{!User_Credential__c.Password__c}"/>  
    <apex:inputSecret value="{!User_Credential__c.Security_Token__c}"/>  
   </apex:pageBlockSection>  
  </apex:pageBlock>  
 </apex:form>  
</apex:page>  




2. Encryption setup in Salesforce
We don't want to use hard coded values. Therefore we need to setup few settings + place holder for our encryption key. I use custom setting. Named it "Encryption Settings", and added 3 fields there:
After the setup create record in this custom settings. The Encryption Method that we will use is AES128, the Encryption Self Len we will use is 10. This is just dummy text that will added to the text before encryption in order to defend against hacking.

How to get your Encryption Key? You can either wait to step 4, where we will add step that generate such key, or use script that generate key and store it in the custom settings:

Blob key = Crypto.generateAesKey(128);
String keyString = EncodingUtil.base64Encode(key);
System.debug('Encryption Key: ' + keyString);

3. Utility process that get text and encrypt it based on the setup from 1
We need function that receive input String and encode it based on the custom setting. Below simply code does the job. Most of logic is based on standard functions from classes EncodingUtil and Crypto.



public class EncryptionUtilities{  

 public static String hashString(String input){  

  String encryptKey = Encryption_Settings__c.getInstance().Encryption_Key__c;  
  String encryptMethod = Encryption_Settings__c.getInstance().Encryption_Method__c;  
  Integer saltLen = Integer.valueOf(Encryption_Settings__c.getInstance().Encryption_Salt_Len__c);  
  String salt = EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, saltLen);  
 
  Blob encrypted = Crypto.encryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(encryptKey), Blob.valueOf(input + salt));  

  return EncodingUtil.base64Encode(encrypted);  
 }  
}  


4.Trigger for the specific object that encrypt the data
In the custom object User_Credential__c we will setup trigger that when new record created or the password/token were changed it should encrypt those values.

The trigger will use the function from step 2


trigger UserCredential_Trigger on User_Credential__c (before insert, before update) {  

 if(Trigger.isInsert || Trigger.isUpdate){  
  for(User_Credential__c userCredential: Trigger.new){  

   if((userCredential.Password__c != null)  
    && (Trigger.isInsert || (Trigger.isUpdate && userCredential.Password__c != Trigger.oldMap.get(userCredential.Id).Password__c))){  

    userCredential.Password__c = EncryptionUtilities.hashString(userCredential.Password__c);  
   }  

   if((userCredential.Security_Token__c != null)  
    && (Trigger.isInsert || (Trigger.isUpdate && userCredential.Security_Token__c != Trigger.oldMap.get(userCredential.Id).Security_Token__c))){  

    userCredential.Security_Token__c = EncryptionUtilities.hashString(userCredential.Security_Token__c);  
   }  
  }  
 }  
}  




5. Process for replacing the encrypted data
It might be good idea to provide process for replacing the encryption key, for cases it was stolen. In such process, we need to change the Encryption Key in the custom setting and replace all existing values that encrypted with the old key, as we won't be able to decode them with the new key. Also note that from the code, you can learn how to decode the encryption data in using Salesforce standard Encrypto class.

This function can be added to the class that used in step 2 - EncryptionUtilities - and you might want to provide page/button that invoke it. 


//Used to replace the Encryption Key in custom setting, and recreate  
//all the encryptions in existing records  
public static String replaceEncriptionKey(){  

 String retMsg = '';  

 Savepoint sp = Database.setSavepoint();  

 try{  
  //Get current setting from custom metadata  
  Encryption_Settings__c encryptionSettings = [SELECT Id, Encryption_Key__c FROM Encryption_Settings__c LIMIT 1];  
  String currentEncriptionKey = encryptionSettings.Encryption_Key__c;  

  //Generate new encryption key  
  Blob key = Crypto.generateAesKey(128);  
  String keyString = EncodingUtil.base64Encode(key);  

  //save new encryption key to custom metadata - use apex Metadata API  
  encryptionSettings.Encryption_Key__c = keyString;  

  update encryptionSettings;  

  //Replace existing encrypted values  
  String encryptMethod = Encryption_Settings__c.getInstance().Encryption_Method__c;  
  Integer saltLen = Integer.valueOf(Encryption_Settings__c.getInstance().Encryption_Salt_Len__c);  

  List<User_Credential__c> l_userCredentials = new List<User_Credential__c>();  

  for(User_Credential__c userCredential : [SELECT Id, Password__c, Security_Token__c FROM User_Credential__c]){  

   Blob blobAfter64DecodePass = EncodingUtil.base64Decode(userCredential.Password__c);  
   Blob blobAfterDecodePass = Crypto.decryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(currentEncriptionKey), blobAfter64DecodePass);  
   String originalPass= blobAfterDecodePass.toString();  
   userCredential.Password__c = originalPass.substring(0, originalPass.length() - saltLen);  

   Blob blobAfter64DecodeToken = EncodingUtil.base64Decode(userCredential.Security_Token__c );  
   Blob blobAfterDecodeToken = Crypto.decryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(currentEncriptionKey), blobAfter64DecodeToken);  
   String originalToken = blobAfterDecodeToken.toString();  
   userCredential.Security_Token__c = originalToken.substring(0, originalToken.length() - saltLen);  

   l_userCredentials.add(userCredential);  
  }  

  //save all records - this should invoke the trigger that will use the new custom setting  
  update l_userCredentials;  
 }  
 catch(Exception ex){  
  Database.rollback(sp);  

  retMsg = 'Excpetion : ' + ex.getMessage();  
 }  
 return retMsg;  
}  


6. Java util class that decode the data.
The main method  - getDescryptedAsString  - get String encrypted and the key for decrypt and return the original text.


import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.util.Arrays;  
import java.util.Base64;  

public class ExternalDecoder {  

 private static final String characterEncoding = "UTF-8";  
 private static final String cipherTransformation = "AES/CBC/PKCS5Padding";  
 private static final String aesEncryptionAlgorithm = "AES";  
   
 public static String getDecryptedAsString(String encryptedText, String key) throws Exception{  
  return new String(decryptBase64EncodedWithManagedIV(encryptedText, key), characterEncoding);  
 }  
     
 public static byte[] decryptBase64EncodedWithManagedIV(String encryptedText, String key) throws Exception {  
  byte[] cipherText = Base64.getDecoder().decode(encryptedText.getBytes());  
  byte[] keyBytes = Base64.getDecoder().decode(key.getBytes());  
  return decryptWithManagedIV(cipherText, keyBytes);  
 }  
   
 public static byte[] decryptWithManagedIV(byte[] cipherText, byte[] key) throws Exception{  
  byte[] initialVector = Arrays.copyOfRange(cipherText,0,16);  
  byte[] trimmedCipherText = Arrays.copyOfRange(cipherText,16,cipherText.length);  
  return decrypt(trimmedCipherText, key, initialVector);  
 }  
   
 public static byte[] decrypt(byte[] cipherText, byte[] key, byte[] initialVector) throws Exception{  
  Cipher cipher = Cipher.getInstance(cipherTransformation);  
  SecretKeySpec secretKeySpecy = new SecretKeySpec(key, aesEncryptionAlgorithm);  
  IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);  
  cipher.init(Cipher.DECRYPT_MODE, secretKeySpecy, ivParameterSpec);  
  cipherText = cipher.doFinal(cipherText);  
  return cipherText;  
 }  
}  


Usage of the code:

String encryptPassword = <get value from SF>;
String encryptionKey =<get value from SF or store it in some setup file>;

String decodeValue = ExternalDecoder.getDecryptedAsString(encryptPassword, encryptionKey);
String password = decodeValue.substring(0, decodeValue.length()-10);













Salesforce Deployment with Java

Perhaps the most familiar deployment tool for SF admins/developers is the change set.
However for many of them, this is only method known for deployment. Some knows there are additional "advance" option (ant, eclipse, IDE, workbench, metadata API) but most are not really know how to use them.
In many cases, after getting familiar with other methods, admins/developers find it much more comfortable.

In some projects, where I was the only developer, I found it useful to deploy any changes directly from my local file system. After implementing the changes in my Developer sandbox, I import all the components into my local file system using IDE, then using java tool to zip the relevant changes and deploy the changes to QA sandbox. I added some simple options like saving the deployed ZIP file for deploying it later to other environments, or running validation only.
I'm adding here the code mostly because good way to learn how to use SF metadata api.

The usage of the tool is quite simple. First time you need to setup the property file in the app folder or you can setup the properties from the app menu Options -> Option.

ZIP_FILES_FOLDER - the folder where you want to store the zip files
TARGET_URL - should be either login/test (login for production)
PACAKGE_API - package API you want to user during deployment
LOCAL_FOLDER - folder where your local SF files are stored. This must be valid src folder, and the folder inside must match the folder names according to SF API. Note, that if you generated the folder using retrieve or by other tools that SF support (like Eclipse IDE) then the folder will be valid.
SF_USER - SF user name
DEPLOT_METHOD - should be either validate/deploy

After opening the app, click button 'Add Files' to select the components you want to deploy.

  • Click clear to remove the files you added
  • Click create package to save the ZIP file with the components you added
  • Click Load ZIP in case you want to use ZIP file that saved previously
  • Click Deploy ZIP to start deployment (you will need to enter your SF credential.











Technical side (code)
Of course I'm note going to go thru all the code, I'll just explain the main and important points. The rest you can review by yourself. Note, that you should have some background in Java or DotNet in order to understand it.
See full code + jar file for download at:
https://github.com/liron50/sf-local-deployment-tool

1. Main classes: I have the main class LocalDeploymentTool. This contains all the GUI - buttons, messages, menus etc... In this case, I added in this class also the actions for the buttons, although it can be good idea to separate them to different classes (view and controller).
However, you can see that each of the button have dedicated Action, and for most of the action, when it's require more than few lines of code, I moved the logic into class - DeploymentUtilities. In addition, constant and some other setup values are located in class - DeploymentParams.

2. Note, in the class DeploymentParams, there is setup regarding the different SF component types.
See map folder_SFType_Map. It hold per each type it SF API name and boolean indicating if this type contains meta-xml file in addition to the main file.
Also note that it doesn't contains all components types in SF. Other types, can be added, but there are some types in SF that require special logic.

3. When working with ZIP I'm using the ZipArchive library. For some cases I notice that when using library from java.util.zip I might get error during deployment.

4. I'm using 2 additional classes. First is InputDialog. It's actually stand alone class that can be used anywhere to get several user inputs. It is in used when I need to deploy and should setup the username/password/some other parameters.
Second class SyncWorker. It's only used to run the deploy process in background. Otherwise the app GUI will be locked during deployment.

5. The app uses several external jar files. 2 of them are generated from SF wsdl (partner.jar, metadata.jar), if you need to create them please refer to SF documentation.

6. The deployment logic is based on SF example. See documentation + example:
https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm
















Simple Text Comparison Tool

I know there are many such tool for such process, many of them online. My favorite site is DiffNow. Main issue that the site limits the number of comparison you are allowed to use per day. Other site/tool have some logic issues. I think if you will try to develop such tool, you might discover that it cannot be perfect. You will always have the special case that your logic doesn't parse perfectly.
I came to such need, and indeed find the difficulties in such process.
Main target of this article is to assist you with the design.
Finally you can test it with this link.

Design
At first you probably thinking lets start with the basic - loop over the lines per their numbers, if the line from the first file (file1) not exists in the second file (file2) then the line was removed, if it in the other position the line was added, if lines are different it was modified.

However, next you came to some other issues that need more than a the basic design:
Issue: If a new line was added at start, then it re-positioning all the other lines, and then you cannot compare the rest of the files line by line (otherwise it will find that all lines were modified).
Solution: Identify those cases in advance that lines removed/added and add "empty line" in the relevant file, in order to keep the lines in both files align as possible.

Issue: In many cases there are only differences in indentation (space, tab, etc) due to different editors. How your tool should handle those cases?
Solution: I decided that during comparing each 2 line I will check if lines are equals using trim function, in such case need to mark those line in special color.

Issue: In case line was modified, am I simply going to highlight this line or should I highlight the specific change (word(s)) ?
Solution/Decision: In my solution in case 2 lines found different, the program calculate number of different words. In case only 1 word is the different it mark only this words, but in other cases it mark the whole lines.

Screen
User will enter 2 text and press compare button. This will run the compare logic and show in output the 2 files that in each there are some marks for the changed lines. I decided to mark in red line that was exists at file1 but not exists in file2 (line was removed), green is the opposite (line was added), cyan indicate line that was changed, and grey will indicate "dummy" line that there just to keep the alignment between the 2 files.

Few Addition Points:
-Should handle the scroll functionality:
Add navigation buttons next to each file (I used "<<" and ">>"). To support this need to setup the Id attribute per each changed section in some specific format, later you can use JS code to navigate to each change according to the format you decided.
When user scroll in 1 file, the page should automatically scroll the other file, this way the user always see the same section in both files.

-Add lines numbers as user will probably want to know the lines number that was changed.
This part, obviously, need to be done after the compare logic, as you don't want that the lines number will be part of the compare.

-Summary details: it is a good idea to show some summary information (e.g. total lines that was removed, total added, etc), this means that during the compare logic need to count the changes.


Simple Compare Tool.



Causion with Describe Methods (Winter 18 Regression)

After Winter 18 release I encounter issue, that can demonstrate the risks when using describe methods improperly for dynamically looping over object fields.

Common usage for describe method can be as follow:


 Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe();  
 Schema.SObjectType OppSchema = schemaMap.get('Opportunity');  
 map<String, Schema.SObjectField> OppfieldMap = OppSchema.getDescribe().fields.getMap();  
   
 for(String field: OppfieldMap.keySet()){  
      //Do something  
 }  


The customer develop customize sync process between Quote & Opportunity.
His process uses describe method to get all the Quote and Opportunity fields and synchronize any field which have the same API name.
This process start causing errors or unexpected behaviour after winter 18 release.

After investigating the issue, we found that the main change that break the process is that salesforce introduce/expose in Quote object the field ownerId, which is populated by the quote creator and cannot be updated.
Generally, quote is related to Opportunity in Master-Detail relation. In such case, normally the child (quote) doesn't have owner. The owner of the parent (opportunity) is also the owner of child (quote).

But seems SF might break this normal behaviour for Quote object. It is not documented in the release note, but it is probably related to the pilot Quotes Without Opportunities Pilot - Winter '18, which contains below feature and make sense to have the field ownerId in Quote.
    1. Create new quotes without opportunities
    2. Update or remove opportunities from existing quotes
    3. Control access on old and new quotes
    4. Change owner on old and new quotes


Anyway, back to the customize sync process. After winter 18, when running the sync process, if trying to sync the fields from Opportunity to Quote the process was failing with exception, as it try to update the opportunity owner to the quote owner, and the field is not writable.
If trying to sync the other direction - Quote to opportunity - the process success, but it wasn't the expected result - opportunity owner was override with the quote owner (which is the quote creator and is not always the same person).

As fix, we exclude hard-coded the field ownerId from the process. It solve the issue for now.
For future, their process does need to be amended to sync only explicit list of fields.

Some insights I took from this case:
1.should be careful when using describe methods and working with the entire fields. Even when retrieving the fields dynamically, it's recommended to limits the result with other setup -e.g. fieldSet, Custom Settings.... Actually, in many case you might use only the former setup (fieldSet, Custom Settings) to control the fields you need, instead of going over all object fields.

2.When working with standard Object should be carefull when override standard functionality.
You should use dynamic process, which allow you to have changes in future without changing the code, but must take under consideration also SF changes for their standard.

3.Run full tests after the release changes are in sandbox. Recommended full sandbox with integration. It not 100% insurance, but might help to detect regression issue in advance.

Using Salesforce History Data

There is build-in functionality in Salesforce for tracking history on specific fields, which can save you development efforts.

Per each object you can enable the tracking history feature, and select the fields you would like track. In each page layout you may add the history related list, that will show all changes for this record. All this feature is build-in and can be done with setup only.

I had once issue, with main object that have several telated childs objects. When user view the history for the object he doesn't want to dive into each child record, but  rather to see all the changes (main object + childs) in one place.
Therefore I develop kind of history page which show all changes together.
At first this page was design for the specific custom object. Lately, with some modification it was amended to work completely generic. Means, it can work on any custom object.
Its show only changes for custom objects, as SF objects sometimes have different names for their history table.

The relevant components are visualforce page and controller, usage is by the URL:
/apex/HistoryPage?ids=<recordID>

The URL should get as parameter: ids. The Id of the record you want to view its history.

First should select the objects (master/child) to view their changes, press 'Get Changes' and view the result.

I created for example custom object: "Parent Object", and 2 child custom object.








Controller Code:


public class vf_HistoryPage {  

 public Id objID {get; set;}        //main object id (should get as URL parameter)   
 public String objName {get; set;}     //main object name (should get as URL parameter)   

 public List<HistoryChange> historyChangeLst {get; set;}     //list with all changes   

 public list<String> childObjLst {get; set;}                       //list of all child objects   
 public map<String, Boolean> obj_showBol_map {get; set;}               //for each object if need to show its changes   
 public map<String, String> objLabel_API_map {get; set;}               //map from Label to API   

 private map<String, String> objAPI_objLabel=new map<String, String>();     //for each object its label name   
 private map<String, String> objAPI_fieldName_map=new map<String, String>();   //for each object the field name of his parent   

 private map<String, Set<String>> tableFieldAdd_Map=new Map<String, Set<String>>();   //used to prevent from same change return twice   

 public vf_HistoryPage() {  

  //record ID should be as parameters in the URL   
  objID=ApexPages.currentPage().getParameters().get('ids');   
  objName=objID.getsobjecttype().getDescribe().getName();   

  //DescribeResult of the main object   
  Schema.DescribeSObjectResult objectResult = Schema.getGlobalDescribe().get(objName).getDescribe();   

  //intialize maps   
  objLabel_API_map=new map<String, String>();   
  childObjLst=new list<String>{objectResult.getLabel()};   
  obj_showBol_map=new map<String, Boolean>();   

  //adding the main object   
  obj_showBol_map.put(objName, true);   
  objAPI_objLabel.put(objName, objectResult.getLabel());   
  objLabel_API_map.put(objectResult.getLabel(), objName);   
  objAPI_fieldName_map.put(objName, 'id');   

  for(Schema.ChildRelationship child : objectResult.getChildRelationships()) {  

   Schema.DescribeSObjectResult objRes=child.getChildSObject().getDescribe();   

   if(objRes.isCustom()) {  
    childObjLst.add(objRes.getLabel());   

    obj_showBol_map.put(objRes.getName(), true);   
    objAPI_objLabel.put(objRes.getName(), objRes.getLabel());   
    objLabel_API_map.put(objRes.getLabel(), objRes.getName());   
    objAPI_fieldName_map.put(objRes.getName(), child.getField().getDescribe().getName());   
   }   
  }   
  historyChangeLst=new List<HistoryChange>();   
 }   

 //Get all the changes   
 public PageReference getChanges() {  

  //clear list of changes   
  historyChangeLst.clear();   

  for(String childObjName : obj_showBol_map.KeySet()) {  
   if(obj_showBol_map.get(childObjName)) {  

    //reset this list for the specific object   
    tableFieldAdd_Map.put(childObjName, new Set<String>());   
 
    //collect all the childs IDs   
    Set<ID> childObjIDSet=new Set<ID>();   

    for(sObject childObj : Database.query('SELECT id FROM ' + childObjName + ' WHERE ' + objAPI_fieldName_map.get(childObjName) + ' =\'' + objID +'\'')) {   
     childObjIDSet.add(childObj.id);   
    }  

    //add all the object changes to list of all changes      
    if(!childObjIDSet.isEmpty()) {  
     addAllChanges(childObjIDSet,    
      objAPI_objLabel.get(childObjName),   
      childObjName,   
      childObjName.endsWith('__c') ? childObjName.replace('__c', '__History') : childObjName + 'History');   
    }  
   }   
  }   
  historyChangeLst.sort();   

  return ApexPages.currentPage();   
 }   

 public void addAllChanges(   
  Set<Id> idLst,   
  String objectName,   
  String tableName,   
  String tableHistoryName) {  

  String idConcatenateLst='';   
  String dynamicSQLHistory;   

  for(String objID : idLst) {  
   idConcatenateLst+= idConcatenateLst!='' ? ', \''+objID+'\'' : '\''+objID+'\'';   
  }  

  dynamicSQLHistory='SELECT id, oldvalue, parentid, newvalue, field, createdDate, CreatedBy.LastName, CreatedBy.FirstName';    
  dynamicSQLHistory+=' FROM ' + tableHistoryName + ' WHERE parentId IN ( ' + idConcatenateLst + ')';   
  dynamicSQLHistory+= ' order by createdDate';   

  try {   
   List<sObject> objLst=Database.query(dynamicSQLHistory);   
   sObject userObj;   
   String firstName, lastName;   

   for(sObject histObj : objLst) {  

    userObj=histObj.getSObject('CreatedBy');   
    firstName=(String)(userObj.get('FirstName'));   
    lastName=(String)(userObj.get('LastName'));   

    addChange(tableName,   
     (String)histObj.get('field'),   
     histObj.get('oldvalue'),   
     histObj.get('newValue'),   
     (DateTime)histObj.get('createdDate'),   
     (firstName==null ? '' : (firstName + ' '))+ (lastName==null ? '' : lastName),   
     objectName,   
     (String)histObj.get('parentid'));   
   }   
  }   
  catch(System.QueryException sqe) {  
   System.debug('##err: ' + sqe);   //possible error: object not support history   
   System.debug('##DYNAMIC SQL: ' + dynamicSQLHistory);   
  }   
  catch(Exception e) {  
   ApexPages.addMessage(new ApexPages.Message(ApexPages.severity.ERROR, ' Error with ' + tableName + '. ' + e));   
  }   
 }   

 //add change   
 private void addChange(   
  String table_Str,   
  String field_Str,   
  Object oldValue_Obj,   
  Object newValue_Obj,   
  DateTime createDate,   
  String user,   
  String objectName,   
  String objID) {  

  String fieldLabel_Str, name_Str='';   
  String result_Str='';   
  Boolean creation=false;   

  if(!tableFieldAdd_Map.get(table_Str).contains(field_Str+createDate+objID)) {  

   tableFieldAdd_Map.get(table_Str).add(field_Str+createDate+objID);   
   if(field_Str=='created')   {   
    result_Str='Record created';   
    creation=true;   
   }   
   else   {   
    if(oldValue_Obj != null && newValue_Obj != null) {  
     result_Str+=' changed from ' + (oldValue_Obj==null ? '' : oldValue_Obj) + ' to ' + (newValue_Obj==null ? '' : newValue_Obj);   
    }  
    else if(oldValue_Obj == null && newValue_Obj != null) {  
     result_Str+=' added ' + (newValue_Obj==null ? '' : newValue_Obj);   
    }  
    else if(oldValue_Obj != null && newValue_Obj == null) {  
     result_Str+=' deleted ' + (oldValue_Obj==null ? '' : oldValue_Obj);   
    }  
    else {  
     result_Str+=' Field has been changed';   //for long text field SF doesn't store the values   
    }  
   }   

   if(result_Str!=null && result_Str.trim() !='') {  
    historyChangeLst.add(new HistoryChange(result_Str, user, createDate, objectName, objID, table_Str, field_Str=='RecordType' ? 'RecordTypeID' : field_Str, creation));   
   }  
  }   
 }   

 //Object HistoryChange - represent history record   
 public class HistoryChange implements Comparable {  

  public String changeStr {get; set;}   
  public DateTime changeDate {get; set;}   
  public String userStr {get; set;}   
  public String objectName {get; set;}   
  public String objectID {get; set;}   
  public String objectAPI {get; set;}   
  public String fieldAPI {get; set;}   
  public Boolean creationChange {get; set;}   

  public HistoryChange(String pChange, String pUser, DateTime pChangeDate, String pObjectName, String pObjectID,   
   String pObjectAPI, String pFieldAPI, Boolean pCreationChange) {  
   changeStr=pChange;   
   changeDate=pChangeDate;   
   userStr=pUser;   
   objectName=pObjectName;   
   objectID=pObjectID;   
   objectAPI=pObjectAPI;   
   fieldAPI=pFieldAPI;   
   creationChange=pCreationChange;   
  }   

  public Integer compareTo(Object historyRec) {   
   HistoryChange compareToHC = (HistoryChange)historyRec;   
   if(changeDate!=compareToHC.changeDate) {  
    return changeDate >= compareToHC.changeDate ? 0 : 1;   
   }  
   else {  
    return creationChange ? 1 : 0;   
   }  
  }   
 }   
}  


Visualforce Page:

<apex:page controller="vf_HistoryPage">   
 <apex:pageMessages />   
 <apex:form id="frm">   
  <apex:pageBlock mode="edit" id="mainblock" title="Object Selection">   
   <apex:pageBlockButtons id="buttons" location="bottom">   
    <apex:commandButton value="Get Changes" action="{!getChanges}"/>   
   </apex:pageBlockButtons>   
   <apex:pageBlockSection id="filters" columns="1" collapsible="true">   
    <apex:repeat value="{!childObjLst}" var="obj">   
     <apex:pageBlockSectionItem >   
      <apex:outputText value="{!obj}"/>   
      <apex:inputCheckbox value="{!obj_showBol_map[objLabel_API_map[obj]]}"/>   
     </apex:pageBlockSectionItem>   
    </apex:repeat>   
   </apex:pageBlockSection>   
  </apex:pageBlock>   
  
  <apex:pageBlock mode="edit" id="results">   
   <apex:pageBlockTable value="{!historyChangeLst}" var="change">   
    <apex:column headerValue="Date">   
     <apex:outputText value="{0,date,dd'/'MM'/'yyyy HH:mm:ss}">   
      <apex:param value="{!change.changeDate}" id="datefromid"/>   
     </apex:outputText>   
    </apex:column>   
    <apex:column headerValue="User">   
     <apex:outputText value="{!change.userStr}"/>   
    </apex:column>   
    <apex:column headerValue="Object">   
     <apex:outputLink target="_blank" value="/{!change.objectID}" id="eds"> {!change.objectName}</apex:outputLink>   
    </apex:column>   
    <apex:column headerValue="Change">   
     <apex:outputPanel rendered="{!NOT change.creationChange}">   
      {!$ObjectType[change.objectAPI].fields[change.fieldAPI].Label}   
     </apex:outputPanel>   
     {!change.changeStr}   
    </apex:column>   
   </apex:pageBlockTable>   
  </apex:pageBlock>   
 </apex:form>   
</apex:page>  

Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...