Search code examples
exist-dbxquery-3.0

eXist-db REST GET request for dynamic pdf - cannot read source file


(eXist 4.4, XQuery 3.1)

I offer the user the ability to download PDF documents which are dynamically created at the moment of request. The request has two parameters: the document name (ie doc=MS609-0002.pdf) and the document language version (ie lang=EN).

The function that outputs is in download.xql:

declare function download:download($node as node(), $model as map(*), $doc as xs:string, $lang as xs:string)
 { 
    ...
    return response:stream-binary($pdf,"application/pdf", $filename)
 }

It outputs a PDF fine in both a direct call in an IDE and if I call the function through an eXist HTML template, for example:

http://localhost:8081/exist/apps/deheresi/download?doc=MS609-0002.pdf&lang=EN

However, using HTML means opening another browser window.

Instead I'd like to request a REST GET from a button. I've looked at the eXist REST documentation and I can't get it to work.

According to the documentation, I should issue a GET structured as follows :

 http://localhost:8081/exist/rest/db/deheresi/download.xql?doc=MS609-0002.pdf&lang=EN

But when make that request, I get :

HTTP ERROR 404
Problem accessing /exist/rest/db/deheresi/download.xql. 
Reason: Document /db/deheresi/download.xql not found

This variation with /exist/rest/apps/: http://localhost:8081/exist/rest/apps/deheresi/download.xql?doc=MS609-0002.pdf&lang=EN

Returns the following message with a blank tree:

This XML file does not appear to have any style information associated with it. The document tree is shown below.

And this variation with /exist/db/apps/: http://localhost:8081/exist/db/apps/deheresi/download.xql?doc=MS609-0002.pdf&lang=EN

Returns:

XQueryServlet Error
Error found
Message: Cannot read source file
/Applications/eXist-db.app/Contents/Resources/eXist-db/webapp/db/apps/deheresi/download.xql

I've tested file permissions and there seems to be no problem. Although there may be a REST permission/configuration requirement that I am not aware of? Are there issues with REST on localhost?

EDIT: this is the full function that should process the REST request:

xquery version "3.1";

module namespace get="/db/apps/deheresi/modules/download”;
declare namespace templates="http://exist-db.org/xquery/templates";
declare namespace tei="http://www.tei-c.org/ns/1.0";
declare namespace xsl = "http://www.w3.org/1999/XSL/Transform";
import module namespace xslfo = "http://exist-db.org/xquery/xslfo";

import module namespace document="/db/apps/deheresi/modules/document" at "/db/apps/deheresi/modules/document.xql";
import module namespace document-view="/db/apps/deheresi/modules/document-view" at "/db/apps/deheresi/modules/document-view.xql";
import module namespace document-preprint="/db/apps/deheresi/modules/document-preprint" at "/db/apps/deheresi/modules/document-preprint.xql";
import module namespace document-print="/db/apps/deheresi/modules/document-print" at "/db/apps/deheresi/modules/document-print.xql";
import module namespace functx="http://www.functx.com" at "/db/apps/deheresi/modules/functx.xql";
import module namespace globalvar="/db/apps/deheresi/modules/globalvar" at "/db/apps/deheresi/modules/globalvar.xqm";


declare function download:download($doc as xs:string?, $lang as xs:string?)
{   (: parse $doc to get name of XML to transform, send back pdf with same name :)

    let $docset := upper-case(substring-before($doc,"."))

    let $filename := concat($docset,".pdf")

    let $document := doc(concat($globalvar:URIdata,concat($docset,".xml")))

    let $language := if (lower-case($lang) = "fr")
                     then lower-case($lang)
                     else "en"

    let $filename := concat($docset,".pdf")

    (: get XSLT stylesheet :)
    let $fostylesheet := document-print:single-doc-fo-stylesheet($language)

     (: get XEP FO config:)
     let $config := util:expand(doc("/db/apps/deheresi/xep.xml")/*)

     (: get xml for transformation in correct language :)
     let $xml := document-preprint:single-doc-preprint($document, $language)

     (: create FO xml :)
     let $fo := util:expand(transform:transform($xml, $fostylesheet, ()))

     (: render pdf :)
     let $pdf := xslfo:render($fo, "application/pdf", (), $config)

     return  response:stream-binary($pdf,"application/pdf", $filename)

};

NB: I've put a bounty on this in hopes of receiving an response which walks through the REST input and output function with an example of getting a PDF that is spontaneously generated. This includes any configuration / permission issues that could affect a REST request.


Solution

  • Since you state the PDF is returned when you call this:

    http://localhost:8081/exist/apps/deheresi/download?doc=MS609-0002.pdf&lang=EN

    Perhaps, what you should be doing is handling that response. A simple example would be this in jQuery using FileSaver.js. (You can google FileSaver.js and download and include that in your pages with jQuery):

    function preview_cover(path){
        var pdffilename="cover.pdf";    
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
            if (this.readyState == 4 && this.status == 200){
                saveAs(this.response, pdffilename);
            }}
        xhr.open('GET', 'cover-formatter.xq?cover=' + path + '&page_width=' + page_width + '&page_height=' + page_height);
        xhr.setRequestHeader('Authorization','Basic ' + sessinfo);
        xhr.responseType = 'blob';
        xhr.send();
    }
    

    The above example will download the PDF using modern browsers (Chrome, Firefox, Edge).

    The code behind is this (I snipped all the other stuff away, just leaving the formatting part):

    let $fo := if ($territory = 'WALES') then util:expand(transform:transform($doc, doc("/db/EIDO/data/edit/xsl/EIDOcoverbilingual.xsl"), $parameters))
    else util:expand(transform:transform($doc, doc("/db/EIDO/data/edit/xsl/EIDOcover.xsl"), $parameters))
    let $pdf := xslfo:render($fo, "application/pdf", (), $config)
    let $headers := response:set-header("Content-Disposition", "attachment;filename=document.pdf")
    return
    response:stream-binary($pdf, "media-type:application/pdf","document.pdf")
    

    Below is a more lengthy jQuery Javascript code that attempts to handle the response at the Javascript side. There are a few tricks to note that I will mention first so as to understand. One hack is that iOS or IE9 browsers cannot handle binary downloads in the browser. So the server-side code actually has a hack to create the PDF and if the browser is iE9 or iOS, it stores the result in the DB (or AWS S3) and returns a link to that PDF so that it can be "clicked" to view. Other common browsers can automatically handle the binary data sent back if done correctly. For this we use FileSaver.js plugin Javascript that will download the PDF.

    Other parts you can ignore frankly. Like logEvent which send an Event to Google Analytics, totformats variable tracks users downloads and limits them in any one session. The hack for Chrome downloads is likely not required as that was a bug in Chrome for Android. adding and loading 'loader' classes are for the GUI. The iE9, iOS solution using IP as a variable that is set, this is because the database is replicated and load balanced in many countries and because the data is written to the DB for this one call, we need the IP address of that exact server that has the result in it. This will go away with S3 integration.

    Essentially the key is that is calls the same URL you would and saves the response using:

    saveAs(this.response, pdffilename);
    

    This is a call into FileSaver.js which handles saving the binary data from an XHR GET and downloading it for you. I have snipped this out from a much larger code which handles all of the downloads including dynamically generated ones from RenderX as yours is, but also static PDFs.

    The call is straightforward, just a GET to customer-formatter.xq which is the same in my case as calling http://localhost/customer-formatter.xq (because I strip out /exist and my post for Jetty is 80):

     xhr.open('GET', 'customer-formatter.xq?masterlang=' + masterlang + '&doclang=' + doclang + '&specialty='+ specialty + '&article=' + docnum + '&user_name=' + loggedInUser + '&territory=' + territory + '&expiry=' + expiry + '&page_width=' + page_width + '&page_height=' + page_height + '&column_count=' + column_count + '&phrasechange=' + phrasechange + '&genlink=' + genlink + '&access=' + access + '&scalefont=' + scalefont + '&skin=' + skin + '&watermark=' + watermarkmsg +'&timestamp=' + timestamp);
    
    
            totformats++;
            if (totformats > maxformats)
                window.location.href = '/user?logout=logout';
            var docfilename = ((doclang) ? doclang : '') + ((doctype) ? doctype : '');
            var pdffilename = docnum + '-' + docfilename + '.pdf';    
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function(){
                if (this.readyState == 4 && this.status == 200){
                    // Do IE9 stuff or iPhone/iPad
                    if (version == 9) {
                        var ip = this.responseText;
                        var a = document.createElement("a");
                        a.style = "cursor: pointer;";
                        document.body.appendChild(a);
                        var url = 'http://' + ip + '/IE9/' + loggedInUser + '-' + docnum + '-English.pdf';
                        a.href = url;
                        $(a).attr('target','_blank');
                        a.click();
                        $(a).remove();
                        $(doc).removeClass('loader');
                        $(doc).prop('disabled',false);
                    }
                    else if (isiOS) {
                        var ip = this.responseText.trim();
                        ioswindow.location.href = 'http://' + ip + '/IE9/' + loggedInUser + '-' + docnum + '-English.pdf';
                        $(doc).removeClass('loader');
                        $(doc).prop('disabled',false);
                    }
                    // Hack to partially fix Chrome error, file is now in Chrome downloads
                    else if (Math.max(document.documentElement.clientWidth, window.innerWidth || 0) <= 1024 && window.chrome) {
                        var blob = new Blob([this.response], {type: 'application/pdf'});
                        var a = document.createElement("a");
                        a.style = "display: none";
                        document.body.appendChild(a);
                        var url = window.URL.createObjectURL(blob);
                        a.href = url;
                        a.download = pdffilename;
                        a.click();
                        window.URL.revokeObjectURL(url);
                        $(doc).removeClass('loader');
                        $(doc).prop('disabled',false);
                    }
                    else {
                        saveAs(this.response, pdffilename);
                        $(doc).removeClass('loader');
                        $(doc).prop('disabled',false);
                    }
                }
            }
            xhr.open('GET', 'customer-formatter.xq?masterlang=' + masterlang + '&doclang=' + doclang + '&specialty='+ specialty + '&article=' + docnum + '&user_name=' + loggedInUser + '&territory=' + territory + '&expiry=' + expiry + '&page_width=' + page_width + '&page_height=' + page_height + '&column_count=' + column_count + '&phrasechange=' + phrasechange + '&genlink=' + genlink + '&access=' + access + '&scalefont=' + scalefont + '&skin=' + skin + '&watermark=' + watermarkmsg +'&timestamp=' + timestamp);
            xhr.setRequestHeader('Authorization','Basic ' + sessinfo);
            if (isiOS) 
                xhr.responseType = 'text';
            else
                xhr.responseType = 'blob';
            xhr.send();
            logEvent(docnum, doclang, 'format', specialty, source, docname);