Generic PDF Document

In many cases I was needed to create PDF document per requirement. Thought it will be great to have a generic code that can generate such document by setup.

I come up with such page that fill major parts of common requirements.
This post present this process and is mainly good for learning regarding generic code and it's advantages.

It contain the following components:
1.Apex Page: GenericDocument
2.Controller: vf_GenericDocument
3.3 Custom object:
Generic Document -Main detail regarding the document
Generic Document Table -(M/D with Generic Document) Objects that related to the document
Generic Document Element-(M/D with Generic Document) Element inside the document
4.Setup that define the relevant document.


The development allow the following functionalists:
1.Creating pdf document only by setup.
2.Adding footer/image/margin to the layout.
3.Use data from specific table + its child table(s).
4.Filter specific child records.
5.Add hard-coded section to the layout with rich text editor, which can contain also format text/images/so...
6.Positioning each component in the layout by setup.

What can be done to enhance it? Plenty....Ordering the data in the related child table, formatting this text, formatting each component in the document, roll-up summary fields, and so on...



The usage is by URL below, Where doid is the id of the generic document, recid is the id of the record you want to display.
/apex/GenericDocument?doid=a00w000000VqJY2&recid=001w0000019yccM


My example is with standard object of SF: Account, but it can be used to any custom object as well.

I defined GenericDocument record 'Customer Detail'.



1.Top data define general properties regarding the document: margins, style format, logo/footer images (should be added to Static Resources).

2.First related list - Generic Document Tables - define the objects that being used by the document.
In this example I have 2 objects: Account - main object, Contacts - child object.
Note, the field type: which make the difference.

3.Second related list - Generic Document Elements - define the components inside the document, which can be field from the relevant tables, hard-coded label, formatted text.
For example, first is 'Document Title', which is hard-coded text to be display in specific position.

Another example for Document Element is 'Customer Name', which show the name field from the Account.
As can be notice, in case the Type is 'Field', the fields 'Related To' and 'Source' take place, and mark the object and the field.


The code contain page and controller.

Note lines 13-17, get 5 optional parameters from URL. such parameters can be used to filter dynamically related child list (or they can used to other purposes as well).
For example, if I would like to query only contact where the field 'Level' is having specific value, I would assign in the URL:
/apex/GenericDocument?doid=a00w000000VqJY2&recid=001w0000019yccM&p1=<value>


In the setup:

Of course, the 'Where Filter' field can be also used to filter fixed value.



public class vf_GenericDocument {  

 public Generic_Document__c genericDoc {get; set;}  
 public list<CompObj> mainObjLst {get; set;}    //components related to main objects  
 public map<string, map<string, list<CompObj>>> msoc_compLst {get; set;}  //table --> recordId --> list of fields  
 public string imageLogo {get; set;}  
 public string imageFooter {get; set;}  

 public vf_GenericDocument(ApexPages.StandardController controller) {  
 
  string genericDocId = ApexPages.currentPage().getParameters().get('doid');  
  string objID = ApexPages.currentPage().getParameters().get('recid');  

  //upto 5 dynamic parameters from URL      
  string param1 = ApexPages.currentPage().getParameters().get('p1');  
  string param2 = ApexPages.currentPage().getParameters().get('p2');  
  string param3 = ApexPages.currentPage().getParameters().get('p3');  
  string param4 = ApexPages.currentPage().getParameters().get('p4');  
  string param5 = ApexPages.currentPage().getParameters().get('p5');              
  
  string mainObject;  
  map<string, sobject> mso_flatObj=new map<string, sobject>();  
  
  genericDoc = [ select id, Name, Logo__c, Logo_Height__c, Logo_Width__c, Footer__c, Footer_Height__c, Footer_Width__c, Add_Data_Top__c, Margins__c, Text_Format__c,  
      (select Name, Top__c, Left__c, Width__c, Height__c, Div_Name__c, Source__c, Type__c, Related_To__c, Rich_Text__c from Generic_Document_Elements__r),  
      (select name, API_Name__c, Type__c, Relation_Name__c, where_Filter__c from Generic_Document_Tables__r)  
      from Generic_Document__c where id =:genericDocId];  
      
  imageLogo = genericDoc.Logo__c!=null ? '/resource/' + genericDoc.Logo__c : '';  
  imageFooter = genericDoc.Footer__c!=null ? '/resource/' + genericDoc.Footer__c : '';  
  
  mainObjLst=new list<CompObj>();  
  map<string, string> mss_tableField=new map<string, string>();  
  map<string, Generic_Document_Table__c> mso_table=new map<string, Generic_Document_Table__c>();  
  
  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   mss_tableField.put(t.name, 'select id,');  
   mso_table.put(t.name, t);  
  }  
  
  String sQuery;  
  for (Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r) {  
   if(compRec.Type__c=='Field') {
    mss_tableField.put(comprec.Related_To__c, mss_tableField.get(comprec.Related_To__c) + compRec.source__c + ',');  
   }
  }  

  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   if(t.Type__c=='Master'){  
    sQuery = mss_tableField.get(t.name).subString(0, mss_tableField.get(t.name).length()-1);  
    mainObject=t.name;  
    for(Generic_Document_Table__c t2: mso_table.values()){  
     if(t2.Type__c=='Child') {
      sQuery += ',(' + mss_tableField.get(t2.name).subString(0, mss_tableField.get(t2.name).length()-1) + ' from ' + t2.Relation_Name__c + (t2.where_Filter__c != null ? ' where ' + t2.where_Filter__c : '') + ')';  
     }
    }  
    sQuery += ' from ' + t.API_Name__c + ' where ID = :objID';  
   }  
   else if(t.Type__c=='Flat'){  
    mso_flatObj.put(t.Name,   
     database.Query(mss_tableField.get(t.name).subString(0, mss_tableField.get(t.name).length()-1) + ' from ' + t.API_Name__c + ' where ' + t.Relation_Name__c + ' =:objID limit 1'));  
   }  
  }  
  system.debug('SOQL%%% ' + sQuery);  
  sobject mainObj = database.Query(sQuery);  
  
  for(Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r){  
   if(comprec.Related_To__c==mainObject)  {
    mainObjLst.add(new CompObj(compRec.Type__c=='Field' ? String.valueOf(mainObj.get(compRec.source__c)) : compRec.source__c, compRec));  
   }
   else if(mso_table.get(compRec.Related_To__c).Type__c=='Flat') {
    mainObjLst.add(new CompObj(String.valueOf(mso_flatObj.get(comprec.Related_To__c).get(compRec.source__c)), compRec));  
   }
  }  
  
  msoc_compLst=new map<string, map<string, list<CompObj>>>();   
  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   if(t.type__c=='Child'){   
    msoc_compLst.put(t.Name, new map<string, list<CompObj>>());  
    //loop over all child records  
    for(sObject obj : (list<sObject>) mainObj.getSObjects(t.Relation_Name__c)){  
     msoc_compLst.get(t.Name).put(obj.id, new list<CompObj>());  
     for(Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r) {
      if(comprec.Related_To__c==t.name)  {
       msoc_compLst.get(t.Name).get(obj.id).add(new CompObj(compRec.Type__c=='Field' ? String.valueOf(obj.get(compRec.source__c)) : compRec.source__c, compRec));  
      }
     }
    }  
   }  
  }  
 }  

 public class CompObj {  
  public string value {get; set;}  
  public Generic_Document_Element__c comp {get; set;}  
  
  public CompObj(String vValue, Generic_Document_Element__c vComp) {  
   value=vValue;  
   comp=vComp;  
  }  
 }  
}  


The code in the page:

<apex:page standardController="Generic_Document__c" extensions="vf_GenericDocument" renderAs="PDF" applyBodyTag="false">  
 <head>     
  <style type="text/css">  
   body {  
    {!genericDoc.Text_Format__c}  
   }  
   @page {  
    size: A4;  
    margin: {!genericDoc.Margins__c}  
    position: relative;  
    top: 0mm;  
    left: 0mm;  
    width: 210mm;  
    height: 297mm;  
    page-break-after: avoid;  
    @top-left{  
     content : element(header);  
    }  
    @bottom-right{  
     content : element(footer);  
    }  
   }  
   div.header {  
    position : running(header);    
   }  
   div.footer{  
    position : running(footer);    
   }  
   /* First Page*/  
   div.page1_data {  
    position: absolute;  
    top: 0mm;  
    left: 2mm;  
    width: 210mm;  
    height: 190mm;  
   }   
   <apex:repeat value="{!mainObjLst}" var="param" id="cssRepeat">  
   div.{!param.comp.Div_Name__c}{  
    position: absolute;  
    top: {!param.comp.Top__c}  
    left: {!param.comp.Left__c}  
    width: {!param.comp.Width__c}  
    height: {!param.comp.Height__c}   
   }  
   </apex:repeat>  
   div.add_data{  
    position: absolute;  
    top: {!genericDoc.Add_Data_Top__c}  
    left: 2mm;  
    width: 210mn;  
   }  
  </style>    
 </head>  
 <body>  
  <div class="page">  
   <div class="page1_data">  
    <div class="header">  
     <apex:image url="{!imageLogo}" width="{!FLOOR(genericDoc.Logo_Width__c)}" height="{!FLOOR(genericDoc.Logo_Height__c)}" rendered="{!imageLogo!=''}"/>  
    </div>  
    <apex:repeat value="{!mainObjLst}" var="param" id="dataRepeat">  
     <div class="{!param.comp.Div_Name__c}">  
      <!--  <apex:outputText value="{!param.value}" rendered="{!not IsNull(param.value)}"/>-->  
      <apex:outputText value="{!SUBSTITUTE(JSENCODE(param.value), '\r\n', '<br/>')}" rendered="{!not IsNull(param.value)}" escape="false"/>  
      <apex:outputField value="{!param.comp.Rich_Text__c}" rendered="{!IsNull(param.value)}" />  
     </div>  
    </apex:repeat>  
    <div class="add_data">  
     <apex:repeat value="{!msoc_compLst}" var="tableO" id="tableReapet">  
      <br/><br/>  
      <u>{!tableO}</u>  
      <table width="700px">   
       <apex:repeat value="{!msoc_compLst[tableO]}" var="tableI" id="outer1">  
        <tr>  
         <apex:repeat value="{!msoc_compLst[tableO][tableI]}" var="I" id="outer2">  
          <td width="{!I.comp.width__c}"><apex:outputText escape="false">{!I.value} </apex:outputText></td>  
         </apex:repeat>  
        </tr>  
       </apex:repeat>  
      </table>  
     </apex:repeat>  
    </div>  
    <div class="footer">  
     <center><apex:image url="{!imageFooter }" height="{!FLOOR(genericDoc.Footer_Height__c)}" width="{!FLOOR(genericDoc.Footer_Width__c)}" rendered="{!imageFooter!=''}"/></center>  
    </div>   
   </div>  
  </div>  
 </body>   
</apex:page> 



And finally the output pdf of this example:





Posting to Chatter with Apex Code

Haven't wrote here for some time...Hope you can find the post interesting.

I discover  recently the option to post into chatter with apex code. Some might think it's complicated or aren't familiar with it at all so they aren't using it, however after getting familiar you will see it's quite easy and can be very useful.

I wrote 2 generic functions that post to chatter. Find them useful for several cases.
First method using the ConnectAPI, second without, and I'll explain later the reason for needed
both.

1.ConnectAPI
The ConnectAPI provide some build-in functions and objects to post in chatter.
You can read the SF Documentation regarding, but wait! You can easily get lost in too much information and too many examples, which mostly are not relevant. Advise, is to start with the code below.
Later search specifically classes according to your needs (can google it by 'salesforce + <API Object name>').


public static void CreatePostAttach(  
  Id recordID,   
  string s_FileURL,   
  string s_fileType,   
  string s_fileName,   
  string s_msg,   
  list<string> ls_mentionUser)  {  

 ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput();  
 ConnectApi.BinaryInput feedBinary;  
 ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();  
 messageInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();  
 ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput();  
 textSegment.text = s_msg;  
 messageInput.messageSegments.add(textSegment);   

 for(string userId : ls_mentionUser){  
  ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput();  
  mentionSegment.id = userId;  
  messageInput.messageSegments.add(mentionSegment);  
 }  
 input.body = messageInput;  
 
 if(s_fileName!=null){  
  PageReference p = new PageReference(s_FileUrl);  
  p.setRedirect(true);  
  blob body=p.getContent();  
  ConnectApi.NewFileAttachmentInput fileIn = new ConnectApi.NewFileAttachmentInput();  
  fileIn.title = s_fileName + '.' + s_fileType;  
  input.attachment = fileIn;  
  feedBinary = new ConnectApi.BinaryInput(body, s_fileType, s_fileName + '.' + s_fileType);  
 }  
 ConnectApi.FeedItem feedItemRep = ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.Record, recordID, input, s_fileName!=null ? feedBinary : null);  
}  


This method get 6 parameters:
1.The record Id, to post under its chatter.
2.Attachment URL (optional) - in case you want to add attachment with the post.
3.Attachment Type - in case you provide the Attachment URL, you should provide also the file type.
4.Attachment Name
5.Text Message - The content of the post.
6.list of users to Mention - who should be mentioned at the end of the message.

Assume the rest of the code it's quite self explain. The main object is the FeedItemInput, we add to it MessageBodyInput for the text, MentionSegmentInput for users mentioning.
In addition using the BinaryInput for the attachment.

Example (Result shown in the image at the bottom):
CreatePostAttach(
        '001w0000019yccM',
        '/apex/AccountPage?id=001w0000019yccM',
        'pdf',
        'AccountRep.pdf',
        'Hi..I added this Pdf report.\n\n Visit:\n http://lc169.blogspot.co.il/\n\n',
        new list<string>{'005w0000003Yf6L', '005w0000003Z6Zm'});

-Note: AccountPage is simple visual page render as Pdf that I created.


2.Insert FeedItem
We can do almost the same action without the ConnectAPI- but create and insert new FeedItem.
Which is the second method.
Why I needed it? When using ConnectAPI the submitter of the post is always the current user.
Let's assume you want to post it with admin user, perhaps when critical error occur then it cannot be done with ConnectAPI. You can do it by creating the FeedItem yourself.
The drawback with this method is that you cannot use the mention part.


public static void CreatePostAttachUser(  
  Id recordID,      //Related record ID  
  string s_FileUrl,    //URL to generate attachment  
  string s_postType,     
  string s_fileName,   //File Name  
  string s_msg,      //Message content  
  string createUser){   //User that create. If null then it'll current user     
 
 FeedItem post = new FeedItem();  
 post.ParentId = recordID;  
 post.createdById = createUser;  
 post.Body = s_msg;  
 post.type = s_postType;  
 
 if(s_fileName != null){  
  PageReference p = new PageReference(s_FileUrl);  
  p.setRedirect(true);  
  blob body=p.getContent();  
  post.ContentData = body;  
  post.ContentFileName = s_fileName;     
 }  
 insert post;  
}  

This method also get 6 parameters:
1.Record ID to post under.
2.Attachment URL (optional)
3.Post type - should match one of the picklist value from the Feed Types. In case it post with attachment value should be- 'ContentPost'
4.Attachment Name
5.Text Message
6.User Id of the submitter

Example:
CreatePostAttachUser(
        '001w0000019yccM',
        '/apex/AccountPage?id=001w0000019yccM',
        'ContentPost',
        'AccountRep.pdf',
        'Hi..I added this Pdf report.\n\n Visit:\n http://lc169.blogspot.co.il/\n\n',
        '005w0000003Z6Zm');







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...