Friday, December 13, 2013

System.debug('TCO13'); //The event log

Day 1 - The arrival


I arrived in Washington D.C. at 5 p.m., after 10 hours and a half of plane and more than 1 hour on line at the customs: no need to say I was really tired. That's why the first day in Washington hasn't been that good or exciting, no meeting with anyone, no TCO13 clues (except for the guys that got me at the airport), 6 hours of jet lag that apparently got me down (don't ask me why, I survived to 11 hours jet lag in Polinesia without problems, even 9 hours in San Francisco, but with Washington as well as New York I needed 1 day to make the journey comfortable). The only weird thing to note is what happened at the customs. Officer: "Why are you staying in D.C.?"
Me: "For a nerd convention?"
Officer: "What??"
Me: "A convention of nerds about computer programming...I'm a computer programmer (and so I'm a nerd n.d.r.)"
Officer: [small laugh]
Me: [small laugh]
Officer: [checking my passport]
Officer: [bigger laugh]
Me: "What?"
Officer: "HAHA that's weird!"
Me: "But that's what it is!"
End of the story: I think the officer was a kind of bully when he was young, that's why he was laughing repetedly...fonrtunately he let me pass!

Day 2 - The wheel we will spin


Nothing seemed to happend until I met @eucuepo on the street back to the hotel (I recognized him at first site, despite my difficulty to remember faces!).
Finally I met the great Tim Hicks (as I said in previous post, he's my Cloudspokes bro).
It's been love at first sight...we see eachother from distance and we run each other in time lapse...jokes apart, has been really cool to meet him after one year of emails, hangouts, blog posts and whatever happened!
At noon the jet lag seemed to gain power again, so I decided to take a sleep, but at about 4 p.m. I received a series of email/tweets that actually woke me up! It was @logonkartik who asks if we can meet downstairs at the hotel bar. That's where I met him with @chok68, and finally the Cloudspokes crew started to grow up!
This is some nerd swag I got:

After few chats (trying to speak English better than I could) the crew added more guys: Kyle Bower, Cory McIlroy, Dave Messinger, Chris DeLaurentis, Mike Cardillo, @CloudBytes and George Acker for the Cloudspokes mith part, and @callmecatootie, @jan3594, @wcheung, @soe, @aproxcacs ...I know I'm forgetting the majority!
I also had the pleasure to met Narinder Singh, the CEO/CIO of Cloudspokes.
At 7.30 pm TCO13 opens for the starting party (NASA, Google, Facebook among sponsors!!) and I finally had to spin the wheel. I got:
  • FinancialForce API
  • Amazon Webservices
  • Docusign
But still I have no hint of what I'm gonna do!

Day 3 - The infinite sleep


I know I'm on vacation, I'm in the US, I'm nerding like there won't be a tomorrow, but today I'm really tired. The only notable thing was that I met (finally) Jeff Douglas.
After a 2h city walk, I felt I need more concentration because of the lack of sleep, so I decided to close myself in the hotel room and start coding there, and that's gone pretty cool.
I've coded from 12 pm to 7 pm and finally, together with the other guys, I went to the Faceook party at their Washington Offices: a big open space, with some nerdy things (Nintendo 64, Nerf guns for example), a big wall (I left my sign on it ) and cool Facebook guys that exaplined to us some things about their work and office (like why a room is called beofre "Al Gore"...just because one day the vice president went in their office and entered in that room and a guy said "Yay!" to greet him..."and that's why this is the Al Gore room!"). After that some other lines of code (messing with DocuSign), and I'm ready to dive in the bed (hopefully I'll dream an entire night).

Day 4 - The coding day


Nothing notable today...coding from 5 am to 4 am (yes about 23 hours...just lunch and dinner time of break). This is what I came up with: Sheherazade - The crowdsourcing story teller.
I tried to apply crowsourcing development in a less technical field that is the story telling. What I wanted to achieve was a sort of ideas generator (for plots, song lyrics, books made of pictures) for editors, rewarding users by money prices and comunity glory.
For a complete overview of the app see these demo videos:
  1. http://www.screencast.com/t/jzKwI389rfDJ
  2. http://www.screencast.com/t/nlIoJtgDmS
  3. http://www.screencast.com/t/hwNeUuR2pG8
  4. http://www.screencast.com/t/m7JJrwZ21a2n
  5. http://www.screencast.com/t/hUaaft0WK4qu
And these are the slides of the presentation. I chose to code alone in my hotel room because of concentration and because in the main hall of the event I had to sit in pouf chairs not that comfortable, and what I wanted to do was a bit complicate and need the most of the time I could have.


Day 5 - Judgment and winner announcement


I slept about 3 hours to get the app ready, and made a funny presentation of my wedding day using the app, to show how a plot can be easily constructed using the comunity as a source of ideas. This was the scaring jury:
This is the story: "...And they lived happily ever after..." .

This is me presenting the app (I think I was saying something really stupid...this is my "What did I say?!?" face ):


Than, after a 1 hour sleep, came the second presentation, to a bigger audience:


In a flash of an eye we were about to know the winners....and I got 4th place! Not bad! I think the main reason of the missed first place is that my APIs were hidden while the other solutions actually created "something new" with theier APIs (and in a really smart way...so congrats to my Cloudspokes buddies!!)...you can read in the following Jeff Douglas's post the results and the video demo of the winner apps: The TCO13 Cloud Mashathon – Building Killer Apps with the API “Wheel of Fortune”.

I think this pic summarizes the coolness and amazingness and awesomeness of the whole TCO13:


This has been the most valuable professional expirience I've ever had, I could meet a lot of smart people from all over the world and compete with them in a friedly way (you know money makes people fighting each other), and I can say I was happy for my Cloudspokes buddy win, I recognize their value and great skills! I really hope to be part of the TCO14, and I know I have to work hard to achieve this!
Lastly I want to thank all Cloudspokes and Topcoder staff for the opportunity they gave me and to make me feel a bit special!

This is the final video event:

Monday, November 25, 2013

[NodeJS + Salesforce SOAP WS] How to consume a Salesforce SOAP WSDL

I was wondering how to consume Salesforce WSDLs with nodejs.
I found Node Soap package (see npm) and I tried to consume a Partner WSDL.
Then I saved the WSDL in the "sf-partner.wsdl" file and played with the methods to get nodeJS speak SOAP with Salesforce.

var soap = require('soap');
var url = './sf-partner.wsdl';
soap.createClient(url, function(err, client) {
   console.log('Client created');
   console.log(client.SforceService.Soap); //all methods usable in the stub
});

If you try to console.log(client) you will see too much data

This is the output:

{ login: [Function],
  describeSObject: [Function],
  describeSObjects: [Function],
  describeGlobal: [Function],
  describeDataCategoryGroups: [Function],
  describeDataCategoryGroupStructures: [Function],
  describeFlexiPages: [Function],
  describeAppMenu: [Function],
  describeGlobalTheme: [Function],
  describeTheme: [Function],
  describeLayout: [Function],
  describeSoftphoneLayout: [Function],
  describeSearchLayouts: [Function],
  describeSearchScopeOrder: [Function],
  describeCompactLayouts: [Function],
  describeTabs: [Function],
  create: [Function],
  update: [Function],
  upsert: [Function],
  merge: [Function],
  delete: [Function],
  undelete: [Function],
  emptyRecycleBin: [Function],
  retrieve: [Function],
  process: [Function],
  convertLead: [Function],
  logout: [Function],
  invalidateSessions: [Function],
  getDeleted: [Function],
  getUpdated: [Function],
  query: [Function],
  queryAll: [Function],
  queryMore: [Function],
  search: [Function],
  getServerTimestamp: [Function],
  setPassword: [Function],
  resetPassword: [Function],
  getUserInfo: [Function],
  sendEmailMessage: [Function],
  sendEmail: [Function],
  performQuickActions: [Function],
  describeQuickActions: [Function],
  describeAvailableQuickActions: [Function] }

There is a quicker way to obtain this using the console.log(client.describe()) function, but this seems not to work with big WSDL like Salesforce ones (Maximum stack error)

The first move was to login to obtain a valid session id (using SOAP login action):

soap.createClient(url, function(err, client) {
    client.login({username: [email protected]',password: 'FreakPasswordWithTkenIfNeeded'},function(err,result,raw){
      if(err)console.log(err);
      if(result){
          console.log(result.result);
    });
});

And this is the result

{ metadataServerUrl: 'https://na15.salesforce.com/services/Soap/m/29.0/00Di0000000Hxxx',
  passwordExpired: false,
  sandbox: false,
  serverUrl: 'https://na15.salesforce.com/services/Soap/u/29.0/00Di0000000Hxxx',
  sessionId: 'XXXXXXXXXX',
  userId: '005i0000000MXXXAAC',
  userInfo: 
   { accessibilityMode: false,
     currencySymbol: '€',
     orgAttachmentFileSizeLimit: 5242880,
     orgDefaultCurrencyIsoCode: 'EUR',
     orgDisallowHtmlAttachments: false,
     orgHasPersonAccounts: false,
     organizationId: '00Di0000000HxxxXXX',
     organizationMultiCurrency: false,
     organizationName: 'Challenges Co.',
     profileId: '00ei0000000UM6PAAW',
     roleId: {},
     sessionSecondsValid: 7200,
     userDefaultCurrencyIsoCode: {},
     userEmail: [email protected]',
     userFullName: 'Admin',
     userId: '005i0000000MxxxXXX',
     userLanguage: 'en_US',
     userLocale: 'en_US',
     userName: [email protected]',
     userTimeZone: 'Europe/Rome',
     userType: 'Standard',
     userUiSkin: 'Theme3' } }

Now the problem was to put the new endpoint and the session id or the next call, and this is the solution:

  //sets new soap endpoint and session id
  client.setEndpoint(result.result.serverUrl);
  var sheader = {SessionHeader:{sessionId: result.result.sessionId}};
  client.addSoapHeader(sheader,"","tns","");

And after that you can make wathever call you want:

      client.query({queryString:"Select Id,CaseNumber From Case"},function(err,result,raw){
          if(err){
            //console.log(err);
            console.log(err);
          }
          if(!err && result){
            console.log(result);
          }
      });

The result var will have all the data you expect from the SOAP response:

{ result: 
   { done: true,
     queryLocator: {},
     records: 
      [ [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object] ],
     size: 26 } }

The fact that you cannot call the client.describe() force us to read the WSDL to know which parameters send to the call.

Monday, November 4, 2013

[Salesforce / Canvas / NodeJS] Set up a minimum Canvas App (NodeJS style)

Following the old post [Salesforce / Canvas] Set up a minimum Canvas App (Java style) I've decided to put togheter the code I've used to talk to a Force.com Canvas App in NodeJS.

For those who can't wait the whole article, this is the repo (CanvasApp-NodeJS-Base).

Follow this steps to configure an App (something is changed in the last SF releases):

  • Setup -> Create -> Apps -> (Connected Apps) New
  • Fill Conntected App Name, API Name and Contact Email
  • Check the Enable OAuth Settings in the API (Enable OAuth Settings) section
    • Callback URL: put a valid URL (you don't need it for the scope of this example)
    • Selected OAuth Scopes: select the scopes you want for the NodeJS app acess token to grant
  • In the section Canvas App Settings type:
  • Canvas App URL: the url of the POST action to be called by Salesforce when injecting the session data (e.g. https://my-canvas-app.herokuapp.com/canvas/callback)
  • Access Method: use Signed Request (POST)
  • Locations: choose Chatter Tab (the app will be shown in the Chatter page)

Than you need to enable this app:

  • Setup -> Manage Apps -> Connected Apps -> Your App Name
  • In the OAuth policies select Admin approved users are pre-authorized for the Permitted Users field
  • In the Profiles related list select your profile (all user of that profile are automatically allowed to consume this app)

There are more ways to configure the Canvas App usage, this is the quicker.

Now clone this github repo CanvasApp-NodeJS-Base and create your own app:

$ cd [your test folder]
$ git clone https://github.com/enreeco/CanvasApp-NodeJS-Base
$ heroku create my-canvas-app
$ git push heroku master

The app will be pushed to Heroku (if you are using Heroku :) ) and you will find it listening @ https://my-canvas-app.herokuapp.com. Remember to use the "https" protocol in the callback URL setting, otherwise the canvas app won't work correctly when called from Salesforce (using iframes with different protocols makes the browser go creazy).

To make it work you have to set the secret as an Environmental variable (the secret is the Consumer secret field in the App settings).
On Heroku simply type:

$ heroku config:set SF_CANVASAPP_CLIENT_SECRET=XXXXXXXXX

If you access the app outside the canvas this is what you see:

This is what happens in case of error:

This is what happens if all goes well:

The core of this app is the sf-tools/index.js file which handles the decoding of the POST request that contains all the canvas info (such as token, urls, ...): have a look at the verifyAndDecode(input, secret) function.
You can then use this token and urls to make whatever call you want to your instance (as long as your user has access to it), but its outside of this post!

I leave you with a picture of a cute cat, this sould bring here a lot more readers.

Thursday, October 31, 2013

The unwritten guide (or the untold story, you chooose) of a good (badass) Cloudspoker

Here I am, 1 year and a few weeks have passed from the first time I joined Cloudspokes (there have also been a second time...but it's another story). Look for me on the site, I'm known as ForceLogic (former known as Enreeco...you got it, I have 2 accounts..this is the same "other story").

This is the very first community in which I take a significant part: it's not because I'm antisocial, solitary or because I speak a terrible English, but only because I loose interest in the things I do really quickly.

Things changed when I met Cloudspokes.
It seemed strange that I could be paid for my Force.com knowledge, for things I usually did (and currently do) at work, but being away thousands miles.
At first I thought I was not able to compete, and seeing the mith of Jeff Douglas (talking about Salesforce, his blog was constantly one of my landing pages when googling about Force.com) made me think I cloudn't loose time in things I couldn't handle.

I was absolutely wrong!

Lucky thing is that I bookmarked the site and, randomly, one month later I decided to come in and examine in depth what Cloudspokes really was.

A new world opened to me! I started coding in the weekends or during the nights, learning whatever technology I wanted to learn (there is so much choice you can choose the techs you prefer!).
I already was a good Force.com developer but the Cloudspokes experience incresed of a significant 30% my analysis and coding speed and quality, and made me learn part of the Force.com platform that I didn't know.
Once I felt confident with Force.com challenges, I started with other technologies (pure JS client scripting learning different JS libraries, server side programming in Java, desktop programming, HTML5), ending with my favorite techs (as of now), AngularJS and NodeJS and Bootstrap to give a nice style (I'm an engineer and usually I make the things working without attention to their beauty...believe in me, libraries such as Bootstrap makes you feel a good web developer), thanks to Jeff, Dave, Mike and Kyle, the first Cloudspokes masters I met at the beginning of my journey, with the great support of Tim (my Cloudspokes Bro).

They are not the only ones I have met in this year, but I cannot name all this great guys one by one (and you'll meet them when you'll join the cause).

The real question is: What is Cloudspokes?

A community?
A developers hub?
A bunch of mercenary nerds?
A place for gods of web development?
A showroom of cool code?
The Devil (when I'm completely absorbed by a challenge I actually think it is!)?

The best answer I can give is: Cloudspokes is a constant challenge!

You can be a very noob and you win a challenge even if you are competing with the best of the bests!

Suggestion number 1: Never be afraid to compete!

In this scenario, every one must give 100% of his capabilities, from 50$ projects to 1M$ projects (just kidding, no million dollar challenge...as of now!), nothing is certain, once you lower your guard, altough you are the very best, you can loose with shame (It happened to me more than once!).

Suggestion number 2: code each challenge with the same passion and frenzy as it is the first you do!

Remember that you are not the only one competing and think that all the other submissions will be at least as good as yours, so try to enhance the solution with cool stuff, document your code and make cool video demo ("a video worth a thousand documentation pages", cit. @eucuepo).

Suggestion number 3 (only for non English speakers with an English knowledge as low as mine): don't be shy!

Make a video speaking English, in 2 or 3 video you will acquire confidence (even if reinventing English grammar) and you will please the judges (seeing a solution running is different than only reading a doc!).

Suggestion number 4: have fun, sometimes leave apart your familiar skills and dive into something you actually don't know (I made an AngualarJS + NodeJS challenge without even knowing anything).

You may hate what you learned or loose badly the challenge, but at least you are now aware of that tech, and believe me this is a great success for your skills!

Suggestion number 5: during challenges, make questions as much as you can, even if it makes you feel dumb. It can happen that your "dumb" question triggers a thread of cool stuff or ideas or new point of views.

Suggestione number 6: always complete your tasks but if you can't do it (sometimes could also be a matter of time...we have a life!) tell it to the admins and submit anyway. Something cool could be present in your code that could be used away. This doesn't mean that you have to submit every garbage you produce, so try to figure out if what you did could have at least a single bit of value!

Suggestione number 7: when you are "feeling down" because you are not kicking asses, take a deep breath, let it out slowly, take a break, take another breath, relax your mind...the next time will be better! Cloudspokes is growing up, so there will be more and more competition but you will grow up with it and you'll being soon to master the technique of being a badass Cloudspoker.

Suggestion number 8: join actively the community and try to know other Cloudspokers, the fact that you compete with them doesn't mean you are enemies! And watching their submissions is another source of power (feel like a vampire).

Suggestion number 9: I can't find a 9th point, but cool lists on the web have at least 10 points...

Suggestion number 10: if you haven't already done, join us!

Thursday, February 7, 2013

[Github / Maven] Maven repository using GitHub

This simple post is a reminder on how to create a Maven2 repository and use it with Maven in a "pom.xml" file or in Java Play!.

The first step is to create a GitHub Repository (it will be named "maven2").

Then reproduce the folder structure of a Maven Repository, as I did in my "Maven2" repo (https://github.com/enreeco/maven2).

For instance, given this artifact (jar):

<groupId>com.rubenlaguna</groupId>
  <artifactId>evernote-api</artifactId>
  <name>Official Evernote API</name>
  <version>1.22</version>

Create these folders:

./com
     ./com/rubenlaguna
     ./com/rubenlaguna/evernote-api
     ./com/rubenlaguna/evernote-api/1.22
     ./com/rubenlaguna/evernote-api/1.22/evernote-api-1.22.jar
     ./com/rubenlaguna/evernote-api/1.22/evernote-api-1.22.pom

Now you have a pubblicly accessible Maven2 repository at https://github.com/[username]/[repoName]/raw/master/ (in my case https://github.com/enreeco/maven2/raw/master/).

To consume it in your projects, just add this lines in the "pom.xml" file of your project:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.echoservice</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>echoservlet</artifactId>
    <repositories>
    <repository>
      <id>myRepository</id>
      <url>https://github.com/enreeco/maven2/raw/master/</url>
    </repository>
    </repositories>
    <dependencies>
 . . .
 </depencencies>
 . . .
</project>

To integrate the maven repo in a Java Play! project (necessary if you're messing with Heroku), take the "/prject/Build.scala" file and add those lines:

import sbt._
import Keys._
import PlayProject._

object ApplicationBuild extends Build {

    val appName         = "evernote-integration"
    val appVersion      = "0.1"

    val appDependencies = Seq(
      "com.rubenlaguna" % "evernote-api" % "1.22", //Evernote Thrift Library
      "com.rubenlaguna" % "libthrift" % "1.0-SNAPSHOT",//Thrift core library
      "postgresql" % "postgresql" % "9.1-901.jdbc4", //Postgres
      "org.apache.httpcomponents" % "fluent-hc" % "4.2.1" //Apache HTTP client
    )

    val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings(
     resolvers+= "personal maven repo" at "https://github.com/enreeco/maven2/raw/master/"
    )

}

Maven will search for the selected jars in the default locations, but when it's not finding anything it will search for your personal resolvers.

Tuesday, February 5, 2013

[Visualforce / Tip] Always add a <apex:pagemessages/> tag

Always add <apex:pagemessages/> at the beginning of your VF page to see if there are errors in loading/submitting VF page.
If something underneath the Visuarlfoce is wrong, you don't see anything (no clear log at least)!

I was using the automatic SOQL generation of the "Object__c" standard controller, and to do this I usually use (well, I used to do it in the past, now I only use a series of "{!field}" in a hidden <apex:outputPannel>):

     <apex:inputHidden value="{!Object__c.CreatedDate}" />
     <apex:commandButton value="Do something" action="{!doSomething}"/>

This is clearly a blasphemy,because you cannot set with input hidden a "created date" field, but as I hadn't wrote this piece of code myself, I didn't see it immediatly.

Result was that commandLink/ButtonLink was not working, no log provided (exception made for the page load's initialization debug log), everithing I did made the button simply reloading the page without a real postback.

Using a simple <apex:pagemessages/> I saw the error and manage to know what was the problem.

Wednesday, January 30, 2013

[Apex / JavaScript] What is @RemoteAction? Baby don't hurt me, don't hurt me, no more!

2 hours to find out why @RemoteAction was hurting me.
This is related to Cloudspoke's challenge POC - Bootstrap Visualforce Pages (which I haven't submitted neither).

This is the controller:

@RemoteAction
    public static List queryContacts(List filters, String orderBy) {
    ...
    }

And this is the piece of javascript:

  var orderBy;
  var filters = [];
  function searchContacts()
  {
   var ob = orderBy;
   Visualforce.remoting.Manager.invokeAction(
              '{!$RemoteAction.MyPageController.queryContacts}',filters,ob,
              function(result, event){
               
                  if (event.status) {
                   
                   //result has the List of contacts
                   var contacts = result;
                   console.log(contacts);
                  } else if (event.type === 'exception') {
                       alert(event.message);
                  } else {
                    alert(result+ ' '+event.message);
                  }
              }, 
              {escape: true});
  }
It keeps sayng:
Visualforce Remoting Exception: Method 'queryContacts' not found on controller CSChallenge.MyPageController. Check spelling, method exists, and/or method is RemoteAction annotated.

The problem is the javascript variable "orderBy" that is "undefined"!!

By initializing it with:

     var orderBy = '';

@RemoteAction go smoothly.



Courtesy of Doombringer

Friday, January 25, 2013

[Salesforce / Canvas] Set up a minimum Canvas App (Java style)

This new post is about setting up a Force.com Canvas App and doing beautiful things with it. It comes from an old challenge I won (see Cloudspokes.com blog post), evenif in that case I used Javascript to get the session infos.

What is a Canvas App? In simple words it is a way to integrate external custom web application into Force.com platform, with simplified authorization process.
There are two ways of authentication:
  • OAuth 2.0 (GET)
  • Signed Request (POST)
I'm going to use the last method.

The first thing you have to do is creating a new "Connected App" in Setup->App Setup->Create->Apps->Connected Apps and press the "New" button.

Now set the mandatory fields:
  • Name: yuor application name
  • Contact email: your email
  • Callback URL: this is the callback URL used in the OAuth process. Simply set with the app URL
  • Selected OAuth Scopes: permissions associated to the injected session informations
  • Force.com Canvas: yes it is
  • Canvas App URL: endpoint of your app
  • Access method: Signed Request (POST)

For the sake of testing, you can set "http://localhost[:port]" as the endpoint.

Here is my configuration:



If you click in the "Chatter" tab you now see the new app:


The second step is to host a local web app.
To simplify your play here is a simple Java Play! Application that integrates with Force.com Canvas:

https://github.com/enreeco/CanvasApp-Base

To run the app simply follow my previous post [Java Play! / Heroku] Setup Java Play! + Heroku + Eclipse, for the Play part (if you want you can also use Heroku and configure the whole thing to use Heroku, but it can be a bit frustrating when developing, due to the loss of time in deploying).

How it works?

Salesforce call your app making a POST request, sending a "signed_request" parameter that is a big base64 encoded String in which all session infos are stored. This is an example:

jCblE7oIsZanRIeshRkJxtx2Dk1tS2tP3GWL0Yf1o+s=.eyJjb250ZXh0Ijp7InVzZXIiOn QiOmZhbHNlLCJjdXJyZW5jeUlzb0NvZGUiOiJFVVIifSwibGlua3MiOnsiZW50ZXJwcmlzZVVybCI6Ii 9zZXJ2aWNlcy9Tb2FwL2MvMjYuMC8wMERpMDAwMDAwMEg0bXUiLCJtZXRhZGF0YVVybCI6Ii9zZXJ2aW Nlcy9Tb2FwL20vMjYuMC8wMERpMDAwMDAwMEg0bXUiLCJwYXJ0bmVyVXJsIjoiL3NlcnZpY2VzL1NvYX IsImZ1bGxOYW1lIjoiQWRt ..... wiY2xpZW50SWQiOiIzTVZHOUEya04zQm4xN2h2Vn Uuc2FsZXNmb3JjZS5jb20ifQ==

Using the App Consumer Secret (red circle in the first pic) the Java Salesforce Framework SDK decodes this response and verify that it isn't tamped with.
The SDK can be found here, but it has been included in the CanvasApp-Base github under the "com.salesforce.canvas" package.

N.B. I've made some modification to the SignedRequest class (it appears that my own libraries have a new version and the org.apache.commons.codec.binary.Base64 class has different constructors than the one used in the SDK...so if you have problems just replace this class with the one provided by Salesforce).
The Consumer Secret is stored in the "application.conf" file, in the first lines:
     # Force.com User Secret
     canvas.consumer.secret = XXXXXXXX
This value is accessed through the AppProperties class.

This is the result:

The app uses sessions to store Canvas session info, so if you reload the page:

The application now has a valid Session Id that can be used, accordingly to the App Access Level, to work with Salesforce (from queries to metadata describes).

The core of this web app is the "Application.index()" method that stores the logic of the Signed Request handling:
package controllers;

import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;

import com.salesforce.canvas.CanvasRequest;
import com.salesforce.canvas.SignedRequest;

public class Application extends Controller {

 public static Result index() {

  String message = "";
  CanvasRequest canvasInfo = AppProperties.getSessionCanvasRequest(Controller.session());
  if(canvasInfo != null)
  {
   message += "Hi <b>"+canvasInfo.getContext().getUserContext().getUserName()+"</b>! Canvas Session Info has already been given. Enjoy Canvas App!";
  }
  else
  {
   message += "Canvas App Session info not already set.";
   if(request().method()== "POST")
   {
    message += "Incoming POST request...";
    
    //this is the post info got from Force.com POST request
    String[] signedRequest = request().body().asFormUrlEncoded().get("signed_request");
    
    if(signedRequest!=null)
    {
     System.out.println("Request: "+signedRequest[0]);
     try
     {
      canvasInfo = SignedRequest.verifyAndDecode(signedRequest[0], AppProperties.CANVAS_CONSUMER_SECRET);
      AppProperties.setSessionCanvasRequest(canvasInfo,Controller.session());
      message += "Hi <b>"+canvasInfo.getContext().getUserContext().getUserName()+"</b>! Canvas Session Info has just been correctly given. Enjoy Canvas App!";
     }
     catch(Exception e)
     {
      e.printStackTrace();
      message += "Error occurred (see log): "+e.getLocalizedMessage();
     }
    }
    else
    {
     message += "No session info provided.";
    }
   }
  }
  return ok(index.render(message));
 }

}
And this is the "routes" file:
  GET      /                   controllers.Application.index()
  POST     /                   controllers.Application.index()
The "Application.index()" method serves both GET/POST requests.
Finally this is what happens if you access the page using a simple GET request outside Canvas:

That's all.

Tuesday, January 22, 2013

[APEX] Strange automatic casting error from String to ID

Developing some APEX for a customer, I saw this error:
System.StringException: Invalid id: -1

I checked the code and it was something like this:
     if(aString == anSObject.Id){
          ...
     }
So a String object cannot be compared to an ID type if the first is not an ID?
I switched the members:
     if(anSObject.Id == aString ){
          ...
     }
And no error is thrown.

In my experience it is a strange behavior, because I would have expected that the ID field whould have been casted to String (being the second operand) rather then the String object been casted to ID type...but maybe my whole life is a lie!!

Monday, January 21, 2013

[Java Play! / Heroku] Setup Java Play! + Heroku + Eclipse

I'm not a very good developer, I have to admit it.
I always forget, if I don't "play HTML" for a while, how to define a CSS style class, so you can imagine what kind of memory I have (the worst kind), and what kind of problems I have to set an environment to make a new project, if that implies that I have to follow more than 3 steps!.
This post is dedicated to myself of the next month, and I want to show me how to set up a Java Play! Proejct using Heroku and Eclipse, and some other little things.

First, create a new Play! project:
 $ play new my-new-app
              _            _
  _ __ | | __ _ _  _| |
 | '_ \| |/ _' | || |_|
 |  __/|_|\____|\__ (_)
 |_|            |__/

 play! 2.0.3, http://www.playframework.org

 The new application will be created in \playTest\my-new-app

 What is the application name?
 > my-new-app

 Which template do you want to use for this new application?

   1 - Create a simple Scala application
   2 - Create a simple Java application
   3 - Create an empty project

 > 2

 OK, application my-new-app is created.

 Have fun!

Now you have a simple Java Play! project (with a simple controller that says that your new application is ready.
The next step is to tell Heroku that your application is a Play application, by creating the /conf/dependencies.yml

 # Application dependencies

 require:
  - play 2.0.3

And that this application will be a web application (that needs the "play run" script) with the file Procfile ( in the root of the app, and without extension):

 web:    play run --http.port=$PORT $PLAY_OPTS

Now it's time to create a new GIT local repository (that will be pushed to Heroku):
   $ cd my-new-app
   $ git init
   $ git add .
   $ git commit -m "init"

Now it's time of Heroku.
You have to install the Heroku toolbelt (go here for instructions): if you have troubles with SSH keys follow this article).
Create a new Heroku application (it will be available on "http://my-new-app.herokuapp.com"):
   $ heroku create my-new-app

And push your local repository to git:
   $ git push heroku master
   

To open the app directly from the command line (via the browser)
       $ heroku open

If you don't like the name of your Heroku application, simply login to Heroku and change it in the preference panel.

Now install the Eclipse Heroku plugin (see details here).

If you need to set a specific GIT directory (the place where GIT, and Heroku, projects are stored in your local filesystem):
Import your Heroku project using the plugin just installed (Import... -> Heroku application): the project is not properly a Java project (packages are not highligthed with the usual icon, for example). To do this digit:
       $ play ecplipsify
Refresh the project on Eclipse and you see that now the project is correctly "divided" into packages.

To run the project locally, just use play run (http://localhost:9000)).

I haven't found a way to automatically compile the Play project in eclipse, so if I add a new Scala HTML template, I "eclipsify" one more time, to have a reference to the new view classes on Eclipse (the view.html.* classes created from the HTML files). This is also helpfull if you add new referenced libraries (I'll explain how to do this using GitHub in the next article).


Now that you are ready, it's the final countdown for your new killer app!

Thursday, January 17, 2013

[VisualForce] Block Auto Focus on DatePicker

This little post is born to remember how to avoid auto focus on Visualforce page of a DatePicker field, in case it is the first "focusable" field in the page:
 
     //avoid autofocus on date pickers
     function setFocusOnLoad() {}

This is something I have found online some weeks ago, but I can't remember the original post. So if you claim the paternity of this fix, I'll be glad to add a reference link.

Friday, January 11, 2013

[Salesforce / Visualforce] jQueryUI + Dialog + Postback

I wanna show you what happens if you do a postback inside a modal jQuery UI dialog. This is a problem I had some times ago making this challenge on Cloudspokes.

We have a simple controller:

 
public class MyController
{
 public String value{get;set;}
 
 public void anAction()
 {
  if(value != null && value.length()>0)
   ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR ,'Variable set to ['+value+']'));
  else
   ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR ,'Variable not set'));
 }

}

And this is the Visualforce page:

<apex:page controller="MyController" id="myPage">
 
 <apex:includeScript value="{!URLFOR($Resource.UIJQuery1822, 'js/jquery-1.7.2.min.js')}"/>
 <apex:includeScript value="{!URLFOR($Resource.UIJQuery1822, 'js/jquery-ui-1.8.22.custom.min.js')}"/>
    <apex:stylesheet value="{!URLFOR($Resource.UIJQuery1822, 'css/ui-lightness/jquery-ui-1.8.22.custom.css')}"/>
 
 <script language="javascript">

  //escapes Visualforce ID for jQuery
  function esc(myid) {
           return '#' + myid.replace(/(:|\.)/g,'\\\\$1');
        }
        
   j$ = jQuery.noConflict();
   
  //opens the dialog
  function openDialog(dialogId)
  {
   j$(esc(dialogId)).dialog({title:'Dialog window', modal:true});
  }
  
  //copies value from an input to another
  function copyHiddenFields(fromId, toId)
  {
   j$(esc(toId)).val(j$(esc(fromId)).val());
  }
  
 </script> 
 
 <apex:form id="myForm">
 
  <apex:pageMessages />


  <apex:outputPanel id="__dialog" style="display:none;">
   
   <apex:inputtext value="{!value}"/>
   <apex:commandButton value="Submit" action="{!anAction}"/>
   
  </apex:outputPanel>
  <apex:commandButton value="Open popup" onClick="openDialog('{!$Component.__dialog}'); return false;"/>
 </apex:form>
 
 
</apex:page>
This is the result:



If you try to post back nothing happens.
This is because the dialog is detached from the body and in some way the command button is detached from the javascript that makes the postback.
The trick is to use an <apex:actionFunction> component:

  <apex:actionFunction action="{!anAction}" name="doPostback"/>
  <apex:outputPanel id="__dialog2" style="display:none;">
   <apex:outputPanel >
    <apex:inputtext value="{!value}"/>
    <apex:commandButton onclick="doPostback(); return false;" value="Submit"/>
   </apex:outputPanel>
  </apex:outputPanel>
  <apex:commandButton value="Open popup 2" onClick="openDialog('{!$Component.__dialog2}'); return false;"/>


Now the postback works but there is still a problem: the "value" field is never set: the postback doesn't send any field in the dialog.
To accomplish this, you have to use an helper hidden field and some javascript to copy the dialog field into the hidden field (which is outside the dialog):

  <apex:actionFunction action="{!anAction}" name="doPostback"/>
  <apex:inputHidden value="{!value}" id="inputHidden"/>
  <apex:outputPanel id="__dialog3" style="display:none;">
   <apex:outputPanel >
    <apex:inputtext id="inputText" value="{!value}"/>
    <apex:commandButton 
     onclick="copyHiddenFields('{!$Component.inputText}','{!$Component.inputHidden}'); doPostback(); return false;" 
     value="Submit"/>
   </apex:outputPanel>
  </apex:outputPanel>
  <apex:commandButton value="Open popup 3" onClick="openDialog('{!$Component.__dialog3}'); return false;"/>


Append this simple JS function in the "script" tags:

//copies value from an input to another
  function copyHiddenFields(fromId, toId)
  {
   j$(esc(toId)).val(j$(esc(fromId)).val());
  }


Now the postback sends the correct value to the controller.


I leave you with a song for your soul.

Friday, January 4, 2013

[Salesforce / Apex] POST Mutipart/form-data with HttpRequest

17/10/2014: the solution has been improved. Datails at the end of the post.

Grandma says you cannot post a Mutipart/form-data using an HttpRequest in APEX?
Well, if she says this now you can tell her this is no more true!

All comes from a CloudSpokes challenge (here is the link)...at the time of starting the challenge I was absolutely sure I would have ended up the challenge in less than a day: http gets/posts  are not a big problem in APEX...well so it seemed.

To complete the challenge you had to make 4 REST calls (login, book a new upload, upload the file, set permissions): during testing the last step always failed.

This was the first time I jumped in front of this issue.

If you don't want to know what I did, go directly here.
The first thing I noted was that you cannot send a base64 encoded file to a server expecting a binary file...It wans't that obvious to me, because I've never struggled with file encoding.

The first code was something like this:
 
public static HTTPResponse uploadFile(Attachmnet file)
 {
  String boundary = '__boundary__xxx';
  String header = '--'+boundary+'\n';
     + 'Content-Disposition: form-data; name="data"; filename="'+file.name
     +'"\nContent-Type: application/octet-stream\n\n';

  String footer = '\n--'+boundary+'--';
  
  String body = EncodingUtil.base64Encode(file.Body); //encodes the blob into a base64 encoded String
  
  body = header + body + footer;
  
  HttpRequest req = new HttpRequest();
  req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
  req.setMethod('POST');
  req.setEndpoint('http://posttestserver.com/post.php?dir=what_a_wonderful_post');   //COOL site to test form uploads
  req.setBody(body);
  req.setTimeout(60000);
  req.setHeader('Content-Length',String.valueof(body.length()));
  
  Http http = new Http();
      return http.send(req);
 }

Then I was all "Eureka! An encoded string cannot be understood if the server needs a binary", so the only thing to do is to make a concatenation of header + file.Body.toString() + footer! This works only if the Blob comes from a text file (i.e. TXT, XML or CSV files): in these cases you don't have any problem...but with binary data all you have is the error:

Blob is not a valid UTF-8 string

I had to find another way.


Searching the web for "uploading binary data using apex" I found those bad links:
  • http://success.salesforce.com/ideaView?id=08730000000Kr80AAC
  • http://boards.developerforce.com/t5/Apex-Code-Development/Image-upload-using-multipart-form-data/td-p/243335
  • http://boards.developerforce.com/t5/Apex-Code-Development/sending-a-non-ascii-file-via-Http-POST/td-p/116662

That leaded to the block of comments you can see in the challenge's dashboard.

I didn't give up anyway. I had all data needed to send the request so I knew the solution was out there.

First thing was to understand if there was a way to merge Blobs types: it is not possible in APEX if you don't have the original data (in that case you use String concatenation or List of Integers concatenation, if you have bynary data in form of intergers list).
So I came up with the idea to merge header, body and footer using base64 encoded version, something like this:

String encoded = EncodingUtil.base64Encode(Blob.valueOf(header))+EncodingUtil.base64Encode(file.Body)+EncodingUtil.base64Encode(Blob.valueOf(footer));
 req.setBodyAsBlob(EncodingUtil.base64Decode(encoded));

I found that sometimes it worked (after a bit I understood that that times I was extremely lucky!!).

Debugging and searching the web (see this post for example) I came to know that a base64 encoded String could have padding characters because the base64 encoding is done using chunks of 3 bytes (see Google for details), and if data is not multiple of 3 bytes this padding in needed.

So I decided to remove the trailing "=" from each encoded chunck of the body request and paste them together. But it's not the proper way to play with encoded base64 strings, as removing trailing padding needs a reencoding of the original data.

The idea was to remove in some way, without messing with the encoded strings, all trailing padding "=".

For the header string it was simple, because it was simple text and I could have added some blank spaces to get an encoded string without "=". That's:

 
  String boundary = '__boundary__xxx';
  String header = '--'+boundary+'\n';
     + 'Content-Disposition: form-data; name="data"; filename="'+file.name
     +'"\nContent-Type: application/octet-stream';

  String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\n\n'));
  //this ensures no trailing "=" padding
  while(headerEncoded.endsWith('='))
  {
   header+=' ';
   headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\n\n'));
  }

So in practice I add extra spaces before the "\n\n" ending characters till I have an encoded string without padding.

The Blob file is the main problem. I need the unencoded data to get the needed trailing, so I need a String value of the body: even if with that String how can I change the file to avoid the "=" ? As this data can be anything (form txt files to encoded zips), it is not so simple to add some padding character to avoid the "=" padding (not clear I know)...

If the encoded body doesn't contain any trailing "=", now the problem is over, the sum of the encoded header, body and footer works.

The problem is the last 4 bytes of the encoded body. That is from the 0th byte to the N-4th byte of the file I have no problem, becase it is an encoded version without "=" trailing.

How do I encode those last 4 bytes merging them with the footer?

I discovered that the HttpRequest class has a strange behavior: the setBodyAsBlob() and getBody() are complementary for the use I need. That is the following code doens't throw a "Blob is not a valid UTF-8 string" exception:

   Blob body = file.body;
   HttpRequest tmp = new HttpRequest();
   tmp.setBodyAsBlob(body);
   String bodyString = tmp.getBody();
   System.debug('## Output body:'+bodyString );

The result is a messing sequence of characters. Are they properly encoded? Yes they are, this is a kind of test:

Blob decoded4Bytes = EncodingUtil.base64Decode('AA==');
System.debug('FIRST ENCODING: '+EncodingUtil.base64Encode(decoded4Bytes));
HttpRequest tmp = new HttpRequest();
tmp.setBodyAsBlob(decoded4Bytes);
System.debug('LAST ENCODING: '+EncodingUtil.base64Encode(tmp.getBodyAsBlob()));

Using different kind of random encoded data (other that "AA==") the results of encoding, blobbing, httpRequesting (??!!), is always the same.
This is what i needed:

  1. decode the last 4 bytes in blob
  2. append it into an HttpRequest using the "setBodyAsBlob()"
  3. get the body as string with "getBody()"
  4. merge this string with the footer
  5. base64 encode the resulting string
  6. merge the base64 encoding of header, file body (from 0 to N-4th byte), previous merged string
  7. base64 unencoding the resulting string
  8. here you are the Blob you needed!
This is the resulting code:

public static HTTPResponse uploadFile(Attachmnet file)
{
  String boundary = '__boundary__xxx';
  String header = '--'+boundary+'\n';
  body += 'Content-Disposition: form-data; name="data"; filename="'+file.name
    +'"\nContent-Type: application/octet-stream';

  String footer = '\n--'+boundary+'--';
  
  // no trailing padding on header by adding ' ' before the last "\n\n" characters
  String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\n\n'));
  //this ensures no trailing "=" padding
  while(headerEncoded.endsWith('='))
  {
   header+=' ';
   headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\n\n'));
  }
  //base64 encoded body
  String bodyEncoded = EncodingUtil.base64Encode(file.body);
  //base64 encoded footer
  String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
  
  Blob bodyBlob = null;
  //last encoded body bytes
  String last4Bytes = bodyEncoded.substring(bodyEncoded.length()-4,bodyEncoded.length());
  //if the last 4 bytes encoded base64 ends with the padding character (= or ==) then re-encode those bytes with the footer
  //to ensure the padding is added only at the end of the body
  if(last4Bytes.endsWith('='))
  {
   Blob decoded4Bytes = EncodingUtil.base64Decode(last4Bytes);
   HttpRequest tmp = new HttpRequest();
   tmp.setBodyAsBlob(decoded4Bytes);
   String last4BytesFooter = tmp.getBody()+footer;   
   bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded.substring(0,bodyEncoded.length()-4)+EncodingUtil.base64Encode(Blob.valueOf(last4BytesFooter)));
  }
  else
  {
   bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);
  }
  
  if(bodyBlob.size()>3000000)
  { 
   //this a "public class CustomException extends Exception{}"
   throw new CustomException('File size limit is 3 MBytes');
  }
  
  HttpRequest req = new HttpRequest();
  req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
  req.setMethod('POST');
  req.setEndpoint('http://posttestserver.com/post.php?dir=watchdox');  
  req.setBodyAsBlob(bodyBlob);
  req.setTimeout(60000);
  req.setHeader('Content-Length',String.valueof(req.getBodyAsBlob().size()));
  Http http = new Http();
  HTTPResponse res = http.send(req);
  return res;
}

I tested it with different kind of files, dimensions and it always worked. I'd like to know your thoughts.
See ya!

UPDATE

See this improvement to my solution. I'll add the content right here:
public static void uploadFile(Blob file_body, String file_name, String reqEndPoint){
      // Repost of code  with fix for file corruption issue
      // Orignal code postings and explanations
      // http://enreeco.blogspot.in/2013/01/salesforce-apex-post-mutipartform-data.html
      // http://salesforce.stackexchange.com/questions/24108/post-multipart-without-base64-encoding-the-body
      // Additional changes commented GW: that fix issue with occasional corruption of files
      String boundary = '----------------------------741e90d31eff';
      String header = '--'+boundary+'\nContent-Disposition: form-data; name="file"; filename="'+file_name+'";\nContent-Type: application/octet-stream';
      // GW: Do not prepend footer with \r\n, you'll see why in a moment
      // String footer = '\r\n--'+boundary+'--'; 
      String footer = '--'+boundary+'--';             
      String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
      while(headerEncoded.endsWith('='))
      {
       header+=' ';
       headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
      }
      String bodyEncoded = EncodingUtil.base64Encode(file_body);
      // GW: Do not encode footer yet
      // String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));

      Blob bodyBlob = null;
      String last4Bytes = bodyEncoded.substring(bodyEncoded.length()-4,bodyEncoded.length());

      // GW: Replacing this entire section
      /*
      if(last4Bytes.endsWith('='))
      {
           Blob decoded4Bytes = EncodingUtil.base64Decode(last4Bytes);
           HttpRequest tmp = new HttpRequest();
           tmp.setBodyAsBlob(decoded4Bytes);
           String last4BytesFooter = tmp.getBody()+footer;   
           bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded.substring(0,bodyEncoded.length()-4)+EncodingUtil.base64Encode(Blob.valueOf(last4BytesFooter)));
      }
      else
      {
            bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);
      }
      */
     // GW: replacement section to get rid of padding without corrupting data
     if(last4Bytes.endsWith('==')) {
        // The '==' sequence indicates that the last group contained only one 8 bit byte
        // 8 digit binary representation of CR is 00001101
        // 8 digit binary representation of LF is 00001010
        // Stitch them together and then from the right split them into 6 bit chunks
        // 0000110100001010 becomes 0000 110100 001010
        // Note the first 4 bits 0000 are identical to the padding used to encode the
        // second original 6 bit chunk, this is handy it means we can hard code the response in
        // The decimal values of 110100 001010 are 52 10
        // The base64 mapping values of 52 10 are 0 K
        // See http://en.wikipedia.org/wiki/Base64 for base64 mapping table
        // Therefore, we replace == with 0K
        // Note: if using \n\n instead of \r\n replace == with 'oK'
        last4Bytes = last4Bytes.substring(0,2) + '0K';
        bodyEncoded = bodyEncoded.substring(0,bodyEncoded.length()-4) + last4Bytes;
        // We have appended the \r\n to the Blob, so leave footer as it is.
        String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
        bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);
      } else if(last4Bytes.endsWith('=')) {
        // '=' indicates that encoded data already contained two out of 3x 8 bit bytes
        // We replace final 8 bit byte with a CR e.g. \r
        // 8 digit binary representation of CR is 00001101
        // Ignore the first 2 bits of 00 001101 they have already been used up as padding
        // for the existing data.
        // The Decimal value of 001101 is 13
        // The base64 value of 13 is N
        // Therefore, we replace = with N
        // Note: if using \n instead of \r replace = with 'K'
        last4Bytes = last4Bytes.substring(0,3) + 'N';
        bodyEncoded = bodyEncoded.substring(0,bodyEncoded.length()-4) + last4Bytes;
        // We have appended the CR e.g. \r, still need to prepend the line feed to the footer
        footer = '\n' + footer;
        String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
        bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);              
      } else {
        // Prepend the CR LF to the footer
        footer = '\r\n' + footer;
        String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
        bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);  
      }

      HttpRequest req = new HttpRequest();
      req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
      req.setMethod('POST');
      req.setEndpoint(reqEndPoint);
      req.setBodyAsBlob(bodyBlob);
      req.setTimeout(120000);

      Http http = new Http();
      HTTPResponse res = http.send(req);
}