Serving PDF from CSP

I have described here how to generate PDF content using the PDF generation library PDFlib (see www.pdflib.com) from Caché ObjectScript. This document describes how this content can be served from within a Caché CSP application.

Serving files

Serving PDF content that is generated as a file is straightforward if the file resides somewhere in the CSP application's directory tree. A simple link is all that is required, as in:

<a href="/app/pdf/content.pdf">Content</a>

An obvious advantage if this approach is its simplicity. The user can elect to either open the document directly in their browser (if the browser supports this) or right-click on the link and download the document to their computer. There are some disadvantages as well:

If if any of the above items is of concern, the best alternative is probably to use a Caché stream server to supply the content.

Serving streams

Caché's CSP server is perfectly capable of serving any type of content, including PDF. Arbitrary content of arbitrary length is often referred to as streams. Caché includes a generic stream server, %CSP.StreamServer, that can be looked at for some ideas about the basic techniques involved. Note that I do not reccommend using this class directly, as (when it is enabled in Configuration Manager) it will serve any file in the CSP directory tree, including e.g. the source of CSP files, if these are present.

The best way to serve the PDF content is to create a class deriving from %CSP.Page, and implementing either the OnPreHTTP and OnPage methods, or overriding the Page method. (I elected to do the latter.) Linking to a stream server class (assume it is called PDF.Server) is done as follows:

<a href="/app/PDF.Server.cls?file=content.pdf">Content</a>

When the browser requests an URL of this form, the request is passed to the Caché CSP engine, which calls the specified class's Page method, which in turn (when not overridden), calls OnPreHTTP, OnPage and OnPostHTTP, in that order.

To serve PDF content, a minimum of two things need to be done:

  1. The HTTP header ContentType must be set to application/pdf.
  2. The PDF content must be written after the headers.

For example, the following class serves the document with the filename specified in the file URL parameter:

Class PDF.Server Extends %CSP.Page [ ProcedureBlock ]
{

ClassMethod Page(SkipHeader As %Boolean = 1) As %Status [ ServerOnly ]
{
  Set DoPage=(%request.Method'="HEAD")

  Set PDFStream=##class(%File).%New(%request.Get("file"))
  If '$IsObject(PDFStream) Quit %objlasterror
  Set sc=PDFStream.Open("RSU")
  If $$$ISERR(sc) Quit sc

  If 'SkipHeader {
    Set %response.ContentType="application/pdf"
    ;An Expires header is required to get IE to open the document.
    ;Set to 30 seconds from now:
    Set %response.Expires=30
    Do %response.SetHeader("Content-Length",PDFStream.Size)
    Set sc=%response.WriteHTTPHeader(.DoPage)
    If $$$ISERR(sc) Quit sc
  }

  If 'DoPage Quit $$$OK

  Do PDFStream.OutputToDevice()

  Quit $$$OK
}

}

Note that this will serve any file reachable for the Caché process, and it doesn't have to be PDF! Error conditions aren't handled very elegantly either, if e.g. the requested file does not exist, a basic CSP error page will be displayed that gives no clue as to what the problem was.

It is therefore important to do some error checking, and return a meaningful status to the user if a problem was detected (i.e., the requested content doesn't exist or the current user isn't entitled to see it). The way to do this is set a HTTP response indicative of the problem, and return an HTML document with a description. The following code fragment (that should be placed at the top of the Page method described above) demonstrates how to do this:

  Set DoPage=(%request.Method'="HEAD")

  ;Check if user is authorized to view this document:
  If '..CheckPermission() {
    If 'SkipHeader {
      Set %response.Status="401 Unauthorized"
      Set sc=%response.WriteHTTPHeader(.DoPage)
      If $$$ISERR(sc) Quit sc
    }
    If DoPage Do ..Unauthorized()
    Quit $$$OK
  }

In this fragment, CheckPermission is a classmethod that analizes the %request properties and determines if the document request is valid. It returns a boolean indicating this. The Unauthorized classmethod should return an HTML document describing the problem; a minimal example is:

ClassMethod Unauthorized() [ Private ]
{
    &html<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd"> 
<html>
<head>
<title>Unauthorized</title>
</head>

<body>
<h1>Unauthorized</h1>
<p>You are not authorized to view the requested document.</p>
</body>
</html>
>
  Quit
}

Of course, the document could also be served from a file.

Suggesting a download

To suggest to the browser to open a Save As dialogue for the PDF content, and to suggest a filename, the HTTP Content-Disposition header can be used as follows:

  Do %response.SetHeader("Content-Disposition","attachment; filename="_PDFFileName)

Dynamic content

So far, the example code demonstrates how to send PDF content present in a file. It isn't necessary to have a file, though: any (binary) stream will do. For example, the PDF.Lib class's interface to PDFlib's GetBuffer method returns the in-memory generated PDF as a %GlobalBinaryStream, which can be used exactly as described above.

As described here, I have not been able to get the COM class's GetBuffer method working, but those that do may find they can use it similarly. As far as I have been able to determine, this method may either return a string or a stream, so this should probably be checked for, and the code adjusted accordingly.

Example class

I have created a minimal sample class that incorporates the above techniques, that can be used as starting point for an application-specific (and more secure!) version. It can be found here; licensing is the same as for the other PDFlib-related code, and described here. Note that this class, if used as-is, will serve any file on the server's filesystem that the Caché process has access to, so if this class is to be used, be sure to implement the CheckPermission method!

© 2004 Gertjan Klein. ()
Check HTML - Check CSS