Merge PDF in Salesforce Using Java, ITextPDF and OAuth 2

Its long time, since i wrote any article because of my busy schedule However this time i came with advance one. In this article we are going to use the J2EE (Servlet) to Merge PDF attachment inside salesforce with the help of OAuth and ITextPDF jar file. The reason of writing this article is that there is no native support by Apex to merge two attachments in Salesforce. Either we have to go for AppExchange product like CongaMerg or Drawloop or we can write our own code in any other language like Java and C# and with the help of REST API we can save get and save attachment in Salesforce.

First we will need to setup the OAuth permission so that our local J2EE application can interact with Salesforce. For this login to Salesforce account and Navigate to “Set up | App Set up | Develop | Remote Access” and enter the information. Be careful about the “Callback URL”. It must match in your code. After creating the “Remote Access”, note “Consumer Key” and “Consumer Secret” which will be needed in your code.

Update : “Remote Access” is renamed to Connected App. So throughout this article, if you see image of “Remote Access” then please consider it as Connected App.

Create Remote Access in Salesforce.com for OAuth 2
Create Remote Access in Salesforce.com for OAuth 2

Get Consumer key and Consumer Secret in Salesforce using Remote Access
Get Consumer key and Consumer Secret in Salesforce using Remote Access

So, we will start with setting up the Log4j for our project. Copy below code in “log4j.properties” file. you will need Apache commons logging jar, which you can find in this article’s source code.

# A default log4j configuration for log4j users.
#
# To use this configuration, deploy it into your application's WEB-INF/classes
# directory.  You are also encouraged to edit it as you like.

# Configure the console as our one appender
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n

# tighten logging on the DataNucleus Categories
log4j.category.DataNucleus.JDO=WARN, A1
log4j.category.DataNucleus.Persistence=WARN, A1
log4j.category.DataNucleus.Cache=WARN, A1
log4j.category.DataNucleus.MetaData=WARN, A1
log4j.category.DataNucleus.General=WARN, A1
log4j.category.DataNucleus.Utility=WARN, A1
log4j.category.DataNucleus.Transaction=WARN, A1
log4j.category.DataNucleus.Datastore=WARN, A1
log4j.category.DataNucleus.ClassLoading=WARN, A1
log4j.category.DataNucleus.Plugin=WARN, A1
log4j.category.DataNucleus.ValueGeneration=WARN, A1
log4j.category.DataNucleus.Enhancer=WARN, A1
log4j.category.DataNucleus.SchemaTool=WARN, A1

Now, create a Servlet with name “StartServet” and following code:

package com.sfdc.pdfmerge.sample;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONObject;
import org.json.JSONTokener;

/**
* Servlet implementation class TestOAuth
*/
public class StartServet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	/**
	 * @see HttpServlet#HttpServlet()
	 */
	public StartServet() {
		super();
		// TODO Auto-generated constructor stub
	}

	public static final String ACCESS_TOKEN = "ACCESS_TOKEN";
	public static final String INSTANCE_URL = "INSTANCE_URL";
	String oauthURL = "";

	private static final Logger log = Logger.getLogger(StartServet.class.getName());

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String sfdcInstance = "https://ap1.salesforce.com";

		String accessToken = (String) request.getSession().getAttribute(ACCESS_TOKEN);
		String code = request.getParameter("code");

		String consumerKey = "3MVG9Y6d_Btp4xp7hZ1hn287sIMQ56.b_dOEBDrKHRbtwByFvFJz0A4ETmj_TbTuu4CsCkknL3ZgoWIzUTBnh";
		String consumerSecret = "4972968186270851852";
		String redirectUri = "https://localhost:8443/PDFMerge_J2EE/TestOAuth?isCallBack=1";

		oauthURL = sfdcInstance + "/services/oauth2/authorize?response_type=code&client_id=" + consumerKey + "&redirect_uri="
				+ URLEncoder.encode(redirectUri, "UTF-8");

		if (accessToken == null) {

			try {
				String isCallBack = request.getParameter("isCallBack");
					log.info("Starting OAuth");
					log.info("Is CallBack = "+isCallBack);

					/**
					 * Initiate HTTP request to get the Authorization code
					 */
					DefaultHttpClient httpclient = new DefaultHttpClient();
					HttpPost httpPost = new HttpPost(oauthURL);
					HttpResponse response1 = httpclient.execute(httpPost);

					/**
					 * If HTTP status code is 302, means Salesforce trying to redirect to authorize the Local System
					 */
					if(response1.getStatusLine().getStatusCode() == 302 && isCallBack == null)
					{
						httpclient.getConnectionManager().shutdown();
						log.info("Got 302 request from Salesforce so redirect it");
						response.sendRedirect(oauthURL);
						return;
					}
					else
					{
						log.info("Got Callback, Now get the Access Token");

						httpclient = new DefaultHttpClient();
						String tokenUrl = sfdcInstance + "/services/oauth2/token";

						log.info("Code - "+code);

						List qparams = new ArrayList();
						qparams.add(new BasicNameValuePair("code", code));
						qparams.add(new BasicNameValuePair("grant_type", "authorization_code"));
						qparams.add(new BasicNameValuePair("client_id", consumerKey));
						qparams.add(new BasicNameValuePair("client_secret", consumerSecret));
						qparams.add(new BasicNameValuePair("redirect_uri", redirectUri));

						HttpPost httpPost2 = new HttpPost(tokenUrl);
						httpPost2.setEntity(new UrlEncodedFormEntity(qparams));

						/**
						 * Always set this Header to explicitly to get Authrization code else following error will be raised
						 * unsupported_grant_type
						 */
						httpPost2.setHeader("Content-Type", "application/x-www-form-urlencoded");

						HttpResponse response2 = httpclient.execute(httpPost2);

						HttpEntity entity2 = response2.getEntity();
						System.out.println(entity2.getContent().toString());

						JSONObject authResponse = new JSONObject(new JSONTokener(new InputStreamReader(entity2.getContent())));
						System.out.println(authResponse.toString());
						accessToken = authResponse.getString("access_token");
						String instanceUrl = authResponse.getString("instance_url");

						request.getSession().setAttribute(ACCESS_TOKEN, accessToken);

						// We also get the instance URL from the OAuth response, so set it
						// in the session too
						request.getSession().setAttribute(INSTANCE_URL, instanceUrl);

						log.info("Response is : "+authResponse);
						log.info("Got access token: " + accessToken);

						/**
						In Below line of code, provide the attachment Ids to merge
						*/
						String pdfUrl = "/PDFMerge_J2EE/pdfmerge?ids=00P900000038KTm&parentId=00690000007GLNj&mergedDocName=MergedDoc.pdf";
						response.sendRedirect(pdfUrl);
				}
			} catch (Exception e) {
					e.printStackTrace();
			} finally {
			}
		}
	}
	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
	}
}

