Skip to Content

Inside a web application one typically issue is to garantee that a PDF document can always be present in a browser perspective. Exceptionally if this document must be shown besides form data where an end user can change some accounting relevant things and he/she must compare data in the form and in a scanned PDF document. Furthermore it is relevant to know on which device the end user want to execute the UI. Surely some ordinary browser like Internet Explorer and Mozilla Firefox have there own PDF viewer plugin or uses directly the Adobe Acrobat Reader, but other focus groups like iPad users don’t have such plugins inside the Safari browser. In addition every Safari browser version (based-on the Apple operation system) supports sometime more or less to display PDF documents. In one version you can see e.g. only the first page, but you don’t have a page navigation or you cannot zoom into the document. Other versions of the browser doesn’t show the document at all.

Inside this blog I want to show you how you can solve this issue by extending an aBPM scenario with PDF.js an Javascript based PDF viewer from Mozilla.

Under the following link you will find more information about PDF.js. Here you can also download the viewer itself:

https://mozilla.github.io/pdf.js/

In the next paragraph I want to discribe step-by step how you can extend the generated aBPM screnario with PDF.js.

Szenario environment/assumptions:

Inside of the shown aBPM szenario exists another DC that reads attachements from a document management system/ABAP backend system or 3rd-Party System via WebServices. The WebService in our scenario will be executed inside the AEX of the PO System. This iFlow and the DC that executed this WebService call is not part of this blog. This case is mentionable because inside a typically customer project the PDF document is not fix or part of a deployable component or web archive.

1. Deploy PDF.js via Web DC

1.1. After downloading the PDF.js viewer zip file please extract the content on your file system
1.2.Than copy the folder build and web into in new Web DC. In my case common/pdf/war


1.3. Under the source folder I have created an package that contains an provider servlet implementation who loads attachements via using another DC (see szenario environment/assumptions above). In case the read of the attachment was successfully the servlet return the binary content of the document. In case of an exception the error message will be returned in html format.

package <vendor>.common.pdf.provider;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import <vendor>.common.attachment.AttachmentContent;
import <vendor>.common.attachment.ReadAttachmentContentResponse;
import <vendor>.common.bl.service.AttachmentUrlParamEncoder;
import <vendor>.common.bl.service.CommonServiceInvokerLocal;
import <vendor>.common.bl.service.ParamsDto;

/**
 * Servlet implementation class AttachmentProvider
 */
public class AttachmentProvider extends HttpServlet {
	
	private static final long serialVersionUID = 1L;
	@EJB
	CommonServiceInvokerLocal invoker;
	
    /**
     * @see HttpServlet#HttpServlet()
     */
    public AttachmentProvider() {
        super();
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		respond(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		respond(request, response);
	}
	
	@SuppressWarnings("unchecked")
	private void respond (HttpServletRequest request, HttpServletResponse response) throws IOException{
		// Set to expire far in the past.
		response.setHeader("Expires", "Sat, 6 May 1995 12:00:00 GMT");
		
		// Set standard HTTP/1.1 no-cache headers.
		response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");

		// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
		response.addHeader("Cache-Control", "post-check=0, pre-check=0");

		// Set standard HTTP/1.0 no-cache header.
		response.setHeader("Pragma", "no-cache");
		  
		Map params = request.getParameterMap();
		
			try {
				String requestParams = (String) params.keySet().toArray()[0];
				ParamsDto decodedParams = AttachmentUrlParamEncoder.decodeParams(requestParams);
				ReadAttachmentContentResponse responseAtt = invoker.readAttachmentContent(decodedParams.getSenderSID(), decodedParams.getMasterGUID(), decodedParams.getArchiveDocID(), decodedParams.getArchiveDocType(), decodedParams.getBusinessUnit());
				AttachmentContent contentAtt = responseAtt.getAttachmentContentData();
				byte[] content = contentAtt.getContentData();
				response.setContentType(decodedParams.getMimeType());
				ServletOutputStream out = response.getOutputStream();
				out.write(content);
			} catch (Exception e){
				response.setContentType("text/html");
				PrintWriter outPr = response.getWriter();
				outPr.println(e.getMessage());
			}
	}

}

1.4. After creating the servlet the web.xml and web-j2ee-engine.xml must be maintained to make the servlet available and to create a security role that allows to execute the servlet with a security role (security aspect of this DC, unauthorized users are not allowed to read attachment). In this case I assigned the security role to a default BPM UME role (SAP_BPM_TRIGGER_EVENT) of the Netweaver AS Java.

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name><trackname>~common~pdf~war~<vendorname></display-name>
	<servlet>
		<description></description>
		<display-name>AttachmentProvider</display-name>
		<servlet-name>AttachmentProvider</servlet-name>
		<servlet-class><vendor>.common.pdf.provider.AttachmentProvider</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>AttachmentProvider</servlet-name>
		<url-pattern>/AttachmentProvider</url-pattern>
	</servlet-mapping>
	<security-role>
		<description>Servlet Access</description>
		<role-name>SAP_BPM_TRIGGER_EVENT</role-name>
	</security-role>
	<security-constraint>
		<web-resource-collection>
			<web-resource-name>All resources</web-resource-name>
			<url-pattern>/*</url-pattern>
		</web-resource-collection>
		<auth-constraint>
			<role-name>SAP_BPM_TRIGGER_EVENT</role-name>
		</auth-constraint>
	</security-constraint>
	<login-config>
		<auth-method>FORM</auth-method>
	</login-config>
</web-app>

web-j2ee-engine.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-j2ee-engine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="web-j2ee-engine.xsd">
	<spec-version>2.4</spec-version>
	<security-role-map>
            <role-name>SAP_BPM_TRIGGER_EVENT</role-name>
            <server-role-name>SAP_BPM_TRIGGER_EVENT</server-role-name>
      </security-role-map>
      
      <login-module-configuration>
            <login-module-stack>
                  <login-module>
                        <login-module-name>EvaluateTicketLoginModule</login-module-name>
                        <flag>SUFFICIENT</flag>
                  </login-module>
                  <login-module>
                        <login-module-name>EvaluateAssertionTicketLoginModule</login-module-name>
                        <flag>SUFFICIENT</flag>
                  </login-module>
                  <login-module>
                        <login-module-name>BasicPasswordLoginModule</login-module-name>
                        <flag>REQUISITE</flag>
                  </login-module>
                  <login-module>
                        <login-module-name>CreateTicketLoginModule</login-module-name>
                        <flag>OPTIONAL</flag>
                  </login-module>
           </login-module-stack>
      </login-module-configuration>
</web-j2ee-engine>

1.5. Create additionally an ear Project with a dependency to the war DC
1.6. Build the ear DC and deploy

2. Overwrite the UI5TableViewRenderer

Hint: The aBPM framework allows to build an own implementation of existing renderer that can be used in place of the delivered renderer classes. This circumstance will be used to place the PDF.js viewer inside the view container who builds the external frame where all the other UI elements will be placed.

2.1. Inside of the scenario ejb (in my case invoice/ejb) create a new package where all the custom renderers can the stored. In the shown projects it is the package with name <vendor>.custom.renderer

2.2. Here exists a new class called UI5TabletViewRenderer that contains the original content of the aBPM framework class plus custom changes for a splitter UI5 element. The zipper inside the splitter elements allows to change the size of the left and right content area. To make this UI element available in the result there are 3 parts in the original UI5TabletViewRenderer that must be maintained. Those parts are highlighted in the following source code with changes Part<X> start/end:

package <vendor>.custom.renderer;

import java.io.PrintWriter;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

import <vendor>.common.bl.service.AttachmentUrlParamEncoder;
import <vendor>.common.bl.service.ParamsDto;
import <vendor>.invoice.bo.InvoiceFieldsEnum;
import <vendor>.invoice.utility.PropertyHelper;
import com.sap.consulting.abpm.core.bl.entities.AttachmentMetaData;
import com.sap.consulting.abpm.core.bl.entities.Attribute;
import com.sap.consulting.abpm.core.bl.entities.AttributeMetaData;
import com.sap.consulting.abpm.core.bl.entities.BusinessObjectMetaData;
import com.sap.consulting.abpm.core.bl.entities.UIElement;
import com.sap.consulting.abpm.ui.services.renderer.IRenderer;
import com.sap.consulting.abpm.ui.services.renderer.RenderContext;
import com.sap.consulting.abpm.ui.services.renderer.RenderContext.ValueHelpPrerequisiteInfo;
import com.sap.consulting.abpm.ui.services.renderer.ui5.UI5RenderUtils;
import com.sap.consulting.abpm.ui.services.renderer.ui5.UI5SegmentParameters;
import com.sap.consulting.abpm.ui.services.renderer.ui5.UI5ViewParameters;


public class UI5TabletViewRenderer implements IRenderer {

	@Override
	public void render(RenderContext ctx, PrintWriter writer, Object data) {
		UI5ViewParameters viewParams = (UI5ViewParameters) data;
		BusinessObjectMetaData bomd = ctx.getBomd();
		UI5SegmentParameters segmentParams = new UI5SegmentParameters();
		List<String> segmentNames = bomd.getSegmentNames();
		int noSegments = segmentNames.size();
		String headerSegment = null;
		// start creating the main layout container
		// changes Part1 start
		String poSystemUrl = PropertyHelper.readPoSytemUrl();
		writer.append("var oLayoutDataLeft = new sap.ui.layout.SplitterLayoutData('left',{size: '50%'});");
		writer.append("var pdfviewer_html = new sap.ui.core.HTML('pdfviewer_html', {content: '<iframe id=\"pdfviewer\" src=\"".concat(poSystemUrl).
				concat("/<vendor>~common~pdf~war/web/viewer.html?file=../AttachmentProvider?").
				concat(generateDocumentUrlParams(ctx)).concat("\" style=\"width: 100%; height: 99.4%;\" frameborder=\"0\"></iframe>'});"));
		writer.append("pdfviewer_html.setLayoutData(oLayoutDataLeft);");
		writer.append("var oSplitterV = new sap.ui.layout.Splitter('splitterV');");
		writer.append("oSplitterV.setWidth('100%');");
		writer.append("oSplitterV.addContentArea(pdfviewer_html);");
		//maximize PDF in portrait mode in case of orientation change
		writer.append("var sLayoutDataLeftSize = oLayoutDataLeft.getSize();");
		writer.append("function onOrientationChange (){switch(window.orientation){case 0: case 180: sLayoutDataLeftSize = oLayoutDataLeft.getSize(); " +
				"oLayoutDataLeft.setSize('100%'); break; default: oLayoutDataLeft.setSize(sLayoutDataLeftSize); break;}}");
		writer.append("window.addEventListener(\"orientationchange\", onOrientationChange);");
		// changes Part1 end
		
		// changes Part2 start
		//writer.append("var ").append(viewParams.getTargetContainerName()).append("=new sap.ui.layout.VerticalLayout({class:'sapUiSmallMargin',width:'100%'});");
		writer.append("var ").append(viewParams.getTargetContainerName()).append("=oSplitterV;");
		// changes Part2 end
		
		// heading segment is only available for root container!
		if (viewParams.isRoot()) {
			// retrieve the header-segment-name.
			headerSegment = (bomd.getConfiguration() != null && bomd.getConfiguration().getFeatures() != null)
				? StringUtils.trimToNull(bomd.getConfiguration().getFeatures().getHeaderSegmentName())
				: null;	
				
			// render the header-segment on top of all other segments and on top of the tab-bar
			if (headerSegment != null) {
				noSegments--;
				this.renderHeaderSegment(ctx, writer, viewParams, headerSegment);
			}
		}
		
		// continue with all non header-segments...
		if (noSegments > 1) {
			// normal rendering with TabBar and multiple TabItems for each segment
			writer.append("var tabContainer=new sap.m.IconTabBar('FormRootIconTabBar',{expandable:false});")
				.append("tabContainer.addStyleClass('iconTabBarPaddingTop');");			
		}
		else {
			// skip the TabBar usage in case there is only one segment
			writer.append("var tabContainer=new sap.ui.layout.VerticalLayout('FormRootIconTabBar',{class:'sapUiSmallMargin',width:'100%'});");						
		}
		// changes Part3 start
		// add the container to "root" layout container
		//writer.append(viewParams.getTargetContainerName()).append(".addContent(tabContainer);");
		writer.append("oSplitterV.addContentArea(tabContainer);");
		// disable things from aBPM Framework
		writer.append("var thePage = sap.ui.getCore().getElementById(\"formView--FormPage\");");
		writer.append("thePage.setShowHeader(false);");
		writer.append("sap.ui.getCore().getElementById(\"__button0\").setVisible(false);");
		writer.append("sap.ui.getCore().getElementById(\"__button1\").setVisible(false);");
		writer.append("sap.ui.getCore().getElementById(\"formView--showBusinessLog\").setVisible(false);");
		writer.append("var headerToolbarDOMRef = sap.ui.getCore().getElementById(\"__toolbar0\").getDomRef();");
		writer.append("var footerToolbarDOMRef = sap.ui.getCore().getElementById(\"formView--FormularToolbar\").getDomRef();");
		writer.append("oSplitterV.setHeight((footerToolbarDOMRef.offsetTop-headerToolbarDOMRef.offsetHeight)+'px');");
		writer.append("window.addEventListener(\"resize\", function(event) {var headerToolbarDOMRef = sap.ui.getCore().getElementById(\"__toolbar0\").getDomRef();" +
				"var footerToolbarDOMRef = sap.ui.getCore().getElementById(\"formView--FormularToolbar\").getDomRef();" +
				"oSplitterV.setHeight((footerToolbarDOMRef.offsetTop-headerToolbarDOMRef.offsetHeight)+'px');},true);oSplitterV.triggerResize();");
		// changes Part3 end
		
		// now iterate over the segment and create the necessary segment rendering based on the segement renderer 
		for (String segmentName : segmentNames) {
			// Skip the header segment for rending as tab
			if (headerSegment != null && StringUtils.equals(headerSegment, segmentName)) {
				continue;
			}

			String segmentId = bomd.getSegmentIdentifierForName(segmentName);
			// information for segment renderer that is different for each segement
			segmentParams.setSegmentName(segmentName);
			segmentParams.setParentContainerName(segmentId);
			
			if (noSegments > 1) {
				AttributeMetaData amd = bomd.getAttributeForSegment(segmentName);
				
				writer.append("var ").append(segmentId).append("=new sap.m.IconTabFilter({text:'")
					.append(UI5RenderUtils.getSegementName(ctx, amd)).append("',key:'").append(segmentId).append("'});")
					.append("tabContainer.addItem(").append(segmentId).append(");");
			}
			else {
				writer.append("var ").append(segmentId).append("=new sap.ui.layout.VerticalLayout({width:'100%'});")
					.append("tabContainer.addContent(").append(segmentId).append(");");				
			}
						
			// render the segment
			IRenderer segementRenderer = ctx.getFactory().getRenderer(UIElement.SEGMENT, ctx, segmentParams);
			segementRenderer.render(ctx, writer, segmentParams);
			
			// add the layout into a tap-strip
			writer.append(UI5RenderUtils.generateAttributeBinding(segmentId,"bo>attributes/"+segmentId+"/", "setVisible", 
					"bindProperty('visible',", "visibleCV"));
		}
		// write all coding for loading and binding the help value information
		if (viewParams.isRoot()) {
			this.renderHelpValues(ctx, writer);
			this.renderPrerequistes(ctx, writer);
		}
	}
		
	
	private String generateDocumentUrlParams(RenderContext ctx){
		Attribute attachmentAttrib = ctx.getBO().getAttribute(InvoiceFieldsEnum.ATTACHMENTS);
		List<AttachmentMetaData> attachmentMetaDatas = attachmentAttrib.getAttachmentMetaDataList();
		//find the main document
		for (AttachmentMetaData aAttachmentMetaData : attachmentMetaDatas) {
			String encodedParams = AttachmentUrlParamEncoder.getEncodedParams(aAttachmentMetaData.getLink());
			ParamsDto decodedParams = AttachmentUrlParamEncoder.decodeParams(encodedParams);
			// check on isMainDocument
			if (decodedParams.isMainDocument()) {
				return encodedParams;
			}
		}
		return null;
	}
	
	/**
	 * 
	 * @param ctx
	 * @param writer
	 */
	private void renderHelpValues(RenderContext ctx, PrintWriter writer) {
		// create a method call to load all necessary value helps
		Set<String> elements = new HashSet<String>();
		Set<String> attributes = new HashSet<String>();

		for (RenderContext.ValueHelpNeededInfo info : ctx.getNeededValueHelps()) {
			String relationName = this.getCompleteRelationName(info.getAttributeMetaData().getBusinessObjectMetaData(), "");
			relationName += info.getAttributeMetaData().getTechnicalName();
			
			if (!StringUtils.isEmpty(relationName) && !attributes.contains(relationName)) {
				attributes.add(relationName);
			}
			
			elements.add("'" + info.getAttributeName() + "'");
		}

		writer.append("oController.oHelpValueAttributes='").append(StringUtils.join(attributes, ";")).append("';");
		writer.append("oController.oHelpValueElements=[").append(StringUtils.join(elements, ",")).append("];");
	}
	
