Force.com HTTPCallout Test Class

Below is the test class to test an HTTP Callout from apex:

Main Test Class

@isTest
global class HttpCalloutToFeedTest {
	@isTest static void testCallOutToFeed() {
           Test.setMock(HttpCalloutMock.class, new HttpCalloutToFeedMock());
           HttpResponse res = HttpCalloutToFeed.sentMessageToFeed();

           System.assertEquals(201, res.getStatusCode());
	}
}

HTTP Callout Mock

@isTest
global class HttpCalloutToFeedMock implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest httpreq) {
      System.assertEquals('http://cloudsole-feed.herokuapp.com/sfdc/public/feed', httpreq.getEndpoint());
      System.assertEquals('POST', httpreq.getMethod());
      String message = '{"name": "Test Class for CloudSole Feed", "message": "This test passed!!!"}';
      
      httpreq.setHeader('Accept', 'application/json');
      httpreq.setHeader('Content-type', 'application/json');
      httpreq.setBody(message);
      httpreq.setMethod('POST');

      HttpResponse res = new HttpResponse();
      res.setStatusCode(201);
      return res;
  }
}

As this is a POST there is no allot of response but the statusCode. 201 means created so the feed was successfully created.

cloudsole-streaming force.com streaming with rabbitmq

Rabbit MQ Class

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownSignalException;

public class RabbitStream {

	public ConnectionFactory createRabbitConnection(String hostname)
	{
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(hostname);
		return factory;
	}

	public Connection createConnection(ConnectionFactory rabbitFactory) throws IOException
	{
		Connection rabbitconnection = rabbitFactory.newConnection();
		return rabbitconnection;
	}

	public Channel createChannel(Connection rabbitConnection, String queueName) throws IOException
	{
		final Channel rabbitChannel = rabbitConnection.createChannel();
		rabbitChannel.queueDeclare(queueName, false, false, false, null);
		return rabbitChannel;
	}

	public void basicPublish(Channel rabbitChannel, String queueName, String message) throws IOException
	{
		rabbitChannel.basicPublish("", queueName, null, message.getBytes());
	}

	public QueueingConsumer consumeChannel(Channel rabbitChannel, String queueName) throws IOException
	{
	    final QueueingConsumer rabbitconsumer = new QueueingConsumer(rabbitChannel);
	    rabbitChannel.basicConsume(queueName, true, rabbitconsumer);
	    return rabbitconsumer;
	}


	public List<String> consumeQueue(QueueingConsumer rabbitConsumer) throws ShutdownSignalException, ConsumerCancelledException, InterruptedException
	{
		List<String> messageList = new ArrayList<String>();
		while (true) {
		      QueueingConsumer.Delivery delivery = rabbitConsumer.nextDelivery();
		      String message = new String(delivery.getBody());
		      messageList.add(message);
		}
	}

	public Channel createRabbitFactory(String hostName, String queueName) throws IOException
	{
		RabbitStream mainStream = new RabbitStream();
		return mainStream.createChannel(mainStream.createConnection(mainStream.createRabbitConnection(hostName)), queueName);
	}

}

Cometd Class

import com.force.sdk.streaming.client.ForceBayeuxClient;
import com.force.sdk.streaming.client.ForceStreamingClientModule;
import com.force.sdk.streaming.client.PushTopicManager;
import com.force.sdk.streaming.exception.ForceStreamingException;
import com.force.sdk.streaming.model.PushTopic;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class PushTopicFactory {
	
	public Injector createInjector()
	{
		Injector injector = Guice.createInjector(new ForceStreamingClientModule());
		return injector;
	}

	public ForceBayeuxClient createClient(Injector injector)
	{
		  ForceBayeuxClient client = injector.getInstance(ForceBayeuxClient.class);
		  return client;
	}
	
	public PushTopicManager createPushTopManager(Injector injector)
	{
		PushTopicManager pushTopicManager = injector.getInstance(PushTopicManager.class);
		return pushTopicManager;
	}
	
	public PushTopic getTopicByName(PushTopicManager pushTopicManager, String topicName) throws ForceStreamingException
	{
		PushTopic topic = pushTopicManager.getTopicByName(topicName);
		return topic;
	}
	
	public PushTopic createPushTopic(PushTopicManager pushTopicManager, String name, Double apiVersion, String query, String description)
	{
		PushTopic createTopic = pushTopicManager.createPushTopic(new PushTopic(name, apiVersion, query, description));
		return createTopic;
	}
	
	public PushTopic pushTopicFactory(String topicName) throws ForceStreamingException
	{
		PushTopicFactory pushTopicFactory = new PushTopicFactory();
		Injector injector = pushTopicFactory.createInjector();
		pushTopicFactory.createClient(injector);
		PushTopicManager publicTopicManager = pushTopicFactory.createPushTopManager(injector);
		
		return pushTopicFactory.getTopicByName(publicTopicManager, topicName); 
	}
	
	public ForceBayeuxClient createPushTopicClientFactory()
	{
		PushTopicFactory pushTopicClientFactory = new PushTopicFactory();
		ForceBayeuxClient fbClient = pushTopicClientFactory.createClient(pushTopicClientFactory.createInjector());
		return fbClient;
	}
}

Main Class

import java.io.IOException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import com.rabbitmq.client.Channel;
import com.example.pushtopic.PushTopicFactory;
import com.example.rabbit.RabbitStream;
import com.force.sdk.streaming.client.ForceBayeuxClient;
import com.force.sdk.streaming.client.PushTopicManager;
import com.force.sdk.streaming.exception.ForceStreamingException;
import com.force.sdk.streaming.model.PushTopic;
import com.google.inject.Injector;

public class StreamMain {
	
	private static final String TOPICNAME = "AccountPush";
	private static final String QUEUENAME = "streaming";
	private static final String HOSTNAME= "localhost";
	
	public static void main(String[] args) throws ForceStreamingException, InterruptedException, Exception {
			
		   final Log log = LogFactory.getLog(StreamMain.class);
		
			final RabbitStream rabbitFactory = new RabbitStream();
			final Channel factoryChannel = rabbitFactory.createRabbitFactory(HOSTNAME, QUEUENAME);
		
			PushTopicFactory pushTopicFactory = new PushTopicFactory();
			Injector injector = pushTopicFactory.createInjector();
			ForceBayeuxClient client = pushTopicFactory.createClient(injector);
			PushTopicManager publicTopicManager = pushTopicFactory.createPushTopManager(injector);
			PushTopic createTopic = pushTopicFactory.createPushTopic(publicTopicManager, "NewAccountPushTopic", 27.0, "select Id, Name from Account", "New Push Topic");
	
			PushTopic topic = pushTopicFactory.getTopicByName(publicTopicManager, createTopic.getName()); 
			
			client.subscribeTo(topic, new ClientSessionChannel.MessageListener() 
			{   
				public void onMessage(ClientSessionChannel channel, Message message) 
				{
					try {
						rabbitFactory.basicPublish(factoryChannel, QUEUENAME, message.getJSON());
						log.info(message.getJSON());
					} catch (IOException e) {
						log.error(e);
						e.printStackTrace();
					} 
				}
			});
          }
}

Salesforce custom LinkedIn button

An easy way to search for users in Salesforce is to add a ‘View in LinkedIn’ button

On Contact:

http://www.linkedin.com/commonSearch?type=people&keywords={!Contact.FirstName}+{!Contact.FirstName}&pplSearchOrigin=GLHD&pageKey=nmp-home&search=Search

On Account (create custom formula fields to Contact FirstName and LastName):

http://www.linkedin.com/commonSearch?type=people&keywords={!Account.First_Name__c}+{!Account.Last__c}&pplSearchOrigin=GLHD&pageKey=nmp-home&search=Search

Apigee as Force.com REST provider

Apigee has an extensive Force.com REST interface to interact with Salesforce. Follow this tutorial to create an app in apigee, create a remote connection in Salesforce, add a user to the app to get your smart-key.

Complete the tutorial below to get the information needed to run this class:

http://developer.apigee.com/salesforce_tutorial.html

You will need the following to run this class:
1. Smart-key
2. Application Name

import com.apigee.sdk.oauth.api.exception.RestCallException;
import com.apigee.sdk.oauth.api.rest.RestCallResult;
import com.apigee.sdk.oauth.api.rest.RestCall;
import com.apigee.sdk.oauth.impl.model.Request;

public class ForceApigee {

	   //Configure your SMARTKEY and APPNAME
	   final static String SMARTKEY = "YOURSMARTKEY";
	   final static String APPNAME = "YOURAPPLICATIONNAME";
	   final static String PROVIDER = "salesforce";
	   
	   final static String VERSIONENDPOINT = "/services/data";
	   final static String RESOURCESENDPOINT = "/services/data/v27.0";
	   final static String SOBJECTENDPOINT = "/services/data/v27.0/sobjects";
	   final static String SOBJECTBASICENDPOINT = "/services/data/v24.0/sobjects/Account";
	   final static String SOBJECTDESCRIBEENDPOINT = "/services/data/v27.0/sobjects/Account/describe";
	   final static String SOQLQUERYENDPOINT = "/services/data/v24.0/query?q=SELECT name from Account";
	   final static String SOQLSEARCHENDPOINT = "/services/data/v24.0/search?q=FIND {test} IN ALL FIELDS";
	
	   public static String callToSalesforce(Request.HttpVerb verb, String Endpoint)
	   {
		   try 
		   {
	           RestCallResult result = new RestCall(verb, Endpoint)
	             .withSmartKey(SMARTKEY)
	             .toProvider(PROVIDER)
	             .usingApp(APPNAME)
	             .invoke();
				 return result.getResponsePayload().toString();
	       } catch (RestCallException e) {
			   return e.getMessage();
	       }
	   } 
	   
	   public static void main(String[] args)
	   {
	   		System.out.println("***VERSIONENDPOINT****: \n" + callToSalesforce(Request.HttpVerb.GET, VERSIONENDPOINT));
	   		System.out.println("***RESOURCESENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, RESOURCESENDPOINT));
	   		System.out.println("***SOBJECTENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, SOBJECTENDPOINT));
	   		System.out.println("***SOBJECTBASICENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, SOBJECTBASICENDPOINT));
	   		System.out.println("***SOBJECTDESCRIBEENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, SOBJECTDESCRIBEENDPOINT));
	   		System.out.println("***SOQLQUERYENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, SOQLQUERYENDPOINT));
	   		System.out.println("***SOQLSEARCHENDPOINT***: \n" + callToSalesforce(Request.HttpVerb.GET, SOQLSEARCHENDPOINT));
	   }
}

Force.com FourSquare Visualforce Page

<apex:page showHeader="false" showChat="false" sidebar="false">
<html>
<head>
  <title>foursquare :: Explore Sample</title>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript" id="jquery"></script>
  <link href="{!URLFOR($Resource.ForceSquare, 'ForceSquare/leaflet.css')}" type="text/css" rel="stylesheet" />
  <script src="{!URLFOR($Resource.ForceSquare, 'ForceSquare/apisamples.js')}" type="text/javascript"></script>
  <script type="text/javascript" src="https://ss0.4sqi.net/scripts/third_party/jquery.ba-bbq-eddd4adf74d0c1310a401475178c57df.js"></script>
  <script src="{!URLFOR($Resource.ForceSquare, 'ForceSquare/leaflet.js')}" type="text/javascript"></script>
  <script src="{!URLFOR($Resource.ForceSquare, 'ForceSquare/wax.leaf.min.js')}" type="text/javascript"></script>
 
  <style type="text/css">
    html { height: 100%; }
    body { height: 100%; margin: 0; padding: 0; }
    /* Give our markers a background image */
    .leaflet-marker-icon {
      background: url(https://foursquare.com/img/pin-blue-transparent.png);
      padding: 6px;
      padding-bottom: 17px;
      top: -6px;
      left: -6px;
      }
  </style>
 
  <script type="text/javascript">
  
  //<![CDATA[
  var client_id = '<YOURCLIENTID>';
  var callback_url = 'https://cs17.salesforce.com/apex/FourSquarePage';

  /* Attempt to retrieve access token from URL. */
  if ($.bbq.getState('access_token')) {
    var token = $.bbq.getState('access_token');
    $.bbq.pushState({}, 2)
  } else if ($.bbq.getState('error')) {
  } else {
    /* Redirect for foursquare authentication. */
    window.location.href = 'https://foursquare.com/oauth2/authenticate?client_id=' + client_id
    + '&response_type=token&redirect_uri=' + callback_url;
  }  

  /* HTML 5 geolocation. */
  navigator.geolocation.getCurrentPosition(function(data) {
    var lat = data['coords']['latitude'];
    var lng = data['coords']['longitude'];
    /* Create map. */
    var map = new L.Map('map_canvas')
      .setView(new L.LatLng(lat, lng), 15);
    /**
     * This is a sample map url that you need to change.
     * Sign up at http://mapbox.com/foursquare for a custom map url.
     */
    var mapboxUrl = 'http://a.tiles.mapbox.com/v3/foursquare.map-b7qq4a62.jsonp';
    wax.tilejson(mapboxUrl, function(tilejson) {
      map.addLayer(new wax.leaf.connector(tilejson));
    });

    /* Query foursquare API for venue recommendations near the current location. */
    $.getJSON('https://api.foursquare.com/v2/venues/explore?ll=' + lat + ',' + lng + '&oauth_token=' + token, {}, function(data) {
      venues = data['response']['groups'][0]['items'];
      /* Place marker for each venue. */
      for (var i = 0; i < venues.length; i++) {
        /* Get marker's location */
        var latLng = new L.LatLng(
          venues[i]['venue']['location']['lat'], 
          venues[i]['venue']['location']['lng']
        );
        /* Build icon for each icon */
        var leafletIcon = L.Icon.extend({
          iconUrl: venues[i]['venue']['categories'][0]['icon'],
          shadowUrl: null,
          iconSize: new L.Point(32,32),
          iconAnchor: new L.Point(16, 41),
          popupAnchor: new L.Point(0, -51)
        });
        var icon = new leafletIcon();
        var marker = new L.Marker(latLng, {icon: icon})
          .bindPopup(venues[i]['venue']['name'], { closeButton: false })
          .on('mouseover', function(e) { this.openPopup(); })
          .on('mouseout', function(e) { this.closePopup(); });
        map.addLayer(marker);
      }       
    })
  })
  //]]>
  </script>
 
  
</head>
<body>
  <div style="width: 100%; height: 100%;" id="map_canvas"></div>
</body>
</html>
</apex:page>

Set in visualforce page:

var client_id = ‘YOURCLIENTAPPID’;
var callback_url = https://.salesforce.com/apex/FourSquarePage

Set in FourSquare App Setup Page:

Redirect URL https://.salesforce.com/apex/FourSquarePage

Sencha Touch Chatter Mobile Application

Chatter Controller

public with sharing class ChatterController {

    public class Response {
        public Boolean success;
        public String errorMessage;
        public List<SObject> records;
        public Integer total;
        Response() {
            records = new List<SObject>();
            success = true;
        }
    }
    
    //One of the parameters supplied by the DirectProxy read method.
    public class QueryRequest {
        Integer start;
        Integer recordCount;
        List<Map<String, String>> sortParams;
        
        Public QueryRequest() {
            start = 1;
            recordCount = 1;
        }
 
        Public QueryRequest(Integer pStart, Integer pRecordCount) {
            start = pStart;
            recordCount = pRecordCount;
        }
 
    }
 
    @RemoteAction
    public static Response Add(List<FeedItem> chatterData){
        return insertLeadList(chatterData);
    }
    
    @RemoteAction
    public static Response Destroy(List<FeedItem> chatterData){
        return deleteChatterList(chatterData);
    }
    
    @RemoteAction
    public static Response Edit(List<FeedItem> chatterData){
        return editChatterList(chatterData);
    }
    
    @RemoteAction
    public static Response EditComment(List<String> chatterCommentData){
        return editCommentChatterList(chatterCommentData);
    }
    
    @RemoteAction
    public static Response Query(QueryRequest qr){
 
        Response resp = new Response();
 
        //Enforce a limit on the number of rows requested.
        final integer QUERY_LIMIT = 500;
        if( qr.start >= QUERY_LIMIT ){
            resp.success = false;
            resp.errorMessage = 'Maximum number of records (' + String.valueOf(QUERY_LIMIT) + ') exceeded!';
            return resp;
        }
 
        try {
            getAllChatter(qr, resp);
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Query failed: ' + e.getMessage();
        }
 
        return resp;
    }
    
    private static Response insertCommentOrLike(List<FeedComment> chatterComment, List<FeedLike> chatterLike)
    {
        Response resp = new Response();
        resp.success = true;
 
        try {
            insert chatterComment;
            insert chatterLike;
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Insert failed: ' + e.getMessage();
        }
        return resp;
    }
    
    private static void getAllChatter(QueryRequest qr, Response resp){
 
        //Page size is set in the Sencha store as recordCount.  
        Integer pageSize = qr.recordCount; 
 
        //Page number will be calculated.
        Integer pageNumber = 0;
    
        //Start is the record number indicating the start of the page.
        if( qr.start > 0 ){
            pageNumber = qr.start / pageSize;   
        }
 
        //Calculate the offset for SOQL.
        Integer offset = pageNumber * pageSize;
 
        //Build the query in pieces.
        String fieldList = 'Id, Title, Body, LikeCount,CommentCount';
 
        //Construct a base query to which the page offsets will be added.
        String baseQuery = 'SELECT ' + fieldList + ' FROM FeedItem Order By CreatedDate DESC';
 
        //Construct a count query to pass back the total records matching a search criteria.
        String baseCountQuery = 'SELECT COUNT() FROM FeedItem';
 
        //Construct the fetch query with the offset.
        String fetchQuery = baseQuery + ' LIMIT ' + pageSize + ' OFFSET ' + offset;
 
        try {
            //Set the count.
            resp.total = Database.countQuery(baseCountQuery);
 
            //Set the fetched recordset.
            resp.records = Database.query(fetchQuery);
            
            //Set the status flag.
            resp.success = true;
 
        } catch (Exception e) {
 
            //Set the total count of records matching the query.
            resp.total = 0;
            
            //Set the recordset to return.
            resp.records = new List<Lead>();
            
            //Set the status flag.            
            resp.success = false;
 
        }    
                                    
    }
    
    private static Response insertLeadList(List<FeedItem> chatterData){
        Response resp = new Response();
        resp.success = true;
 
        try {
            chatterData[0].ParentId = UserInfo.getUserId();
            insert chatterData;
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Insert failed: ' + e.getMessage();
        }
        return resp;
    }
    
    private static Response deleteChatterList(List<FeedItem> ChatterData){
 
        Response resp = new Response();
        resp.success = true;
 
        try {
            delete ChatterData;
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Deletion failed: ' + e.getMessage();
        } 
        return resp;
    }
    
    private static Response editChatterList(List<FeedItem> chatterData){
        Response resp = new Response();
        resp.success = true;
 
        try {
            System.debug('Hier: ' + chatterData);
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Deletion failed: ' + e.getMessage();
        } 
        return resp;
    }
    
    private static Response editCommentChatterList(List<String> chatterCommentData)
    {
        Response resp = new Response();
        resp.success = true;
 
        try {
            System.debug('Hier: ' + chatterCommentData);
        } catch (Exception e) {
            resp.success = false;
            resp.errorMessage = 'Deletion failed: ' + e.getMessage();
        } 
        return resp;
    }
}

Chatter Component

<apex:component controller="ChatterController">
 <script type="text/javascript">
    Ext.application({
    name: "ChatterPlus",
    models: ["Chatter"],
    stores: ["Chatter"],
    controllers: ["Chatter"],
    views: ["ChatterList", "ChatterEditor", "ChatterLikeCommentEditor"],
   
    launch: function () {
        var chatterListView = {
            xtype: "chatterlistview"
        };
        
        var chatterEditorView = {
            xtype: "chattereditorview"
        };
        
        var chatterLikeCommentEditor = {
            xtype: "chatterlikecommentview"
        };
        
        Ext.Viewport.add([chatterListView, chatterEditorView, chatterLikeCommentEditor]);
    }
});
Ext.define("ChatterPlus.view.ChatterList", {
    extend: "Ext.Container",
    requires: "Ext.dataview.List", 
    alias: "widget.chatterlistview",
    
    config: {
 
        //Take up the full space available in the parent container.
        layout: {
            type: 'fit'
        },
 
 
        //Add the components to include within the list view. 
        items: [
        {
            xtype: "toolbar",
            title: "Chatter",
            docked: "top",
 
            items: [
                { 
                    xtype: 'spacer' 
                },
                {
                    xtype: "button",
                    text: 'New',
                    ui: 'action',
                    itemId: "newButton"
                }
            ]
        }, 
        {
            xtype: "toolbar",
            docked: "bottom",
            itemId: "bottomToolBar",
                        
            items: [
               
            ]
        },
        {
            //The main list and its properties. 
            xtype: "list",
            store: "Chatter",
            itemId:"chatterList",
            
            onItemDisclosure: true,
            indexBar: true,
            grouped: false,
            disableSelection: false,
 
            plugins: [
                {
                    xclass: 'Ext.plugin.ListPaging',
                    autoPaging: true
                }
            ],

            loadingText: "Loading Feed...",
            emptyText: '<div class="chatter-list-empty-text">No feeds found.</div>',
            
            itemTpl: '<div class="list-item-line-main">{Title}</div>' + 
                     '<div class="list-item-line-main">{Body}</div>',                   
        }],
        
        listeners: [{
            delegate: "#newButton",
            event: "tap",
            fn: "onNewButtonTap"
        }, {
            delegate: "#syncButton",
            event: "tap",
            fn: "onSyncButtonTap"
        }, {        
            delegate: "#chatterList",
            event: "disclose",
            fn: "onChatterListDisclose"
        },
        {
            delegate: "#chatterList",
            event: "refresh",
            fn: "onChatterListRefresh",
        },
        {
            //Listener on the view's Activate event fires when redisplayed by transition.
            event: "activate",
            fn: "onChatterListViewActivate",
        }]  
    },
    onSyncButtonTap: function () {
        console.log("syncChatterCommand");
        this.fireEvent("syncChatterCommand", this);
    },
 
    onNewButtonTap: function () {
        console.log("newChatterCommand");
        this.fireEvent("newChatterCommand", this);
    },
 
    onChatterListDisclose: function (list, record, target, index, evt, options) {
        console.log("editChatterCommand");
        this.fireEvent('editChatterCommand', this, record);
    }, 
    onChatterListRefresh: function () {
        console.log("onChatterListRefresh");
        this.updateListCounter();
    },
 
    onChatterListViewActivate: function () {
        console.log("onChatterListViewActivate");
        this.updateListCounter();
    },
 
    //Function to get count of records in the list and show on the search button's badge. 
    updateListCounter: function () {
        var listCount = Ext.getStore("Chatter").getCount();
        //this.getComponent("bottomToolBar").getComponent("syncButton").setBadgeText(listCount);
    }  

});

Ext.define("ChatterPlus.view.ChatterEditor", {
    extend: "Ext.form.Panel",
    requires: "Ext.form.FieldSet",
    alias: "widget.chattereditorview",
 
    config: {
     scrollable: 'vertical',
 
        items: [
            {
                xtype: "toolbar",
                docked: "top",
                title: "Edit Chatter",
        
                items: [
                    {
                        xtype: "button",
                        ui: "back",
                        text: "Home",
                        itemId: "backButton"
                    },
                    { xtype: "spacer" },
                    {
                        xtype: "button",
                        ui: "action",
                        text: "Save",
                        itemId: "saveButton"
                    }
                ]
            },
            {
                xtype: "toolbar",
                docked: "bottom",
                items: [
                    {
                        xtype: "button",
                        iconCls: "trash",
                        iconMask: true,
                        itemId: "deleteButton"
                    }
                ]
            },
            { xtype: "fieldset",
                title: 'Chatter Info',
                items: [
                    {
                        xtype: 'textfield',
                        name: 'Title',
                        label: 'Title',
                        required: true
                    },
                    {
                        xtype: 'textareafield',
                        name: 'Body',
                        label: 'Body',
                        maxRows: 1,
                        required: true
                    },
                    {
                        xtype: 'textfield',
                        name: 'CommentCount',
                        label: 'Comment Count',
                        disabled: true
                        
                    },
                    {
                        xtype: 'textfield',
                        name: 'LikeCount',
                        label: 'Like Count',
                        disabled: true
                    },
                ]           
            },
            { xtype: "fieldset",
                title: 'Chatter Comment & Like',
                items: [
                    { 
                         xtype : 'textareafield',
                         name : 'Comment',
                         label : 'Comment',
                         maxRows: 1
                    },          
                    { 
                         xtype : 'checkboxfield',
                         name : 'Like',
                         label : 'Like'
                    },
                ]
            },      
        ],
        listeners: [
            {
                delegate: "#backButton",
                event: "tap",
                fn: "onBackButtonTap"
            },
            {
                delegate: "#saveButton",
                event: "tap",
                fn: "onSaveButtonTap"
            },
            {
                delegate: "#deleteButton",
                event: "tap",
                fn: "onDeleteButtonTap"
            }
        ]
    },
    
    onSaveButtonTap: function () {
        console.log("saveChatterCommand");
        this.fireEvent("saveChatterCommand", this);
    },
 
    onDeleteButtonTap: function () {
        console.log("deleteChatterCommand");
        Ext.Msg.confirm("Delete Feed", "Are you sure?", function(button){
            if (button == 'yes') {
                this.fireEvent("deleteChatterCommand", this);
            } else {
                return false;
            }
        }, this);
    },
 
    onBackButtonTap: function () {
        console.log("backToHomeCommand");
        this.fireEvent("backToHomeCommand", this);
    }
 
});


Ext.define("ChatterPlus.view.ChatterLikeCommentEditor", {
    extend: "Ext.form.Panel",
    requires: "Ext.form.FieldSet",
    alias: "widget.chatterlikecommentview",
 
    config: {
    }
 
 });
    
    
    
Ext.define("ChatterPlus.controller.Chatter", {
    extend: "Ext.app.Controller",
 
    config: {
 
        refs: {
            chatterListView: "chatterlistview",
            chatterEditorView: "chattereditorview",
            chatterList: "#chatterList",         
        },
 
        control: {
            chatterListView: {
                // The commands fired by the list container.
                syncChatterCommand: "onSyncChatterCommand",
                newChatterCommand: "onNewChatterCommand",
                editChatterCommand: "onEditChatterCommand",
            },
            chatterEditorView: {
                // The commands fired by the note editor.
                saveChatterCommand: "onSaveChatterCommand",
                deleteChatterCommand: "onDeleteChatterCommand",
                backToHomeCommand: "onBackToHomeCommand"
            }
        }
    },
 
    //View Transitions
    slideLeftTransition: { type: 'slide', direction: 'left' },
    slideRightTransition: { type: 'slide', direction: 'right' },
    
    activateChatterEditor: function (record) {
        var chatterEditorView = this.getChatterEditorView();
        chatterEditorView.setRecord(record); 
        Ext.Viewport.animateActiveItem(chatterEditorView, this.slideLeftTransition);
    },
 
    activateChatterList: function () {
        Ext.Viewport.animateActiveItem(this.getChatterListView(), this.slideRightTransition);
    },
 
    onSyncChatterCommand: function () {
        console.log("onSyncChatterCommand");
        this.loadList();
    },
     
    onNewChatterCommand: function () {
        console.log("onNewChatterCommand");
        var newChatter = Ext.create("ChatterPlus.model.Chatter");
        this.activateChatterEditor(newChatter);
    },
    
     onEditChatterCommand: function (list, record) {
        console.log("onEditChatterCommand");
        this.activateChatterEditor(record);
    },
    
    loadList: function () {
 
        //Get a ref to the store and remove it.
        var chatterStore = Ext.getStore("Chatter");
 
        var model = Ext.ModelMgr.getModel('ChatterPlus.model.Chatter');
        model.getProxy();
        
        chatterStore.getData().clear();   
        chatterStore.loadPage(1); 
        
        //Reshow the list.
        this.activateChatterList();
    },
    
    // Base Class functions.
    launch: function () {
        console.log("launch");
        this.callParent(arguments);
        
        //Load up the Store associated with the controller and its views. 
        console.log("load Chatter");
        this.loadList();
        
   },
   
   onSaveChatterCommand: function () {
        console.log("onSaveChatterCommand");
       
        var chatterEditorView = this.getChatterEditorView();
        var currentChatter = chatterEditorView.getRecord();
        var newValues = chatterEditorView.getValues();
        this.getChatterEditorView().updateRecord(currentChatter);     
 
        var errors = currentChatter.validate();
        if (!errors.isValid()) {
            var msg = '';
            errors.each(function(error) {
                msg += error.getMessage() + '<br/>';
            });
            console.log('Errors: ' + msg);
            Ext.Msg.alert('Please correct errors!', msg, Ext.emptyFn);
            currentChatter.reject();
            return;
        }
 
        //Get a ref to the store.
        var chatterStore = Ext.getStore("Chatter");
        var chatterCommentStore = Ext.getStore("ChatterComment");
        //var newChatterComment = Ext.create("ChatterPlus.model.ChatterComment");
        
        //Add new record to the store.
        if (null == chatterStore.findRecord('id', currentChatter.data.id)) {
            chatterStore.add(currentChatter);
           //chatterCommentStore.add(newChatterComment);
        }
        
        chatterCommentStore.sync();
        //Resync the proxy and activate the list.   
        chatterStore.sync();
        this.activateChatterList();
    },
 
    onDeleteChatterCommand: function () {
        console.log("onDeleteChatterCommand");
        
        //Get a ref to the form and its record. 
        var chatterEditorView = this.getChatterEditorView();
        var currentChatter = chatterEditorView.getRecord();

        var chatterStore = Ext.getStore("Chatter");
        chatterStore.remove(currentChatter);
              
        chatterStore.sync();
        this.activateChatterList();
    },
 
    onBackToHomeCommand: function () {
        console.log("onBackToHomeCommand");
        this.activateChatterList();
    },
 
    init: function() {
 
        this.callParent(arguments);
        console.log("init");
 
        //Listen for exceptions observed by the proxy so we can report them and clean up.
        Ext.getStore('Chatter').getProxy().addListener('exception', function (proxy, response, operation, options) {
            // only certain kinds of errors seem to have useful information returned from the server
            if (response) {
                if (response.errorMessage) {
                    Ext.Msg.alert('Error', response.errorMessage);
                } else {
                    Ext.Msg.alert('Error', operation.config.action + ' failed: ' + response.errorMessage);
                }
            } else {
                Ext.Msg.alert('Error', operation.config.action + ' failed for an unknown reason: proxy = ' + proxy);
            }
        });
 
    },
 
});

Ext.define("ChatterPlus.model.Chatter", {
    extend: "Ext.data.Model",
 
    config: {
        idProperty: 'Id',
 
        fields: [
            { name: 'Id', type: 'string', persist: false},
            { name: 'Title', type: 'string'},
            { name: 'Body', type: 'string'},
            { name: 'LikeCount', type: 'string', persist: false},
            { name: 'CommentCount', type: 'string', persist: false},
            { name: 'Comment', type: 'string', persist: false},
            { name: 'Like', type: 'string', persist: false},
            { name: 'ParentId', type: 'string'}
        ],
 
        validations: [
             { type: 'presence', field: 'Title', message: 'Enter a Title' },
             { type: 'presence', field: 'Body', message: 'Enter a Body' } 
        ],
 
        //Bind each CRUD functions to a @RemoteAction method in the Apex controller
        proxy: {
            type: 'direct',
            api: {
                read:     ChatterController.Query,
                create:   ChatterController.Add,
                update:   ChatterController.Edit,
                destroy:  ChatterController.Destroy
            },
            limitParam: 'recordCount',   // because "limit" is an Apex keyword
            sortParam: 'sortParams',     // because "sort" is a keyword too
            pageParam: false,            // we don't use this in the controller, so don't send it
            reader: {
                type: 'json',
                rootProperty: 'records',
                messageProperty: 'errorMessage'
            },
            writer: {
                type: 'json',
                root: 'records',
                writeAllFields: false,   // otherwise empty fields will transmit as empty strings, instead of "null"/not present
                allowSingle: false,      // need to always be an array for code simplification
                encode:  false           // docs say "set this to false when using DirectProxy"
            }
 
        }
    },
 
});

Ext.define("ChatterPlus.model.ChatterComment", {
    extend: "Ext.data.Model",
 
    config: {
        idProperty: 'Id',
 
        fields: [
            { name: 'Comment', type: 'string'}
        ],
 
        validations: [
            
        ],
 
        //Bind each CRUD functions to a @RemoteAction method in the Apex controller
        proxy: {
            type: 'direct',
            api: {
                update:   ChatterController.EditComment
            },
            writer: {
                type: 'json',
                root: 'records',
                writeAllFields: false,   // otherwise empty fields will transmit as empty strings, instead of "null"/not present
                allowSingle: false,      // need to always be an array for code simplification
                encode:  false           // docs say "set this to false when using DirectProxy"
            }
 
        }
    },
 
});



ChatterController.Query.directCfg.method.getArgs = 
    function (params, paramOrder, paramsAsHash) {
        console.log('getArgs: ' + params.data);
        return [params]; 
    }
 
Ext.data.proxy.Direct.prototype.createRequestCallback =
    function(request, operation, callback, scope){
        var me = this;
        return function(data, event){
             console.log('createRequestCallback: ' + operation);    
             me.processResponse(event.status, operation, request, data, callback, scope);
    };
    
};

Ext.define("ChatterPlus.store.Chatter", {
    extend: "Ext.data.Store",
    requires: "Ext.data.proxy.LocalStorage",
    config: {
        model: "ChatterPlus.model.Chatter",
        autoLoad: true,
        pageSize: 25,     
    },
 
    listeners: {
        load: function(){
             console.log('store.load(): loaded!');                
        },
    }
 
});

</script>
</apex:component>

Force.com REST Swagger Spring MVC

Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The overarching goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync. With Swagger, deploying managing, and using powerful APIs has never been easier.

Spring Swagger Controller code

package com.thysmichels.swagger4forcedotcom.controllers;

import com.knappsack.swagger4springweb.controller.ApiDocumentationController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * This is an example of how you might extend the ApiDocumentationController in order to set your own RequestMapping
 * (instead of the default "/api") among other possibilities.  Going this route, you do not necessarily have to define
 * the controller in your servlet context.
 */
@Controller
@RequestMapping(value = "/api")
public class ExampleDocumentationController extends ApiDocumentationController {

    public ExampleDocumentationController() {
        setBaseControllerPackage("com.thysmichels.swagger4forcedotcom.controllers.api");
        setBaseModelPackage("com.thysmichels.swagger4forcedotcom.models");
        setApiVersion("v1");
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String documentation() {
        return "api";
    }
}

Force.com REST Implementation Code

package com.thysmichels.swagger4forcedotcom.controllers.api;

import java.util.List;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.core.Response;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;


import com.force.api.ForceApi;
import com.force.api.QueryResult;
import com.thysmichels.swagger4forcedotcom.models.Account;
import com.thysmichels.swagger4forcedotcom.services.SalesforceLogin;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiError;
import com.wordnik.swagger.annotations.ApiErrors;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;

@Controller
@RequestMapping(value = "/api/v1/account")
@Api(value = "Account operations", listingClass = "AccountController", basePath = "/api/v1/account", description = "All operations for accounts")
public class AccountController {
	
	@Inject
	@Named("SalesforceLogin")
	private SalesforceLogin sfdcLogin;
	private ForceApi api;
	
	@PostConstruct
	public void init()
	{
		api = sfdcLogin.loginToSalesforce();
	}
	
	 @ApiOperation(value = "Find all accounts", notes = "Get all account currently available", httpMethod = "GET", responseClass = "Account", multiValueResponse = true)
	 @ApiError(code = 500, reason = "Process error")
	 @RequestMapping(value = "/", method = RequestMethod.GET, produces = "application/json")
	 public @ResponseBody List<Account> showAllAccounts() {
	    	QueryResult<Account> res = api.query("SELECT Name FROM Account", Account.class);
	        return res.getRecords();
	 }
	
	@ApiOperation(value = "Find account by Id", notes = "Get account by specifying Id", httpMethod = "GET", responseClass = "Account", multiValueResponse = true)
	@ApiErrors(value = { @ApiError(code = 400, reason = "Invalid ID supplied"), @ApiError(code = 404, reason = "Account not found") })
	@RequestMapping(value = "/{accountId}", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody Account[] findAccountById(@ApiParam(internalDescription = "java.lang.string", name = "accountId", required = true, value = "string") @PathVariable String accountId) {
    	Account account = api.getSObject("account", accountId).as(Account.class);
        return new Account[]{account};
    }

	
	@ApiOperation(value = "Delete a account", notes = "Delete a specific account with the given ID", httpMethod = "DELETE")
    @ApiError(code = 500, reason = "Process error")
    @RequestMapping(value = "/{accountId}", method = RequestMethod.DELETE, produces = "application/json")
    public @ResponseBody String deleteAccount(@ApiParam(internalDescription = "java.lang.string", name = "accountId", required = true, value = "string") @PathVariable String accountId) {
    	api.deleteSObject("account", accountId);
    	return "{status: success}";
    }
	
	@ApiOperation(value = "Create a account using Param", notes = "Creates a new account in salesforce using Param", httpMethod = "POST")
    @ApiError(code = 500, reason = "Process error")
    @RequestMapping(method = RequestMethod.POST, produces = "application/json")
    public @ResponseBody String createAccountFromParamName(@ApiParam(internalDescription = "java.lang.string", value="string", name = "Name", required = false) @RequestParam(required = true) String Name) 
    {	
		Account newAccount = new Account();
		newAccount.setName(Name);
    	String id = api.createSObject("Account", newAccount);
    	return "{id:" + id +"}";
    }
	
	@ApiOperation(value = "Create a account", notes = "Creates a new account in salesforce", httpMethod = "POST")
    @ApiError(code = 500, reason = "Process error")
    @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
    public @ResponseBody String createAccountFromJSON(@RequestBody Account newAccount) 
    {	
    	String id = api.createSObject("Account", newAccount);
    	return "{id:" + id +"}";
    }
	
	@ApiOperation(value = "Update Account", notes = "Update a existing Account", httpMethod = "POST") 
	@RequestMapping(value = "/{accountId}", method = RequestMethod.POST, consumes = "application/json")
	 public @ResponseBody String updateAccount(@PathVariable String accountId, @RequestBody Account account) {
		 api.updateSObject("Account", accountId, account);
		 return "{status: success}";
	 }
}

Screenshots

Salesforce Spring MVC Swagger

Salesforce Spring MVC Swagger1