Above servlet is starting/landing servlet and authenticate your application with Salesforce using Consumer Key and Consumer Secret. After Authentication using OAuth it will call another servlet to merge PDF and save back in Salesforce.

Now, create a new Servlet which will actually read the attachment Ids from parameter and merge PDF using “iTextPDF” jar and save back again in Salesforce.

package com.sfdc.pdfmerge.sample;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
public class PDFMergeServlet extends HttpServlet {
	private String accessToken;
	private String instanceURL;

	private static final Logger log = Logger.getLogger(PDFMergeServlet.class.getName());

	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
	    log.severe("PDF Merge Servlet Called");
		String sfdcInstance = req.getParameter("instanceURL");

		accessToken = (String)req.getSession().getAttribute(StartServet.ACCESS_TOKEN);
		instanceURL = (String)req.getSession().getAttribute(StartServet.INSTANCE_URL);

		if (accessToken == null)
			return;

		String ids = req.getParameter("ids");
		String parentId = req.getParameter("parentId");
		String mergedDocName = req.getParameter("mergedDocName");

		List pdfs = new ArrayList();
		ByteArrayOutputStream o = null;
		if (accessToken != null && ids != null && parentId != null)
		{
			Salesforce sfdc = new Salesforce(instanceURL, accessToken);
			for (String id : ids.split(","))
			{
				pdfs.add(sfdc.getAttachment(id));
			}

			o = new ByteArrayOutputStream();
			MergePDF.concatPDFs(pdfs, o, false);

			sfdc.saveAttachment(parentId, mergedDocName, o, "application/pdf");
		}

		try
		{
			if (o != null)
			{
				o.flush();
				o.close();
			}

			for (InputStream pdf : pdfs)
			{
				pdf.close();
			}
		}
		catch (Exception e){e.printStackTrace();}

		resp.setContentType("text/plain");
		resp.getWriter().println("Documents merged successfully");
	}

}

Create a utility Java Class “MergePDF” which is used in above servlet:

package com.sfdc.pdfmerge.sample;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.itextpdf.text.Document;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfImportedPage;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfWriter;

public class MergePDF {

    public static void concatPDFs(List<InputStream> streamOfPDFFiles,
            OutputStream outputStream, boolean paginate) {

        Document document = new Document();
        try {
            List<InputStream> pdfs = streamOfPDFFiles;
            List<PdfReader> readers = new ArrayList<PdfReader>();
            int totalPages = 0;
            Iterator<InputStream> iteratorPDFs = pdfs.iterator();

            // Create Readers for the pdfs.
            while (iteratorPDFs.hasNext()) {
                InputStream pdf = iteratorPDFs.next();
                PdfReader pdfReader = new PdfReader(pdf);
                readers.add(pdfReader);
                totalPages += pdfReader.getNumberOfPages();
            }
            // Create a writer for the outputstream
            PdfWriter writer = PdfWriter.getInstance(document, outputStream);

            document.open();
            BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA,
                    BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
            PdfContentByte cb = writer.getDirectContent(); // Holds the PDF
            // data

            PdfImportedPage page;
            int currentPageNumber = 0;
            int pageOfCurrentReaderPDF = 0;
            Iterator<PdfReader> iteratorPDFReader = readers.iterator();

            // Loop through the PDF files and add to the output.
            while (iteratorPDFReader.hasNext()) {
                PdfReader pdfReader = iteratorPDFReader.next();

                // Create a new page in the target for each source page.
                while (pageOfCurrentReaderPDF < pdfReader.getNumberOfPages()) {
                    document.newPage();
                    pageOfCurrentReaderPDF++;
                    currentPageNumber++;
                    page = writer.getImportedPage(pdfReader,
                            pageOfCurrentReaderPDF);
                    cb.addTemplate(page, 0, 0);

                    // Code for pagination.
                    if (paginate) {
                        cb.beginText();
                        cb.setFontAndSize(bf, 9);
                        cb.showTextAligned(PdfContentByte.ALIGN_CENTER, ""
                                + currentPageNumber + " of " + totalPages, 520,
                                5, 0);
                        cb.endText();
                    }
                }
                pageOfCurrentReaderPDF = 0;
            }
            outputStream.flush();
            document.close();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (document.isOpen())
                document.close();
            try {
                if (outputStream != null)
                    outputStream.close();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }
}

you will also need to map the Servlet URL in “web.xml” file, which you can find in source code attached.
To run above application, enter this URL in address bar “https://localhost:8443/PDFMerge_J2EE/“.

After running above code, first time it will ask you to enter the username and password of salesforce account to allow access to your local application and then it will get all the attachments specified in code and after merging it will again attach it to parent id passed in parameter.

jar files needed to run above code:

  • commons-codec-1.1.jar
  • commons-codec-1.6.jar
  • commons-lang3-3.1.jar
  • commons-lang3-3.1-javadoc.jar
  • commons-lang3-3.1-sources.jar
  • commons-lang3-3.1-tests.jar
  • commons-logging-1.1.1.jar
  • fluent-hc-4.2.1.jar
  • httpclient-4.2.1.jar
  • httpclient-cache-4.2.1.jar
  • httpcore-4.2.1.jar
  • httpmime-4.2.1.jar
  • itextpdf-5.1.0.jar

Also, please note that Salesforce OAuth can be run only on “https” protocol. Below articles are also recommended to read :

Simple guide to setup SSL in Tomcat
Digging Deeper into OAuth 2.0 on Force.com

Download Complete Source code for – Merging PDF Using ITextPDF and OAuth2 on Salesforce using Servlet

Posted

in

, ,

by


Related Posts

Comments

13 responses to “Merge PDF in Salesforce Using Java, ITextPDF and OAuth 2”

  1. sonu Avatar
    sonu

    i am new to salesforce i am confuse about its future scope currently i am small company having 15 employees???

    sir please reply???

    1. JitendraZaa Avatar
      JitendraZaa

      Salesforce has bright future.. DOnt worry, just go ahead with this technology

  2. Michel suman Avatar
    Michel suman

    I don’t have knowledge in java or .net or html,I want to learn salesforce crm to start my career in this. Can i go with this? How much part of coding is for a developer and administrator?

  3. saurabh Avatar
    saurabh

    sir .can you please provide the equivalent description for connectivity through window phone 8.
    Please reply.

    1. JitendraZaa Avatar
      JitendraZaa

      Hi Saurabh,
      Sorry to say but i have not tried like this. You can try to check Mobile SDK of SFDC.

  4. Abhi Tripathi Avatar

    Hi Jitendra, Awesome blog post, but this link https://localhost:8443/PDFMerge_J2EE/ is not working

    1. Jitendra Zaa Avatar

      Hi Abhi, its your local server you need to setup, then only this URL will work

  5. Ed Avatar
    Ed

    Very useful post. Question: Where is the Salesforce class used below located?

    Salesforce sfdc = new Salesforce(instanceURL, accessToken);

    1. Jitendra Zaa Avatar

      Complete Source code is not posted in this article, Please download zip file from link available at end of this post.

      1. Ed Avatar
        Ed

        Gotcha! Thanks.

  6. Varun Nair Avatar
    Varun Nair

    We have a VF page which prints as a pdf, but we need to merge this pdf with other pdf documents located at a different location(public URL) as a single document; please advise

  7. karim Avatar
    karim

    hi jithendra…we are making the rest callouts from saleforce to .net application which is in developing, and hosted locally in private server and later it is hosted with the public server with the IP restriction..such that only office system can hit it.. how it possible do you have any idea…
    when comes to the concept of whitelisting , they wont whitelist the ip ranges of salesforce , as it is not static, and for the security reasons..

    1. karim Avatar
      karim

      with out whitelisting the ip address , we should connect to the localserver , any suggestions

Leave a Reply to Michel sumanCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Jitendra Zaa

Subscribe now to keep reading and get access to the full archive.

Continue Reading