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.
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
Leave a Reply