Appirio's Tech Blog

Thursday, December 30, 2010

Learning Ruby for Force.com Developers – Part 1

With salesforce.com's recent purchase of Heroku, Ruby just got more interesting. I've never dabbled in Ruby (although I've wanted to!) so this is an excellent reason to dig into the language and see what it's all about. Since I'm learning Ruby I thought some people might also benefit from the experience and make the learning curve a little less steep.

So today we're kicking off a series on getting started with Ruby and Heroku. I'm not sure how many posts there will be but I hope to post something every week on the subject. We welcome any input you might have to make this journey easier. I know there are some Ruby experts in the salesforce.com community so please keep us honest and on the right track. Here are the goals of the series:
  • Learn Ruby
  • Develop an app locally using Ruby on Rails and the SQLite database
  • Modify the app to use Database.com and the Force.com Toolkit for Ruby instead of the SQLite database
  • Deploy the app to Heroku
Assumptions

For this series we're assuming that you have some type of basic programming background, have no knowledge of Ruby (I certainly don't), are using a Mac (sorry PC guys/gals but most of the difference is simply setup and configuration) and have a little time to experiment and learn.

Technology Overview

So with any new technology there is a lot of stuff to learn and digest. Here are some key aspects that you should know:

RubyRuby is an object-oriented interpreted scripting language created by Yukihiro Matsumoto (more affectionately known as Matz) in Japan starting in 1993. Ruby is an interpreted language (similar to PHP, Python and JavaScript) that is compiled when it is executed. Languages like Java and C++ are pre-compiled into a binary before being executed. Ruby is very portable, elegant and somewhat easier to learn. The disadvantage, since it is an interpreted language, is that it runs slower than equivalent compiled applications and your source code is visible to anyone using your application. Wikipedia has a really good, quick overview of the language and syntax.

RubyGemsRubyGems is a package manager for the Ruby programming language that provides a standard format for distributing Ruby programs and libraries (in a self-contained format called a "gem"), a tool designed to easily manage the installation of gems, and a server for distributing them. You will use RubyGems to install new programs and libraries on your computer.

Ruby on Rails - RoR is an open source web application framework developed by 37Signals of BaseCamp fame. Like many web frameworks, RoR uses the Model-View-Controller (MVC) architecture pattern to organize application programming. RoR includes tools that make common development tasks easier "out of the box", such as scaffolding that can automatically construct some of the models and views needed for a basic website. Also included are WEBrick, a simple Ruby web server that is distributed with Ruby, and Rake, a build system, distributed as a gem. RoR also uses ActiveRecord (an object-relational mapping system) for database access.

Git - Git is a distributed revision control system similar to Subversion or CVS. You don't need Git to develop Ruby applications however I believe you will need Git to push your application to Heroku. Git is really gaining ground lately and I believe that saleforce.com is moving Code Share to github.com.

Installing Ruby

Depending on your platform, Ruby may or may not be installed already. If you are using a somewhat newer Mac then Ruby should be installed. Open Terminal and type ruby --version. You should see something like the following.



The current stable version of Ruby is 1.9.2 but I have 1.8.7 installed and it seems to be working fine for now. If you'd like to download a newer version you can do so from here. If you are on a PC, then you can use the RubyInstaller.

Interactive Ruby

Now that you have Ruby installed it's time to start getting serious. Ruby comes with a program called "Interactive Ruby" (IRB) that will show the results of any Ruby statements you provide it. Playing with Ruby using IRB like this is a great way to learn the language. To get started open Terminal and type irb. On Windows, open fxri from the Ruby section of your Start Menu.

Now for the obligatory "Hello World". Type puts "Hello World" and hit enter. There you go. Your first Ruby program.


Dig into Ruby

Now it's time to really dig into the Ruby language. Here are some links that I've found helpful in order that I would recommend.
  • Ruby in Twenty Minutes - I would start with this intro first.
  • Try Ruby - An interactive tutorial that lets you try out Ruby right in your browser. This is a cool tutorial but it "seemed" to crash on me a number of times.
  • Ruby Essentials - a free on-line book designed to provide a concise and easy to follow guide to learning Ruby.
  • Ruby Koans - These are a series of files that you download and run locally to learn the language, syntax and structure.
  • Hackety Hack - A fun and easy way to learn about programming (through Ruby) using the Shoes GUI Toolkit.
  • Ruby Documentation - A great list of Ruby references. Pick and choose from here.
If anyone has any suggestions for other helpful sites please chime in. Spend some time using Ruby and I think next time we'll get into Ruby on Rails.

Tuesday, December 28, 2010

Salesforce Cloudstock Hackathon Demo from Dreamforce 2010

Jeff Douglas

This is the demo that I put together for the Cloudstock Hackathon and I tried to throw in as many partner services as possible. I finally ended up with five so it was dubbed the “Kitchen Sink” demo. I thought some people may find it useful as it shows how to use the Force.com REST API in conjunction with OAuth2 using the Spring MVC framework. Pat Patterson put together a great Getting Started with the Force.com REST API article but my app is slightly different and IMHO easier, since it uses the Spring Framework.

The app is a external-facing recruiting site that advertises the open Appirio positions. Please remember that this is a demo and I put most of the code into a couple of controllers to make it easier to show. The code can definitely be refactored in certain places.

Here’s how the app uses the partner services:

Partner How Used?
Use Force.com as the datastore for open jobs. Access to Force.com using OAuth2 and the REST API.
Send a job to a friend via SMS.
Application built using Spring STS, Spring Roo and Spring MVC. The application runs locally on tc Server.
Store metrics for jobs on MongoHQ.
Store pdf job descriptions on Box for download by applicants.

Here’s a video of the application so you can see it in action plus some explanation of the Spring code. The controller code for the OAuth functionality and interacting with the Job records is following the jump.

It may be easier to watch this at Youtube with a larger picture. Just double-click the video.


You can find all of the source code at github, but here is a preview of a few of the important files.

OAuthController - Responsible for API authorization to Force.com.

package com.appirio.jobs.web;
 
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
 
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
 
/**
 * @author Jeff Douglas (jeff@appirio.com)
 */
@RequestMapping("/oauth/**")
@Controller
public class OAuthController {
 
  private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
  private static final String INSTANCE_URL = "INSTANCE_URL";
 
  private String clientId = null;
  private String clientSecret = null;
  private String redirectUri = null;
  private String environment = null;
  private String authUrl = null;
  private String tokenUrl = null;
  private String accessToken = null;
 
  private void init() {
 
      redirectUri = "https://127.0.0.1:8443/AppirioCareers/oauth/_callback";
      environment = "https://na5.salesforce.com";
      // client id and secret from Force.com Remote Access
      clientId = "YOUR-CLIENT-ID";
      clientSecret = "YOUR-CLIENT-SECRET";
 
        try {
      authUrl = environment + "/services/oauth2/authorize?response_type=code&client_id=" 
        + clientId + "&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8");
    } catch (UnsupportedEncodingException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
    }
 
        tokenUrl = environment + "/services/oauth2/token";
 
  }
 
  /* Start the OAuth process if no session with the access token is found. 
   * If a session exists, the redirect the user to the /job/list page. */
  @RequestMapping(value = "/oauth/start", method = RequestMethod.GET)
  public String startOauth(WebRequest req) {
 
    init();
    // check for an access token in the servlet session
    accessToken = (String) req.getAttribute(ACCESS_TOKEN, RequestAttributes.SCOPE_SESSION);
 
    if (accessToken != null) {
      System.out.println("Session found!! Access token: "+accessToken);
      return "forward:/job/list";
    } else {
      System.out.println("No session... need to auth.");        
      return "redirect:" + authUrl;
    }
 
  }
 
  /* OAuth callback from Force.com after authrozing the application.  */
  @RequestMapping(value = "/oauth/_callback", method = RequestMethod.GET)
  public String endOauth(WebRequest req) {
 
    init();
    String accessToken = null;
    String instanceUrl = null;
    String code = req.getParameter("code");
    HttpClient http = new HttpClient();
    PostMethod post = new PostMethod(tokenUrl);
    post.addParameter("client_secret", clientSecret);
    post.addParameter("redirect_uri", redirectUri);
    post.addParameter("grant_type", "authorization_code");
    post.addParameter("code", code);
    post.addParameter("client_id", clientId);
 
    try {
      JSONObject json = null;
      http.executeMethod(post);
      String respBody = post.getResponseBodyAsString();
      System.out.println("post response: " + respBody);
      try {
        json = new JSONObject(respBody);
        accessToken = json.getString("access_token");
        instanceUrl = json.getString("instance_url");
      } catch (JSONException e) {
        e.printStackTrace();
      }
      System.out.println("found access token: "+accessToken);
      System.out.println("found instance url: "+instanceUrl);
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } finally {
      post.releaseConnection();
    }
 
    System.out.println("Setting Access token: "+accessToken);
    System.out.println("Setting Instance Url: "+instanceUrl);
 
    /* Set the token and url to the session so other servlets can access it. */
    req.setAttribute(ACCESS_TOKEN, accessToken, RequestAttributes.SCOPE_SESSION);
    req.setAttribute(INSTANCE_URL, instanceUrl, RequestAttributes.SCOPE_SESSION);
 
    return "forward:/job/list";
  }
 
  @RequestMapping
  public String index() {
    return "oauth/index";
  }
}

JobController - Implements the main functionality of the application.

package com.appirio.jobs.web;
 
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod; 
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
 
import com.appirio.jobs.domain.Job;
import com.appirio.jobs.domain.SmsMessage;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.TwilioRestResponse;
 
/**
 * @author Jeff Douglas (jeff@appirio.com)
 */
 
@RequestMapping("/job/**")
@Controller
public class JobController {
 
  private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
  private static final String INSTANCE_URL = "INSTANCE_URL";
  private static Mongo m;
  private static DB db;
  private static DBCollection coll;
  private static DBCursor cur;
  private ArrayList jobs = new ArrayList();
 
 
    @RequestMapping(value="/job/list", method=RequestMethod.GET)
    public ModelAndView list(WebRequest req) {
 
      // if the job collection is empty then fetch jobs from Force.com
      if (jobs.isEmpty()) {
 
        // fetch the access token and url from the servlet session
        String accessToken = (String) req.getAttribute(ACCESS_TOKEN, RequestAttributes.SCOPE_SESSION);
        String instanceUrl = (String) req.getAttribute(INSTANCE_URL, RequestAttributes.SCOPE_SESSION);
 
        System.out.println("Access token: "+accessToken);
        System.out.println("Instance Url: "+instanceUrl);
 
        jobs = new ArrayList();
      HttpClient httpclient = new HttpClient();
          GetMethod gm = new GetMethod(instanceUrl + "/services/data/v20.0/query");
          //set the token in the header
          gm.setRequestHeader("Authorization", "OAuth "+accessToken);
          //set the SOQL as a query param
          NameValuePair[] params = new NameValuePair[1];
          //no need to url encode here...it will cause your query to fail
          params[0] = new NameValuePair("q","Select Id, Name, Job_Title__c, Location__c, " +
              "Duties__c, Skills__c, Salary__c, Box_Url__c from Job__c Order by Job_Title__c");
          gm.setQueryString(params);
 
          String respBody = "";
 
          try {
        httpclient.executeMethod(gm);
        respBody = gm.getResponseBodyAsString();
            System.out.println("response body: " + respBody);
      } catch (HttpException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
      } catch (IOException e2) {
        // TODO Auto-generated catch block
        e2.printStackTrace();
      } finally {
              gm.releaseConnection();
          }
 
          try {
              JSONObject json = new JSONObject(respBody);                
              JSONArray results = json.getJSONArray("records");
 
             for(int i = 0; i < results.length(); i++) {
               // add the json payload to a Job object
               Job job = new Job(results.getJSONObject(i).getString("Id"), 
            results.getJSONObject(i).getString("Name"),
            results.getJSONObject(i).getString("Job_Title__c"),
            results.getJSONObject(i).getString("Location__c"),
            results.getJSONObject(i).getString("Duties__c"),
            results.getJSONObject(i).getString("Skills__c"),
            results.getJSONObject(i).getString("Salary__c"),
            results.getJSONObject(i).getString("Box_Url__c"));
 
               // add the job to the collection
               jobs.add(job);
             }
 
 
          } catch (JSONException e) {
              e.printStackTrace();
          }
 
          System.out.println("jobs found: "+jobs.size());
 
      }
 
      ModelAndView mav = new ModelAndView("job/list");
      mav.addObject("jobs", jobs);
      return mav;
    }
 
    @RequestMapping(value="/job/{id}/display", method=RequestMethod.GET)
    public ModelAndView display(@PathVariable String id, Model model) {
      Job job = getJobById(id);
      incrementCount(job.getName(),"views");
      ModelAndView mav = new ModelAndView("job/display");
      mav.addObject(getJobById(id));
      return mav;
    }
 
    @RequestMapping(value="/job/{id}/sms", method=RequestMethod.GET)
    public ModelAndView message(@PathVariable String id, Model model) {
      ModelAndView mav = new ModelAndView("job/sms"); 
      SmsMessage sms = new SmsMessage();
      sms.setPhone("1111111111");
      sms.setMessage("Check out this AWESOME job with Appirio!");
      mav.addObject("smsMessage", sms);
      mav.addObject(getJobById(id));
      return mav;
    }
 
    @RequestMapping(value="/job/{id}/print", method=RequestMethod.GET)
    public String print(@PathVariable String id, Model model) {
      Job job = getJobById(id);
      incrementCount(job.getName(),"downloads");
      System.out.println(job.getBoxUrl());
      return "redirect:"+job.getBoxUrl();
    }
 
    @RequestMapping(value = "/job/{id}/smsSend", method = RequestMethod.POST)
    public ModelAndView smsSubmit(@PathVariable String id, @ModelAttribute SmsMessage sms, Model model) {
 
      Job job = getJobById(id);
      sendSms(job, sms.getPhone(), sms.getMessage());
      incrementCount(job.getName(),"messages");
 
      ModelAndView mav = new ModelAndView("job/smsConfirm"); 
      mav.addObject("phone", sms.getPhone());
      mav.addObject("message", sms.getMessage());
      mav.addObject(job);
      return mav;
 
    }
 
  private void incrementCount(String name, String type) {
 
    try {
      m = new Mongo("flame.mongohq.com", 27065);
      db = m.getDB("AppirioCareers");
      char[] password = { '4','+','r','E','o','x','x','x','x','x'};
      boolean auth = db.authenticate("YOUR-USERNAME", password);
      System.out.println("Mongo auth?: "+auth);
      coll = db.getCollection("jobs");  
    }
    catch (UnknownHostException ex) {
      ex.printStackTrace();
    }
    catch (MongoException ex) {
      ex.printStackTrace();
    }
 
    cur = coll.find(new BasicDBObject("name", name));  
    while (cur.hasNext()) {
      BasicDBObject doc = (BasicDBObject)cur.next();
      if (type.equals("views"))
        doc.put("views", (Integer)doc.get("views")+1);
      else if (type.equals("messages"))
        doc.put("messages", (Integer)doc.get("messages")+1);
      else
        doc.put("downloads", (Integer)doc.get("downloads")+1);
      coll.update( new BasicDBObject("name", name), doc );
    }
 
  }
 
    private void sendSms(Job job, String phone, String message) {
 
        String AccountSid = "YOUR-ACCOUNT-ID";
        String AuthToken = "YOUR-AUTH-TOKEN";
        String ApiVersion = "2010-04-01";
 
        TwilioRestClient client = new TwilioRestClient(AccountSid, AuthToken, null);
 
        String msg = "\n"+message+"\n"+job.getJobTitle()+"\nhttp://appirio.com/careers";
 
        System.out.println("size: "+msg.length());
 
        //build map of post parameters 
        Map params = new HashMap();
        params.put("From", "14155992671");
        params.put("To", phone);
        params.put("Body", msg);
        TwilioRestResponse response;
        try {
            response = client.request("/"+ApiVersion+"/Accounts/"+AccountSid+"/SMS/Messages", "POST", params);
 
            if(response.isError())
                System.out.println("Error making outgoing call: "+response.getHttpStatus()+"\n"+response.getResponseText());
            else {
                System.out.println(response.getResponseText());
 
            }
        } catch (TwilioRestException e) {
            e.printStackTrace();
        }
 
    }
 
    private Job getJobById(String id) {
      Job job = null;
      for (Job j : jobs) {
        if (j.getId().equals(id)) {
          job = j;
          break;
        }
      }
      return job;
    }
 
    @RequestMapping
    public String index() {
        return "job/index";
    }
 
}

Monday, December 27, 2010

VMforce Demo - Dreamforce 2010

Jeff Douglas

A number of people were not able to make the VMforce demo so I threw together a short video showing my demo app and the code. I plan on doing a number of tutorials, demos and videos in the near future so stay tuned.

Wednesday, December 15, 2010

Google Chrome Extension – Force.com Logins

One of our brilliant guys in our Appirio Japan office, Toshihiro Takasu, wrote this really slick Google Chrome Extension for managing your salesforce.com logins in Chrome:
  • Keeps your salesforce.com account information (usrename, password, security token, and description).
  • Lets you log into the account you selected with new tab.
  • Lets you log into the account you selected with new window (separate session).
  • Groups your account for easy management.
  • Searches accounts by username for you to quickly access the account.
  • Export and import the account information in XML format.
You can install the extension here.



I've seen a few questions on Twitter regarding the security that is used to store credentials. I spoke with Toshi and he's updated the extension's page to include some more info. Essentially the extension is using Chrome's localStorage which is only accessible to the extension itself and the Chrome developer tool. This is the same storage method that Chrome uses internally to save your credentials when you fill out a form. So if you trust Chrome to auto-fill your salesforce.com login credentials then this extension is doing basically the same thing but just making your life a little easier. It's up to you. Hope you find it useful.

Monday, December 6, 2010

Introducing [insert name here]

Dave Messigner

It is my pleasure to introduce you to our new, yet to be named community. We are launching it into beta this week at Dreamforce and the first task we have put out to the community is to name it. What better way to launch an open community, then to have the community name itself? (Plus a $2000 prize for the winning name doesn’t hurt either.)

The purpose of this site will be to build a cross cloud community of developers interested in solving and discussing cloud based problems. It will recognize and reward the best and brightest cloud programmers and enthusiasts from across the globe. Challenges will be posted asking the community to solve a variety of problems; if you solve the challenge, you win the prize. In addition to winning money, as a registered member you will have a public profile to show off your work, awards, skills and recognition earned in the community.

We are planning to redesign and launch the community in January, but we didn’t want to miss the opportunity to show the concept at the premier cloud conference, Dreamforce, and sign up some early participants. Take an early peek at the new site mockups (large file 40 MB) to get an idea what the site will be about and what profiles will look like.

The site will be powered by Appirio, but we hope to add many other sponsors over time. So register to see new challenges, win some money and let us know what you think!

Dave
 
2006-2012 Appirio Inc. All rights reserved.
Appirio.com | Support | Resource Center | Contact | Careers | Privacy Policy