Working with Salesforce Content in APEX

Tim Broyles Development Technologies, Tutorial 5 Comments

Attention: The following article was published over 10 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

As you know, if you are an APEX developer, having direct access to content via ContentDocument or ContentVersion in a related list or a ‘Lookup Relationship’ is not possible in the current version of Salesforce (Winter ’14). Nevertheless, there are projects that need to expose content in a Web Service, or a visual force page to provide access to the documents in the Salesforce content libraries. How does one bridge the gap to provide a programmatic solution to the Content problem?

The Stunt Man Solution

The short answer is we need a stand-in. You know, a stunt man that does all of the hard work for the star. In this case, the star is content. For whatever reason, Salesforce content is not available to do look-ups and related lists. Perhaps it’s a union issue? I have no idea. So in steps the proxy ‘stuntman’ to save the day!

The proxy needs to mirror all of the actions of the star, taking on the complete persona. To make this happen he is going to need the same lifecycle as the content document. Preferably an automated solution that is intrinsic to Salesforce that can mirror the creation, update, and delete lifespan of the CRM Content Document.

Create the Proxy object

  1. The first step is to create the object that will ‘stand-in’ for the content object. I will call the object DocumentProxy.
  2. Create a custom field for the ContentDocumentId as a text field.
    — Check the “Do not allow duplicate values” box.
  3. Create a custom field for the ContentVersionId as a text field.
    — Optionally create a custom field in the “Salesforce CRM Content“. You can find this under Build – Customize – Salesforce CRM Content – Fields.
  4. Create a new Lookup Relationship called DocumentProxy and save. This is a key point to understand. The reason we have a proxy object to begin with is that we cannot do a lookup on content. This figure below shows the relationship you just created to content, where the DocumentConsumer would act as the related object for this relationship.

Working with content (1)

Trigger Happy

The object has been created, however there is no provision for the lifecycle yet in the workspace. The next step is to create the triggers that will enforce the creation, update and deletion of your new DocumentProxy.

Working with content (2)
If you have created a Salesforce project in Eclipse you can skip this part. If you are not using Eclipse and the Salesforce plugin I highly recommend that you install it now. All of the information you need you can find here at wiki.developerforce.com.

Now create a new Force.com project, you will need your Security Token and password. One gotcha is to select the correct Environment. Developer Edition and Sandbox are not the same, and if you select the wrong one, you will not be able to log-in and create your project.

Lifecycle – Create Trigger

In the Eclipse Force.com project, right-click on triggers. Next, in the context menu, hover over Force.com and select New Apex Trigger. Give the trigger a meaningful name like ContentNewInstance, select content in the Object: dropdown and select After insert in the Apex Trigger Operations. This will in effect create a call to this trigger each time a piece of content is created. It is bad practice to place your trigger code in here, so what we will do next is create a worker object (ContentLifecycleTriggerHandler) that will do the lifecycle work for us.

trigger ContentNewInstanceTrigger on ContentVersion (after insert){
   if(Trigger.isAfter){
      if(Trigger.isInsert){
         ContentLifecycleTriggerHandler.onAfterInsert(Trigger.new);
      }
   }
}

To create the ContentLifecycleTriggerHandler, right click on the classes tab in your project and hover over force.com and select New Apex Class and give it the name ContentLifecycleTriggerHandler.

In this new class create a public method that accepts a ContentVersion List, why a list? Always write your APEX code assuming bulk data is going to be processed. This can be a topic for another time, but let’s say there are 5000 documents being migrated into the system, you do not want to process them one at time as this would create extra overhead. Salesforce will save up and send them in batches. Salesforce is a multi-tenant cloud hosted system, so a little extra overhead can multiply very quickly.

Below is what we have so far in this object. This pattern is a good model to follow. It allows you to distribute control with public methods on your triggers handlers and also makes the code easier to follow.

public with sharing class ContentLifecycleTriggerHandler {

   public static void onAfterInsert(List newList){
      createLifecycleDocument(newList);
   }
}

The createLifecycleDocument method takes a list of ContentVersion. Each member of the list is a newly created document on Force.com. We need to create a new object DocumentProxy__c for each member of the list and assign the ContentDocumentID and ContentVersionId to the proxy object.

private static void createLifecycleDocument(List newList){
   List<DocumentProxy__c> documentProxyList = new List< DocumentProxy __c>();
   for(ContentVersion cv :newList){
      DocumentProxy __c doc = new DocumentProxy __c();
         doc.ContentVersionId__c = cv.Id;
         doc.ContentDocumentId__c = cv.ContentDocumentId;
         doc.Name = cv.Title + '.' + cv.FileType.ToLowerCase();
         documentProxyList.add(doc);
      }
   insert documentProxyList;
}

Lifecycle – Update Trigger

The way content is handled in Salesforce has been a learning process for me. One piece of information I needed to understand the content implementation I did not find until very late in my design process. It is simple page with object relationships that clearly shows 1 ContentDocument has Many ContentVersion objects. You will notice that the create trigger is on a ContentVersion object. If you keep this implementation, you will be challenged with exceptions each time a new version is created. If you choose to change it, which I recommend, the change will become clear in this section.

Working with content (3)

The update trigger is created on ContentDocument, not ContentVersion. Why? If a piece of content is changed and the version is incremented the onUpdate trigger will not run on ContentVersion, the onUpdate trigger will run on ContentDocument and the onCreate trigger will run on ContentVersion. This is where the unique flag becomes so important on the DocumentProxy, if you want a one to one relationship between the latest version of the content and the DocumentProxy.

Create a trigger called ProxyDocumentLifecycleManagment and select ContentDocument in the Object drop down. Using the same pattern we used for the last trigger the FulfillmentContentTriggerHandler will do the bulk of the work.

trigger ProxyDocumentLifecycleMaintenance on ContentDocument (after update) {

   if(Trigger.isAfter){
      if(Trigger.isUpdate){
         ContentLifecycleTriggerHandler.onAfterUpdate(Trigger.new);
      }
   }
}

In the handler create the public router for your method. This comes in handy when you need to handle reentry on a trigger. You can easily test if your method is already in use and make a decision whether or not to do work on this object. For example:

public static void onAfterupdate(List newList){
   if (!IsProcessing.isProcessing){
      IsProcessing.isProcessing = true;
      updateFulfillmentContent(newList);
   }
}

When you update the DocumentProxy do the following:

  1. Retrieve the DocumentProxy from the DB
  2. Replace the ContentVersonId on the DocumentProxy with the LatestPublishedVersionId on the ContentDocument
  3. Update the DocumentProxy in the DB
private static void updateFulfillmentContent(List newList){

   List< DocumentProxy __c> documentProxyList = new List< DocumentProxy __c>();
   for (ContentDocument cd : newList){
      if (cd != null){
         List<DocumentProxy__c> documentProxy = findDocumentProxyWithId(cd.Id);
         if (documentProxy != null && documentProxy.Size() > 0){
            DocumentProxy__c doc = documentProxy [0];
            doc.Name = cv.Title + '.' + cv.FileType.ToLowerCase();

            doc.ContentVersionID__c = cv.LatestPublishedVersionId;
               documentProxy List.add(doc);
         }
      }
   }
   if (documentProxyList.size() > 0)
      upsert documentProxyList;
}

Lifecycle – Delete Trigger

To the existing ProxyDocumentLifecycleMaintenance add the before delete keywords. Note there is no new object in this instance, trying to delete Trigger.new is futile, and believe me I have tried.

trigger ProxyDocumentLifecycleMaintenance on ContentDocument (before delete, after update) {

  if(Trigger.isBefore){
      if(Trigger.isDelete){
         ContentLifecycleTriggerHandler.onBeforeDelete(Trigger.old);
      }
   }
   if(Trigger.isAfter){
      if(Trigger.isUpdate){
         ContentLifecycleTriggerHandler.onAfterUpdate(Trigger.new);
      }
   }
}

The final touch is the delete method. We will add this to our now-familiar ContentLifecycleTriggerHandler.

private static void deleteFulfillmentDocument(List deleteList){
   if (deleteList == null) return;
   List <DocumentProxy__c> pDocList = new List<DocumentProxy__c >();
   DocumentProxy__c pDoc;

      for(ContentDocument cd: deleteList)
      {
         pDocList = [select Id from DocumentProxy__c where ContentDocumentId__c = :cd.Id];
         if (pDocList.size() > 0)
            delete pDocList;
      }
}

Final Considerations

I mentioned in the update trigger section that the create method needs to be referenced to the ContentDocument. I leave this exercise in your capable hands, it should be a rather simple exercise to add this to the existing ContentDocument trigger and the lifecycle object.

One last thing, I have adapted this from production code and have, as they say in dragnet, ‘Changed the names to protect the innocent’. If I caused some red squiggles I apologize in advance.

Happy Coding!

— Tim Broyles, [email protected]

Reference Questions

I’m hoping that this blog post answers a couple of the questions I have seen prevalent online, mainly:

0 0 votes
Article Rating
Subscribe
Notify of
guest

5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments