23

Suppose I am developing a managed package and I want to build some additional packages that can integrate to it. Let's call the main package Package A.

I want to create a separate package (Package B). When Package B is installed in an org where Package A is deployed, I'd like to build "awareness" into Pacakage A so that it knows that Package B is installed. This shouldn't be to difficult since I can do some describes and look for the SObjects in Package B to know if it's installed.

IMPORTANT NOTE: Package B should NOT require that Package A is installed. Package B can be used on its own without Package A.

Now the tricky (maybe impossible) part...

I'd like for Package B to expose an API for integration to Package A or any other application in the future. Ideally, Package A would be able to access the global classes to provide the integration. For example, maybe one of the triggers in Package A would call a static method from Package B like PackageBClass.doSomething();

Is there any way to do this? I thinks not. I can't build Package B references into Package A or it would create a dependency (extension). I also can't do the reverse for the same reason. They need to be two independent packages that can work together if need be. I looked at the new Apex Type Methods but this wouldn't work because there would have to be a dependency between the apps for a shared Interface.

The only thing that I can think of that might work is a third package that depends on both Package A and Package B that acts like a proxy. This is less than ideal though.

Any thoughts or suggestions?

6
  • 3
    Could you maybe use an existing Salesforce interface as a bi-directional entry point? For example, use Process.Plugin which implementors must implement an invoke and describe method? Less than ideal, but apart from your third package I wouldn't know a way. Commented Jan 7, 2013 at 16:34
  • 3
    I think Phil's right on this one, Kevin. Outside of using a proxy package, I think your best bet is to leverage Schedulable or Process.Plugin as a common interface. I think this would work pretty well, actually --- I've done dynamic instantiation of classes that implement Schedulable many times and it works great, and I bet Process.Plugin would work just as well. You're right about the System.Type methods not being sufficient yet --- until we have dynamic method calling (e.g. with System.Method.invoke(methodName)), you have to rely on a common interface being present.
    – zachelrath
    Commented Jan 7, 2013 at 17:14
  • Thanks, guys. This really helps. I might try the Process.Plugin route. Commented Jan 7, 2013 at 17:31
  • P.S. If one of you wants to write this up, I'll accept it. It's the best solution. Commented Jan 7, 2013 at 18:23
  • Ok, I'm almost done for the day...if @zachelrath hasn't done so by the morning (please feel free to do so), I will :) Commented Jan 7, 2013 at 18:33

2 Answers 2

15

I think there's really 3 approaches:

  1. COMMON 3RD PACKAGE: Include a common interface as part of a 3rd package that both Package A and B depend on. This way you can consistently dynamically instantiate instances of classes that implement that common interface and run "API logic" by calling methods that are common to all implementations of that interface from within either Package A or B.

  2. LEVERAGE NATIVE INTERFACES: You can also leverage built-in Apex interfaces such as Schedulable or Process.Plugin, which expose methods that all of your API classes can implement, providing a consistent API invocation structure.

  3. WILD AND CRAZY: As another possibility, here's an insane solution that avoids the need for extensions, dependencies, or a common interface. It's all based on storing your "API" within the constructors or properties of user-defined Apex classes. It is better expressed in code than in words:

    // Define your package 'API' in one or more classes
    
    global class MyPackageAPI {
    
    // Define /expose your API 'methods' in global subclasses
    // Here, our package 'API' defines 3 methods
    //
    // 1. insertAccount            - inserts a test account
    // 2. insertAccountWithName    - inserts an account with a given name
    //    PARAMS: accountName(String) - the name of the account to insert
    // 3. insertMultipleObjects      - inserts several objects passed as params
    //    PARAMS: account(JSON serialized Account SObject) - Account to insert
    //            contacts(JSON serialized List<Contact>) - List<Contact> to insert
    //            cases(JSON serialized List<Case>) - List<Case> to insert
    
    
    // Paradigm 1: when you don't need parameters to be passed,
    // you can execute your API methods right from a Method constructor
    global class InsertAccount() {
        // The constructor of your subclass defines the API method
        global InsertAccount(){
           // Run your logic in here.
           insert new Account(Name='ZachExecutableAccount');
        }
    }
    
    // Paradigm 2
    // when you DO need parameters to be passed in,
    // define properties on your subclass,
    // with any logic being executed as a 'side-effect' of the setters
    
    global class InsertAccountWithName() {
        // The properties of your method define the parameters and logic
        global String accountName {
           global get; global set {
               // Setters are passed an implicit value parameter
               // Which we'll use as part of our logic
               insert new Account(Name=value);
           } 
        }
    }
    
    // Paradigm 3: 
    // 
    // Pass in multiple arguments,
    // such as an account, contacts, and cases.
    global class InsertMultipleObjects {
       // Makes sure that we haven't run our logic yet
       private boolean runAlready;
       global String account {
          get; global set {
            runAlready = false; 
            account = value;
            InsertAll();
          }
       }
       global String contacts {
          get; global set {
            runAlready = false;
            contacts = value;
            InsertAll();
          }
       }
       public String cases {
          get; global set {
            runAlready = false;
            cases = value;
            InsertAll();
          }
       }
       private void InsertAll(){
         if (this.runAlready == false 
         && this.account != null 
         && this.contacts != null 
         && this.cases != null) {
            this.runAlready = true;
            // Convert our data from JSON into SObjects,
            // then insert
            System.Type accType = System.Type.forName('Account'); 
            Account a = (Account) JSON.deserialize(this.account,accType);
            insert a;
            List<Contact> people = (List<Contact>) JSON.deserialize(this.contacts,List<Contact>.class);
            for (Contact c : people) c.AccountId = a.Id;
            insert people;
            List<Case> casesList = (List<Case>) JSON.deserialize(this.cases,List<Case>.class);
            for (Case c : casesList) c.AccountId = a.Id;
            insert casesList;
        }
    }
    
      // **********************
      // UNIT TESTS
      // **********************
    
      // Exercise the InsertMultipleObjects() API method
      public static testMethod void TestInsertMultipleOBjects() {
        // Create some test data
        Account account = new Account(Name='UnitTestAccount');
        List<Contact> contacts = new List<Contact>{
        new Contact(FirstName='Johan',LastName='Strauss'),
        new Contact(FirstName='Sebastian',LastName='Bach')
        };
        List<Case> cases = new List<Case>{
        new Case(Subject='Broken Harpischord',Priority='Critical',Status='New'),
        new Case(Subject='Missing child',Priority='High',Status='New')
        };
    
        // Get a reference to our API method
        System.Type t = Type.forName('MyPackageAPI.InsertMultipleObjects');
        // "Call" our API Method, passing in some parameters
        Object o = JSON.deserialize('{'
            + '"account":'+JSON.serialize(account)+','
        + '"contacts":'+JSON.serialize(contacts)+','
        + '"cases":'+JSON.serialize(cases)+''
      +'}',t);
    
        // Verify that our API method worked
        System.assertEquals(1,[select count() from Account where Name = :account.Name]);
        System.assertEquals(2,[select count() from Contact where Account.Name = :account.Name]);
        System.assertEquals(2,[select count() from Case where Account.Name = :account.Name]);
       }
    }
    

SUMMARY

In essence, your API framework consists of two parts:

  1. API METHOD DISCOVERY - using System.Type, e.g.

    // Query for an API method
    System.Type apiMethod = System.Type.forName(namespace.apiClassName.apiMethodName);
    // Does the method exist?
    boolean apiMethodExists = apiMethod != null;
    
  2. API METHOD INVOCATION, using JSON.deserialize (if your method has parameters) or Type.newInstance (if your method does NOT need parameters):

    //
    // WITHOUT PARAMETERS
    //
    Object apiCall = apiMethod.newInstance();
    
    //
    // WITH PARAMETERS
    //
    
    // Run the method, if it exists
    Object apiCall = JSON.deserialize(
         '{"param1":"hello world","param2":"foobar"}',
         apiMethod
    );
    // Did the call succeed?
    boolean callSucceeded = apiCall != null;
    

To dynamically determine whether a given package's 'API' is exposed, you can also leverage SOQL queries on the ApexClass object, possibly with a predetermined naming convention:

List<ApexClass> apis = [
   select Name, NamespacePrefix 
   from ApexClass 
   where Name = 'MyPackageAPI'
];
4
  • ok...so by deserializing your making it call the relevant set on the object instance, got it! Commented Jan 7, 2013 at 19:02
  • Phil, exactly, I clarified the approach strategy at the bottom of the answer.
    – zachelrath
    Commented Jan 7, 2013 at 19:12
  • Whoa, option 3 is some serious mad-science! Love it. Reminds me what I did here...another JSON serialize/deserialize hack: github.com/kevinohara80/Force.com-Helper-Classes/blob/master/… Commented Jan 7, 2013 at 19:59
  • @zachelrath I've been playing with these approaches. I don't understand at all why the json deserialization approach works. I can't find any documentation that suggests that deserializing a class creates an instance of the class. Nor that the parameters are passed as you've demonstrated. I've also noticed that if a class has a constructor, the constructor is invoked with the newInstance() approach, but not with json deserialization. Very curious to understand this better.
    – R Roesler
    Commented Dec 26, 2019 at 1:20
4

I prototyped a solution using Process.Plugin - it worked fine. However I suggest that you test that the implementation doesn't affect the operation of the Flow designer.

I considered a couple of other options at that time:

  1. Create a "message" sobject in one package and use dynamic DML to post records to it from another package. Use a trigger to capture the message and act upon it. You could deserialise the content of a long-text field and use this as the basis for the action. You could pass back a response on the same record, and ultimately flag it as "complete" so the record can be recycled

  2. Use an http callout from one package to invoke an API in the second package. I had some concerns over the limited payload size, and the response time


I have been working on a plugin proxy solution, though now I come to add that to my answer I see that this was actually suggested in the closing part of the original question; doh! I will link back here.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .