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?
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
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.
packagecom.jeffdouglas;importjava.io.IOException;importjavax.servlet.RequestDispatcher;importjavax.servlet.ServletException;importjavax.servlet.http.*;importorg.apache.log4j.Logger;importtwitter4j.Twitter;importtwitter4j.TwitterException;importtwitter4j.TwitterFactory;importtwitter4j.http.RequestToken;publicclass LoginServlet extends HttpServlet {privatestaticfinal Logger log = Logger.getLogger(LoginServlet.class);publicvoid doGet(HttpServletRequest req, HttpServletResponse resp)throwsIOException{
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 tokenSecretString 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/OAuthString 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.
packagecom.jeffdouglas;importjava.io.IOException;importjava.util.List;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importorg.apache.log4j.Logger;importtwitter4j.Status;importtwitter4j.Twitter;importtwitter4j.TwitterException;importtwitter4j.TwitterFactory;importtwitter4j.http.AccessToken;importcom.sforce.ws.*;importcom.sforce.soap.partner.*;importcom.sforce.soap.partner.sobject.SObject;publicclass SendChatterServlet extends HttpServlet {privatestaticfinal Logger log = Logger.getLogger(SendTweetServlet.class);publicvoid doGet(HttpServletRequest req, HttpServletResponse resp)throwsIOException{
PartnerConnection connection =null;// get the user's last tweetString 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.");}}privateString 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 sessionif(session.getAttribute("accessToken")==null){// get the request token from the sessionString 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.
packagecom.jeffdouglas;importjava.io.IOException;importjava.util.Iterator;importjavax.servlet.http.*;importorg.apache.log4j.Logger;importtwitter4j.Twitter;importtwitter4j.TwitterException;importtwitter4j.TwitterFactory;importtwitter4j.http.AccessToken;importcom.sforce.ws.*;importcom.sforce.ws.bind.XmlObject;importcom.sforce.soap.partner.*;importcom.sforce.soap.partner.sobject.SObject;publicclass SendTweetServlet extends HttpServlet {privatestaticfinal Logger log = Logger.getLogger(SendTweetServlet.class);publicvoid doGet(HttpServletRequest req, HttpServletResponse resp)throwsIOException{
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");}}privatevoid 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 sessionif(session.getAttribute("accessToken")==null){// get the request token from the sessionString 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());}}privateString 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.
packagecom.jeffdouglas;importjava.io.IOException;importjavax.servlet.RequestDispatcher;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importorg.apache.log4j.Logger;importtwitter4j.Twitter;importtwitter4j.TwitterException;importtwitter4j.TwitterFactory;importtwitter4j.http.AccessToken;importcom.sforce.soap.partner.Connector;importcom.sforce.soap.partner.PartnerConnection;importcom.sforce.soap.partner.sobject.SObject;importcom.sforce.ws.ConnectionException;importcom.sforce.ws.ConnectorConfig;publicclass SendBothServlet extends HttpServlet {privatestaticfinal Logger log = Logger.getLogger(SendTweetServlet.class);privatevoid 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());}}privatevoid 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 sessionif(session.getAttribute("accessToken")==null){// get the request token from the sessionString 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());}}publicvoid doPost(HttpServletRequest req, HttpServletResponse resp)throwsIOException{
sendToChatter(req.getParameter("status"));
sendToTwitter(req.getParameter("status"),req);
resp.getWriter().println("Sent the following to both Chatter and Twitter: "+req.getParameter("status"));}publicvoid doGet(HttpServletRequest req, HttpServletResponse resp)throwsIOException{try{
RequestDispatcher rd = req.getRequestDispatcher("post.jsp");
rd.forward(req, resp);}catch(ServletException e){
log.error(e.toString());}}}
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.
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.
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.
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');}
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:
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:
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.
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.
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:
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!
public with sharing class CommandButtonParamController {// an instance varaible for the standard controllerprivate ApexPages.StandardController controller {get; set;}// the object being referenced via urlprivate Contact contact {get;set;}// the variable being set from the commandbuttonpublicString nickName {
get;// *** setter is NOT being called ***
set {
nickName = value;System.debug('value: '+value);}}// initialize the controllerpublic CommandButtonParamController(ApexPages.StandardController controller){//initialize the stanrdard controllerthis.controller= controller;// load the current recordthis.contact=(Contact)controller.getRecord();}// handle the action of the commandButtonpublic PageReference processButtonClick(){System.debug('nickName: '+nickName);// now process the variable by doing something...returnnull;}}
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.
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.
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.
public with sharing class CommandLinkParamController {// an instance varaible for the standard controllerprivate ApexPages.StandardController controller {get; set;}// the object being referenced via urlprivate Contact contact {get;set;}// the variable being set from the commandlinkpublicString nickName {get; set;}// initialize the controllerpublic CommandLinkParamController(ApexPages.StandardController controller){//initialize the stanrdard controllerthis.controller= controller;// load the current recordthis.contact=(Contact)controller.getRecord();}// handle the action of the commandlinkpublic PageReference processLinkClick(){System.debug('nickName: '+nickName);// now process the variable by doing something...returnnull;}}
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.
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.