	/**
	 * 
	 * @param bomd
	 * @param result
	 * @return
	 */
	private String getCompleteRelationName(BusinessObjectMetaData bomd, String result) {
		return (bomd.getRelationName() == null)
			? result //root bo	
			: this.getCompleteRelationName(bomd.getParentBomd(), bomd.getRelationName()) + "/" + result;
	}
	
	/**
	 * 
	 * @param ctx
	 * @param writer
	 */
	private void renderPrerequistes(RenderContext ctx, PrintWriter writer) {
		Map<AttributeMetaData, ValueHelpPrerequisiteInfo> prerequisites = ctx.getPrerequisiteValueHelps();
		for (AttributeMetaData amd : prerequisites.keySet()) {
			ValueHelpPrerequisiteInfo info = prerequisites.get(amd);

			// now provide the Javascript code that triggers an update of the
			// value helps if the
			// value of the according field has changed. This is done by adding
			// a listener to the change event
			String fieldName = UI5RenderUtils.generateValueItemName(amd.getUIElement(), amd.getTechnicalName(), amd.getBusinessObjectMetaData().getId(), info.getLevel());
			String helpValues = StringUtils.join(info.getHelpValues(), ";");

			writer.append(fieldName).append(".attachChange({helpValues:'").append(helpValues).append("'},oController.reloadValueHelps,oController);");
		}
	}
	
	/**
	 * 
	 * @param ctx
	 * @param writer
	 * @param segmentName
	 */
	private void renderHeaderSegment(RenderContext ctx, PrintWriter writer, UI5ViewParameters viewParams, String segmentName) {
		String segmentId = "panel" + ctx.getBomd().getSegmentIdentifierForName(segmentName);
		AttributeMetaData amd = ctx.getBomd().getAttributeForSegment(segmentName);
		
		// render panel around the header segment
		writer.append("var ").append(segmentId).append("=new sap.m.Panel({width:'100%',expandable:false});")
			.append("var gridForm=new abpm.custom.layout.Grid({vSpacing:0,hSpacing:1});")
			.append(segmentId).append(".setHeaderToolbar(new sap.m.Toolbar().addContent(new sap.m.Label({text:'")
			.append(UI5RenderUtils.getSegementName(ctx, amd)).append("'})));")
			.append(segmentId).append(".addContent(gridForm);")
			.append(viewParams.getTargetContainerName()).append(".addContent(").append(segmentId).append(");");
				
		// information for segment renderer that is different for each segment
		UI5SegmentParameters segmentParams = new UI5SegmentParameters();
		segmentParams.setSegmentName(segmentName);
		segmentParams.setParentContainerName(segmentId);
						
		// render the segment
		IRenderer segementRenderer = ctx.getFactory().getRenderer(UIElement.SEGMENT, ctx, segmentParams);
		segementRenderer.render(ctx, writer, segmentParams);		
	}
	
	/**
	 *
	 */
	@SuppressWarnings("unused")
	private class Segment implements Comparable<Segment> {
		private int sortId;
		private String segmentName;

		public Segment(int sortId, String groupName) {
			super();
			this.sortId = sortId;
			this.segmentName = groupName;
		}

		@Override
		public int compareTo(Segment o) {
			int compareResult = -1;
			if (this.sortId == o.getSortId()) {
				if (!StringUtils.isEmpty(this.segmentName)) {
					compareResult = this.segmentName.compareTo(o.getSegmentName());
				}
				else if (StringUtils.isEmpty(o.getSegmentName())) {
					compareResult = 0;
				}
				else {
					compareResult = this.segmentName.compareTo(o.getSegmentName());
				}
			}
			else {
				compareResult = this.sortId - o.getSortId();
			}

			return compareResult;
		}

		public int getSortId() {
			return sortId;
		}

		public String getSegmentName() {
			return segmentName;
		}
	}
}

2.3. Save the changes

3. Create CustomRendererFactory

Now there must be build an CustomRendererFactory that allows to use the custom UI5TableViewRenderer of Point 2 in place of the aBPM Default UI5TableViewRenderer.

3.1. Create a new CustomRendererFactory with the following content:

package <vendor>.custom.renderer;

import com.sap.consulting.abpm.core.bl.entities.UIElement;
import com.sap.consulting.abpm.ui.services.renderer.DefaultRendererFactory;
import com.sap.consulting.abpm.ui.services.renderer.IRenderer;
import com.sap.consulting.abpm.ui.services.renderer.RenderContext;

public class CustomRendererFactory extends DefaultRendererFactory {

	@Override
	public IRenderer getRenderer(UIElement uiElement, RenderContext ctx, Object data) {
		if (uiElement.equals(UIElement.VIEW)) {
			return new UI5TabletViewRenderer();
		} else {
			return super.getDefaultRenderer(uiElement, ctx, data);
		}
	}
}

3.2. Save the content
3.3. Build and deploy the scenario implementation

4. Control the output

4.1. Start a corresponding BPM process and open the approval task
4.2. Now there should appears the following result:

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply