Why I Wrote the Salesforce Chatter for Twitter Demo

March 18th, 2010

Yesterday I posted a video and demo of a Chatter app running on Google App Engine integrated with Twitter. While I got a lot of positive feedback from other Salesforce.com employees, Dave Carroll gave me the proverbial beat-down on our Appirio Tech Blog. Everything he said was 100% correct. The app could have easily been developed on Force.com if my only intention was to develop a Twitter app. It was not. I used Twitter because everyone loves Twitter and that made the video nice and easy to understand. I’ve also been doing a lot of OAuth lately and have been working with the Twitter4J API so it was code that I had laying around. I wrote the demo on Google App Engine since roughly 30% of my blog traffic is related to App Engine and GWT. By using Twitter, OAuth and App Engine I was hoping to catch the eye of people that normally don’t develop on the Force.com platform and might not see the super-cool possibilities of Chatter.

Perhaps I didn’t explain myself very well so I’ll try and redeem myself (wish me luck!). One of my favorite things about Chatter is that external apps can send chatter about projects, people, opportunities, etc as well. If you saw our Social PS Enterprise demo at DF09, you should remember how a Google spreadsheet sent Chatter for a project when the hours reported was updated. I hadn’t seen much talk about external app sending Chatter so that’s why I thought it might be useful and could spark ideas. Here are some ideas where I think my code might come in useful:

  • You have some external apps running on Google App Engine or some other Java stack that you want to have send Chatter when an event occurs.
  • You have some type of Salesforce.com integration that runs externally on a hosted server or EC2 that processes records and attaches the resulting file Chatter for the application.
  • You wrote a Bulk API Java app that runs on a scheduled basis and send Chatter as to the number of records processed.
  • We spin up a Google Site each time we start a new project. Sometimes we have resources that do not have access to our Production org, so it would be nice for them to submit Chatter for the project from the Google Site.

Hopefully the code and/or idea in my blog post will be useful to someone else. Let me know what you think?


  • Share/Bookmark

Categories: Salesforce

9 Comments

Integrate Chatter & Twitter on Google App Engine using OAuth

March 17th, 2010

Cross-posted at the Appirio Tech Blog.

At Appirio we’ve been excited about Salesforce Chatter for quite a while. We firmly believe that Chatter has the potential to bridge the gap between enterprise applications and the way people work. We were luckily enough to receive special prerelease access to Chatter to develop our Social PS Enterprise for the Dreamforce ‘09 Chatter Keynote and if you missed the demo at Dreamforce ‘09 you can find it here.

Chatter is now in private beta for 100 companies and it is enabled in our production org. We’ve been using it for couple of weeks now and I find myself logging into our org more and more to check the status of other employees, projects and opportunities. As a developer I really wanted to get my hands on the code and test drive Chatter’s functionality. Luckily Quinton Wall has a great Intro to Chatter on developer.force.com to get me started. Sure, I could have developed an Apex and Visualforce application for Chatter but I naturally wanted to integrate Chatter with Twitter. So what I came up with is a Chatter/Twitter app running on Google App Engine using OAuth for Twitter authentication.

Understanding Chatter

Initially I was under the assumption that Salesforce.com would release some sort of API for Chatter. However, they’ve done something even better. Instead of a new API to learn, Salesforce.com exposed Chatter as a series of sObjects allowing you to query for records using the same SOQL that you know and love and manipulate records using DML. Once you get a grip on the Chatter object model and where data lives, developing applications for Chatter is essentially the same as using the Sales or Service Could.


The Chatter model is based upon familiar social networking “Feed Posts”. These posts are made up of a series of Feed Items and Feed Types. The FeedPost stores most of the information that you are concerned about such as the body, title and any content related data. The FeedPost object also contains the information for all posts for the User object including profile statuses, news feeds and entity updates (accounts, contacts or custom objects). The Feed Types are dependent on what actions you are performing:

  • UserStatus -- this is the user status update (e.g., “What are you working on?”)
  • TextPost -- a post you make from a record
  • LinkPost -- a post that contains a URL link (when you click on the link icon)
  • ContentPost -- a post that contains some type of uploaded content such as a document or graphic
  • TrackedChanges -- whenever a field on a record (set up during Chatter Feed Tracking configuration) is updated

One thing to understand from the beginning is that you do not query for Feed Posts directly. You must query via the Feed Item which contains a reference to the details of the post. So to get the last status update for the current user, you would issue the following SOQL:

SELECT Id, FeedPost.Body FROM UserFeed WHERE ParentId = :Userinfo.getUserId()
And Type = ‘UserStatus’ ORDER BY CreatedDate DESC LIMIT 1

For more sample Chatter code, check out the Chatter Code Recipes.

Functional Design

From a high-level overview, the application is fairly simple. When it initially loads the user is prompted to log into Twitter using OAuth.


Twitter asks you to grant the App Engine application the ability to access and update your Twitter account. I’m currently working on OAuth for Salesforce.com and hope to have both sides of the application using OAuth soon. Currently my Salesforce.com sandbox credentials are hard-coded in the application.


Once you authorize access you are redirected back to the application on Google App Engine and presented the following options:

  • Send your latest tweet to Chatter -- fetches your last tweet from your timeline and sends it to Chatter as a status update.
  • Tweet your latest Chatter status update -- queries for you last Chatter update and tweets it. Since Chatter is designed to be private within your org this option isn’t recommended for production and I only implemented it for academic purposes.
  • Send a status update to both Chatter and Twitter -- presents you with a simple form to enter your status update. Once the form is submitted, your status is sent to both Chatter and Twitter.


Technical Design

The application is developed on Google App Engine using the Force.com Web Service Connector (WSC), Salesfore.com Partner library, and the Twitter4j Java library. Since we are using Google App Engine, download the wsc-gae-16_0.jar and partner-library.jar Jars from the WSC project. I used Chatter on one of our sandboxes so I had to do a little tweaking to get the Partner jar running. Now create a new Web Application Project for App Engine and then drop your two jars and the twitter4j jar into the lib directory. You’ll also need to add them to your project’s build path in Eclipse.

Next you’ll have to register your app with Twitter. This will give you the consumer key, consumer secret and URLs you’ll need to authenticate and make requests to Twitter. I’m storing these credentials along with the Salesforce.com sandbox credentials and user id as static variables in a simple credentials class for ease of use.

The application is a series of JSPs and Servlets and if you’d like the code for the entire project, send me a message. The interesting parts of the application are described below and hopefully you can extrapolate the rest.

LoginServlet

This is the initial request for the application. The code uses the Twitter credentials and gets the authorization URL for the app and presents it to the users in the JSP page. The user clicks this link and is taken to Twitter to authorize the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.jeffdouglas;
 
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.RequestToken;
 
public class LoginServlet extends HttpServlet {
 
  private static final Logger log = Logger.getLogger(LoginServlet.class);
 
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
 
    HttpSession session = req.getSession();
    Twitter twitter = new TwitterFactory().getInstance();
    twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,Credentials.TWITTER_CONSUMERSECRET);
    RequestToken requestToken = null;
 
    try {
      requestToken = twitter.getOAuthRequestToken();
    } catch (TwitterException e) {
      log.error(e.toString());
    }
 
    // get the token and tokenSecret
    String token = (String)requestToken.getToken();
    String tokenSecret = (String)requestToken.getTokenSecret();
    // store the token and tokenSecret in the session
    session.setAttribute("token", token);
    session.setAttribute("tokenSecret", tokenSecret);
 
    // get the url that the user must click to authenticate w/OAuth
    String authUrl = requestToken.getAuthorizationURL();
    req.setAttribute("authUrl", authUrl);
    RequestDispatcher rd = req.getRequestDispatcher("login.jsp");
 
    try {
      rd.forward(req, resp);
    } catch (ServletException e) {
      log.error(e.toString());
    }
 
  }
}

SendChatterServlet

This Servlet runs when the user clicks the Twitter -> Chatter link. The code grabs the user’s last tweet and the uses the Partner Web Services API to submit the sObject with the new Chatter status to Salesforce.com.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.jeffdouglas;
 
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;
 
import com.sforce.ws.*;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.sobject.SObject;
 
public class SendChatterServlet extends HttpServlet {
 
  private static final Logger log = Logger.getLogger(SendTweetServlet.class);
 
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
  throws IOException {
 
    PartnerConnection connection = null;
    // get the user's last tweet
    String tweet = getLastTweet(req,resp);
 
    if (tweet != null) {
 
      try {
        if (connection == null) {
          ConnectorConfig config = new ConnectorConfig();
          config.setUsername(Credentials.SFDC_USERNAME);
          config.setPassword(Credentials.SFDC_PASSWORD);
          connection = Connector.newConnection(config);
        }
 
        // create the sobject to hold the post
        SObject post = new SObject();
        post.setType("FeedPost");
        post.setField("ParentId", Credentials.SFDC_USERID);
        post.setField("Body", tweet);
        // submit the update to Salesforce.com
        connection.create(new SObject[]{post});
 
      } catch (ConnectionException ce) {
        log.error(ce.toString());
      }
 
      resp.getWriter().println("Tweet sent to Chatter: "+tweet);
    } else {
      resp.getWriter().println("Could not fetch the lastes update from Twitter. Nothing sent to Chatter.");
    }
 
  }
 
  private String getLastTweet(HttpServletRequest req, HttpServletResponse resp) {
 
    String tweet = null;
    HttpSession session = req.getSession();
    Twitter twitter = new TwitterFactory().getInstance();
 
    twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
        Credentials.TWITTER_CONSUMERSECRET);
 
    // if the access token is present in the session
    if (session.getAttribute("accessToken") == null){
        // get the request token from the session
        String token = (String) session.getAttribute("token");
        String tokenSecret = (String)session.getAttribute("tokenSecret");
 
        // get the access token from twitter
        AccessToken accessToken = null;
        try {
          accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
        } catch (TwitterException e) {
          log.error(e.toString());
        }
        twitter.setOAuthAccessToken(accessToken);
 
        // save the access token, that are different from request token
        session.setAttribute("accessToken", accessToken.getToken());
        session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());
 
    } else {
        // use the access token from the session
        twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
            (String)session.getAttribute("accessTokenSecret"));
    }
 
    List<Status> statuses = null;
    try {
      // get the user's timeline
      statuses = twitter.getUserTimeline();
      // set their last tweet to return
      tweet = statuses.get(0).getText();
    } catch (TwitterException e) {
      log.error(e.toString());
    }
 
    return tweet;
 
  }
 
}

SendTweetServlet

When the user clicks the Chatter -> Twitter link, this Servlet queries Salesforce.com for the user’s most recent status update, finds the status in the returned XML results and then sends the status out as a tweet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.jeffdouglas;
 
import java.io.IOException;
import java.util.Iterator;
import javax.servlet.http.*;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;
 
import com.sforce.ws.*;
import com.sforce.ws.bind.XmlObject;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.sobject.SObject;
 
public class SendTweetServlet extends HttpServlet {
 
  private static final Logger log = Logger.getLogger(SendTweetServlet.class);
 
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
 
    PartnerConnection connection = null;
    String feedPost = null;
 
    try {
      if (connection == null) {
        ConnectorConfig config = new ConnectorConfig();
        config.setUsername(Credentials.SFDC_USERNAME);
        config.setPassword(Credentials.SFDC_PASSWORD);
        connection = Connector.newConnection(config);
      }
 
      QueryResult results = connection
          .query("SELECT Id, FeedPost.Body FROM UserFeed WHERE "
              + "ParentId = '" + Credentials.SFDC_USERID + "'"
              + " And Type = 'UserStatus' ORDER BY CreatedDate DESC LIMIT 1");
 
      // in this case there will only be 1 record returned, but....
      for (int i = 0; i < results.getRecords().length; i++) {
        SObject feed = results.getRecords()[i];
        feedPost = getFeedBody(feed);
      }
 
    } catch (ConnectionException ce) {
      log.error(ce.toString());
    }
 
    if (feedPost != null) {
      sendTweet(feedPost, req, resp);
      resp.getWriter().println("Chatter message sent to Twitter: " + feedPost);
    } else {
      resp.getWriter().println("Nothing sent to Twitter");
    }
 
  }
 
  private void sendTweet(String tweet, HttpServletRequest req, HttpServletResponse resp) {
 
    HttpSession session = req.getSession();
    Twitter twitter = new TwitterFactory().getInstance();
 
    twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
        Credentials.TWITTER_CONSUMERSECRET);
 
    // if the access token is present in the session
    if (session.getAttribute("accessToken") == null){
        // get the request token from the session
        String token = (String) session.getAttribute("token");
        String tokenSecret = (String)session.getAttribute("tokenSecret");
 
        // get the access token from twitter
        AccessToken accessToken = null;
        try {
          accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
        } catch (TwitterException e) {
          log.error(e.toString());
        }
        twitter.setOAuthAccessToken(accessToken);
 
        // save the access token, that are different from request token
        session.setAttribute("accessToken", accessToken.getToken());
        session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());
 
    } else {
        // use the access token from the session
        twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
            (String)session.getAttribute("accessTokenSecret"));
    }
 
    try {
      // update the user's twitter status
      twitter.updateStatus(tweet);
    } catch (TwitterException e) {
      log.error(e.toString());
    }
 
  }
 
  private String getFeedBody(SObject feed) {
    String feedBody = "";
    Iterator<XmlObject> feedPost = feed.getChildren();
    while (feedPost.hasNext()) {
      XmlObject post = feedPost.next();
      if (post.getValue() == null) {
        Iterator<XmlObject> body = post.getChildren();
        while (body.hasNext()) {
          XmlObject child = body.next();
          if (child.getName().toString().equals(
              "{urn:sobject.partner.soap.sforce.com}Body")) {
            feedBody = child.getValue().toString();
            break;
          }
        }
      }
    }
    return feedBody;
  }
}

SendBothServlet

This Servlet loads the HTML form presenting the user with a textbox to enter their new status. When the form is posted, the status is sent out to both Chatter and Twitter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.jeffdouglas;
 
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;
 
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
 
public class SendBothServlet extends HttpServlet {
 
  private static final Logger log = Logger.getLogger(SendTweetServlet.class);
 
  private void sendToChatter(String status) {
 
    PartnerConnection connection = null;
 
      try {
        if (connection == null) {
          ConnectorConfig config = new ConnectorConfig();
          config.setUsername(Credentials.SFDC_USERNAME);
          config.setPassword(Credentials.SFDC_PASSWORD);
          connection = Connector.newConnection(config);
        }
 
        // create the sobject to hold the post
        SObject post = new SObject();
        post.setType("FeedPost");
        post.setField("ParentId", Credentials.SFDC_USERID);
        post.setField("Body", status);
        // submit the update to Salesforce.com
        connection.create(new SObject[]{post});
 
      } catch (ConnectionException ce) {
        log.error(ce.toString());
      }
 
  }
 
  private void sendToTwitter(String status, HttpServletRequest req) {
 
    HttpSession session = req.getSession();
    Twitter twitter = new TwitterFactory().getInstance();
 
    twitter.setOAuthConsumer(Credentials.TWITTER_CONSUMERKEY,
        Credentials.TWITTER_CONSUMERSECRET);
 
    // if the access token is present in the session
    if (session.getAttribute("accessToken") == null){
        // get the request token from the session
        String token = (String) session.getAttribute("token");
        String tokenSecret = (String)session.getAttribute("tokenSecret");
 
        // get the access token from twitter
        AccessToken accessToken = null;
        try {
          accessToken = twitter.getOAuthAccessToken(token, tokenSecret);
        } catch (TwitterException e) {
          log.error(e.toString());
        }
        twitter.setOAuthAccessToken(accessToken);
 
        // save the access token, that are different from request token
        session.setAttribute("accessToken", accessToken.getToken());
        session.setAttribute("accessTokenSecret", accessToken.getTokenSecret());
 
    } else {
        // use the access token from the session
        twitter.setOAuthAccessToken((String)session.getAttribute("accessToken"),
            (String)session.getAttribute("accessTokenSecret"));
    }
 
    try {
      // update the user's twitter status
      twitter.updateStatus(status);
    } catch (TwitterException e) {
      log.error(e.toString());
    }
 
  }
 
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
  throws IOException {
 
    sendToChatter(req.getParameter("status"));
    sendToTwitter(req.getParameter("status"),req);
 
    resp.getWriter().println("Sent the following to both Chatter and Twitter: "+req.getParameter("status"));
 
  }
 
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
  throws IOException {
 
    try {
      RequestDispatcher rd = req.getRequestDispatcher("post.jsp");
      rd.forward(req, resp);
    } catch (ServletException e) {
      log.error(e.toString());
    }
 
  }
 
}


  • Share/Bookmark

Categories: GAE/J, Google App Engine, Java, Salesforce

No Comments

Writing an Inbound Email Service for Salesforce.com

March 12th, 2010

Creating an inbound email service for Salesforce.com is a relatively straight forward process but there are a few thing to explain to make your life easier. The email service is an Apex class that implements the Messaging.InboundEmailHandler interface which allows you to process the email contents, headers and attachments. Using the information in the email, you could for instance, create a new contact if one does not exists with that email address, receive job applications and attached the person’s resume to their record or have an integration process that emails data files for processing.

You access email services from Setup -> Develop -> Email Services. This page contains the basic code you will always use to start your Apex class. Simply copy this code and create your new class with it. Click the “New Email Service” button to get started and fill out the form. There are a number of options so make sure you read carefully and check out the docs. One handy option is the “Enable Error Routing” which will send the inbound email to an alternative email address when the processing fails. You can can also specify email address(es) to accept mail from. This works great if you have some sort of internal process that emails results or file for import into Salesforce.com. Just like Workflow, make sure you mark it as “Active” or you will pull your hair out during testing.

After you save the new email service, you will need to scroll down to the bottom of the page and create a new email address for the service. An email service can have multiple email addresses and therefore process the same message differently for each address. When you create a new email service address you specify the “Context User” and “Accept Email From”. The email service uses the permissions of the Context User when processing the inbound message. So you could, for example, have the same email service that accepts email from US accounts and processes them with a US context user and another address that accepts email from EMEA accounts and processes them with an EMEA context user. After you submit the from the Force.com platform will create a unique email address like the following. This is the address you send your email to for processing.

testemailservice@8q8zrtgg1w37vpomrhpqftj25.in.sandbox.salesforce.com

Now that the email service is configured we can get down to writing the Apex code. Here’s a simple class the creates a new contact and attaches any documents to the record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
global class ProcessJobApplicantEmail implements Messaging.InboundEmailHandler {
 
  global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, 
    Messaging.InboundEnvelope envelope) {
 
    Messaging.InboundEmailResult result = new Messaging.InboundEmailresult();
 
    Contact contact = new Contact();
    contact.FirstName = email.fromname.substring(0,email.fromname.indexOf(' '));
    contact.LastName = email.fromname.substring(email.fromname.indexOf(' '));
    contact.Email = envelope.fromAddress;
    insert contact;
 
    System.debug('====> Created contact '+contact.Id); 
 
    if (email.binaryAttachments != null && email.binaryAttachments.size() > 0) {
      for (integer i = 0 ; i < email.binaryAttachments.size() ; i++) {
        Attachment attachment = new Attachment();
        // attach to the newly created contact record
        attachment.ParentId = contact.Id;
        attachment.Name = email.binaryAttachments[i].filename;
        attachment.Body = email.binaryAttachments[i].body;
        insert attachment;
      }
    }
 
    return result;
 
  }
 
}

One of the difficult thing about email service is debugging them. You can either create a test class for this or simply send the email and check the debug logs. Any debug statements you add to your class will show in the debug logs. Go to Setup -> Administration Setup -> Monitoring -> Debug Logs and add the Context User for the email service to the debug logs. Simply send an email to the address and check the debug log for that user.

One thing I wanted to see was the actual text and headers that are coming through in the service. Here’s an image of virtually all fields and headers in a sample email. Click for more details.



The following unit test will get you 100% code coverage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static testMethod void testMe() {
 
  // create a new email and envelope object
  Messaging.InboundEmail email = new Messaging.InboundEmail() ;
  Messaging.InboundEnvelope env = new Messaging.InboundEnvelope();
 
  // setup the data for the email
  email.subject = 'Test Job Applicant';
  email.fromname = 'FirstName LastName';
  env.fromAddress = 'someaddress@email.com';
 
  // add an attachment
  Messaging.InboundEmail.BinaryAttachment attachment = new Messaging.InboundEmail.BinaryAttachment();
  attachment.body = blob.valueOf('my attachment text');
  attachment.fileName = 'textfile.txt';
  attachment.mimeTypeSubType = 'text/plain';
 
  email.binaryAttachments = 
    new Messaging.inboundEmail.BinaryAttachment[] { attachment };
 
  // call the email service class and test it with the data in the testMethod
  ProcessJobApplicantEmail emailProcess = new ProcessJobApplicantEmail();
  emailProcess.handleInboundEmail(email, env);
 
  // query for the contact the email service created
  Contact contact = [select id, firstName, lastName, email from contact 
    where firstName = 'FirstName' and lastName = 'LastName'];
 
  System.assertEquals(contact.firstName,'FirstName');
  System.assertEquals(contact.lastName,'LastName');
  System.assertEquals(contact.email,'someaddress@email.com');
 
  // find the attachment
  Attachment a = [select name from attachment where parentId = :contact.id];
 
  System.assertEquals(a.name,'textfile.txt');
 
}

Here are a few links that may be helpful:


  • Share/Bookmark

Categories: Apex, Salesforce

4 Comments

Error Compiling WSC AppEngine Partner Jar for Salesforce.com Sandbox

March 11th, 2010

I’m working on a demo for Google App Engine that connects to one of our Salesforce.com Sandboxes (hope to be done tomorrow or Monday). The Force.com Web Service Connector (WSC) project has a partner-library.jar that you can download and add to your project to get up and running quickly. However, if you want to connect to a Sandbox you have to download your Partner WSDL from your Sandbox and compile it with the wsc-gae-160.jar to generate the stub code.

To generate the stub code, run the following from the terminal:

java -classpath wsc.jar com.sforce.ws.tools.wsdlc wsdl jar.file

Where wsdl is the name of the partner WSDL file you downloaded and jar.file is the output jar file that is generated.

The problem is that seems to throw an exception on a Mac:

Exception in thread "main" java.lang.NullPointerException
at com.sforce.ws.tools.wsdlc.checkTargetFile(wsdlc.java:115)
at com.sforce.ws.tools.wsdlc.(wsdlc.java:66)
at com.sforce.ws.tools.wsdlc.run(wsdlc.java:288)
at com.sforce.ws.tools.wsdlc.main(wsdlc.java:279)

The fix is documented here but you essentially need to supply the entire path for the outputted jar file:

java -classpath wsc-gae-16_0.jar com.sforce.ws.tools.wsdlc chatter-sandbox-partner.wsdl /users/jeff/desktop/chatter-sandbox-partner.jar


  • Share/Bookmark

Categories: Google App Engine, Java, Salesforce

No Comments

Appirio Announces PS Connect at Google Campfire One

March 10th, 2010

Last night at Google Campfire One we demonstrated PS Connect, a new extension to our PS Enterprise product that allows service professionals to run their business directly from their Gmail inbox. We were the closing presentation for the night and you can view the video of the demo below. It’s roughly a 5 minute video and is really great!

So what is PS Connect? PS Connect allows you to use Google Apps as a front end to cloud-based business applications. With our Gmail contextual gadgets you can take actions from inside your Gmail inbox instead of logging into multiple systems.

You can read the full announcement on our website or get more info on PS Connect. If you’d like a deeper dive, you can even sign up for a 30 minute demo.


  • Share/Bookmark

Categories: Appirio, Cloud Computing, Google, Salesforce, The Internet Business

No Comments

Locking sObject Records

March 9th, 2010

I don’t see this discussed often, but Salesforce.com has the ability to lock sObject records while they are being updated to prevent threading problems and race conditions.

To lock records, simply use the FOR UPDATE keywords in your SOQL statements. You do not have to manually commit the records so if your Apex script finishes successfully the changes are automatically committed to the database and the locks are released. If your Apex script fails, any database changes are rolled back and the locks are also released.

1
2
3
4
for (List<Opportunity> ops : [select id from Opportunity 
   where stagename = 'Closed Lost' for update]) {
	// process the records and issue DML
}

The Apex runtime engine locks not only the parent sObject record but all child records as well. So if you lock an Opportunity sObject all of its Opportunity Line Items will be locked as well. Other users will be able to read these records but not make changes to them while the lock is in place.

If your record is locked and another thread tries to commit changes, the platform will retry for roughly 5 -10 seconds before failing with a “Resource Unavailable” error. For end users, I believe if they try to save a locked record from the Salesforce.com UI, they will receive an error message stating that the record has been changed and that they should reload the page. I can’t confirm but I’ve seen this in the past.


  • Share/Bookmark

Categories: Apex, Salesforce

1 Comment

Passing Parameters with a CommandButton

March 4th, 2010

This post is a slight tweak of yesterday’s post, Passing Parameters with a CommandLink. In theory you should just be able to switch out the CommandLink component with a CommandButton component and be golden. However, not so fast. There seem to still be a bug with the CommandButton component.

Here is the Visualforce page with the CommandButton instead of the CommandLink:

1
2
3
4
5
6
7
8
9
10
11
<apex:page standardController="Contact" extensions="CommandButtonParamController">
    <apex:form >
 
        <apex:commandButton value="Process Nickname" action="{!processButtonClick}">
            <apex:param name="nickName" 
                value="{!contact.firstname}" 
                assignTo="{!nickName}"/>
        </apex:commandButton>
 
    </apex:form>
</apex:page>

As with the CommandLink, when the user clicks the button the setters should fire and then call the processButtonClick() method to allow further publishing. However, the setter for nickName is never called!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public with sharing class CommandButtonParamController {
 
    // an instance varaible for the standard controller
    private ApexPages.StandardController controller {get; set;}
     // the object being referenced via url
    private Contact contact {get;set;}
    // the variable being set from the commandbutton
    public String nickName {
    	get; 
    	// *** setter is NOT being called ***
    	set {
    		nickName = value;
    		System.debug('value: '+value);
    	}
    }
 
    // initialize the controller
    public CommandButtonParamController(ApexPages.StandardController controller) {
 
        //initialize the stanrdard controller
        this.controller = controller;
        // load the current record
        this.contact = (Contact)controller.getRecord();
 
    }              
 
    // handle the action of the commandButton
    public PageReference processButtonClick() {
    	System.debug('nickName: '+nickName);
    	// now process the variable by doing something...
    	return null;
    }
 
}

Wes Nolte has done a great job on his blog and the Salesforce.com message boards pointing out the problem and workarounds. A popular option is using the CommandLink but styling it to look like a CommandButton.

You can make the CommandButton function as advertised if you use a rerender attribute and hidden pageBlock component. If you run the Visualforce page below with these modifications the setter will actually fire and set the value of nickName correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
<apex:page standardController="Contact" extensions="CommandButtonParamController">
    <apex:form >
 
        <apex:commandButton value="Process Nickname" action="{!processButtonClick}" rerender="hiddenBlock">
            <apex:param name="nickName" 
                value="{!contact.firstname}" 
                assignTo="{!nickName}"/>
        </apex:commandButton>
 
        <apex:pageBlock id="hiddenBlock" rendered="false"></apex:pageBlock>
 
    </apex:form>
</apex:page>


  • Share/Bookmark

Categories: Apex, Salesforce, Visualforce

6 Comments

Passing Parameters with a CommandLink

March 3rd, 2010

Here’s a small example of how you can pass a value to another method via a command link for Salesforce.com. When the link is clicked, the setter fires for the public member nickName. The button click then calls the processLinkClick method where you can do something like process the variable further with DML statement or running a SOQL query with the value.

The Visualforce page that simply displays a link that copies the contact’s firstName into the public member nickName via the “assignTo” attribute.

1
2
3
4
5
6
7
8
9
<apex:page standardController="Contact" extensions="CommandLinkParamController"> 
    <apex:form >
        <apex:commandLink value="Process Nickname" action="{!processLinkClick}">
            <apex:param name="nickName" 
                value="{!contact.firstname}" 
                assignTo="{!nickName}"/>
        </apex:commandLink>
    </apex:form>
</apex:page>

The controller extension used by the Visualforce page. The processLinkClick method is called after the setters fire, performs some processing and returns a null PageReference allowing Visualforce to refresh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public with sharing class CommandLinkParamController {
 
    // an instance varaible for the standard controller
    private ApexPages.StandardController controller {get; set;}
     // the object being referenced via url
    private Contact contact {get;set;}
    // the variable being set from the commandlink
    public String nickName {get; set;}
 
    // initialize the controller
    public CommandLinkParamController(ApexPages.StandardController controller) {
 
        //initialize the stanrdard controller
        this.controller = controller;
        // load the current record
        this.contact = (Contact)controller.getRecord();
 
    }              
 
    // handle the action of the commandlink
    public PageReference processLinkClick() {
    	System.debug('nickName: '+nickName);
    	// now process the variable by doing something...
    	return null;
    }
 
}


  • Share/Bookmark

Categories: Apex, Salesforce, Visualforce

3 Comments

Google Wave Robot API v2

March 2nd, 2010

Apparently Google Wave is not dead. Google just released new functionality with the post Introducing Robots API v2: The Rise of Active Robots. These new features include:

Active API: In v2, robots can now push information into waves (without having to wait to respond to a user action). This replaces the need for our deprecated cron API, as now you can update a wave when the weather changes or the stock price falls below some threshold. You can learn more in the Active API docs.

This is a great new feature that allows robots to act independently of the wave. This allows Wave to receive notifications and messages with external events occur. Similar to Chatter, if a record is updated in Salesforce.com, this message can be pushed to all recipients in the wave.

Context: Robots can now more precisely specify how much information they want to get back from a particular event. If only the contents of the affected blip needs updating and you want to reduce your robot’s bandwidth, then you can specify the new ‘SELF’ context. On the flip side, if you do need all the information in the wavelet, you can specify the new ‘ALL’ context. You can learn more in the Context docs.

The new Contexts provide much granular control of the wave and how a robot interacts with it:

  • PARENT indicates that the event should pass the parent data. Note that PARENT makes no difference to Wavelet events.
  • CHILDREN indicates that the event should pass any children of the event’s level. For Wavelets, this context passes all child Blips.
  • ALL indicates that the event passes all associated data.
  • SIBLINGS indicates that the event passes any siblings. For Blips, this context will pass data for all sibling blips within the Wavelet.
  • SELF indicates that the event only passes information pertaining to itself.
  • ROOT indicates that the event only passes information pertaining to the root blip.

Filtering: In a similar way, with this new API, the robot can specify what events it needs to respond to, conserving valuable bandwidth — and ignore all those that don’t apply. You can learn more in the Filtering Events docs.

This new feature is great for programmers allowing them respond to events based up regular expressions.

Error reporting: Robots are now able to register to receive errors about failed operations, such as insertion on non-existent ranges. You can learn more in the Error Reporting docs.

You can now handle exception more gracefully and interact with users in a more user-friendly manner.

Proxying-For: Robots can now convey to Google Wave that their actions are actually on behalf of a different user, via the proxyingFor field. For robots like the Buggy sample, which connects with the Google Code issue tracker, this means that the wave can be updated with attribution to users on non-wave systems. You can learn more in the Proxying-For docs.

This new features allow your robot to “impersonate” a named user on another system. So if you wave needs to insert records into an external system, it can do so as a specific user.

These new features will make robots more user friendly and easier to use. I just hope they implement a way to restrict a robot to a specific domain.


  • Share/Bookmark

Categories: Wave

No Comments

Overview – SAP BusinessObjects BI OnDemand for Salesforce.com

February 25th, 2010

SAP just released their BusinessObjects™ BI OnDemand solution for Salesforce.com which is a subscription-based, SaaS solution empowering sales professionals to explore, report and share data from Salesforce.com. It looks like SAP is trying to play nicely with Salesforce.com and is positioning this product as a BI alternative to actual reporting in Salesforce.com. There was no bashing of Salesforce.com in the text and the solution is AppExchange certified.

For companies concerned about the speed of reporting or size of Salesforce data sets, SAP BusinessObjects BI OnDemand provides a preconfigured data warehouse that can be implemented in as little as a day. The data warehouseprovides a duplicate set of data that can dramatically improve reporting time and speed information delivery.

Since they offer a free trial and I have a few years of SAP experience I thought I’d give it a spin. The signup process was pretty painless and after logging in I set up my connection to a production org (looks like sandbox connections are not an option).



The entire application is essentially a series of Flex applications. After logging into my org I was presented with the following screen displaying the metadata for the org and allowing me to drag and drop fields from any object to create a dataset.



It then presents you with a preview of the dataset and the associated SOQL query. Once you confirm the query and save the dataset the data is imported from your org into the warehouse’s datastore.



You can then see your newly created dataset in the list of available datasets.



After clicking on the dataset you are presented with the main UI for that dataset. Your options here include:

  • View data in the dataset
  • Perform ad-hoc queries against the dataset
  • Explore the dataset to slice and dice it to create more specific datasets or visualizations
  • Share the dataset with other uses (similar functionality to Google Docs sharing). I also noticed that you can share the actual web service address.
  • Download the data in CSV format
  • Combine with other datasets for more detailed reporting
  • Duplicate the dataset
  • Delete the dataset

With the Explore tab you can drill down into your data and run various scenario to create your own dataset and save results as a visualization that you can share or display with an iframe.



The product has a ton of features (it’s based off of Crystal Reports which SAP bought in 2007) and I only had time to try a few. If you are looking for a simple way to access and externally report on your Salesforce.com data, you might want to give SAP’s BusinessObjects™ BI OnDemand solution good look.


  • Share/Bookmark

Categories: SAP, Salesforce

4 Comments

Feed

http://blog.jeffdouglas.com /