Part 1, Part 2, and Part 3 guided you through building an application on the CenturyLink Cloud Platform. The tutorials use our cloud products to upload and search documents. The application is deployed in CenturyLink AppFog and uses CenturyLink Object Storage to store documents. Document metadata is stored in CenturyLink Orchestrate.

In this installment, we will add a customized PDF viewer and comment system. The PDF viewer uses the freely-available PDF.js library. Comments are stored in Orchestrate.

Create the Reader View

The view consists of two panels. The left panel contains a PDF reader, and the right panel contains comments on the document. This view starts out as a skeleton that we build throughout the tutorial.

Create views/read.jade in your text editor and insert the following code:

    extends layout

    block content

      div#container

        div#reader
          div#top-controls
            button#prev Previous
            button#next Next
            |   
            span Page:
              span#page_num
              |  /
              span#page_count
          canvas#the-canvas(style="border:1px solid black;width:100%")
          div#bottom-controls
            button#prev2 Previous
            button#next2 Next

        div#comments

Add Reader Display Styles We need to make some changes to our project's stylesheet to make the reader display correctly.

  1. Open public/stylesheets/style.css in your text editor.
  2. At the end of the file, add the following code:

    div#container {
       display: table;
       position: relative;
       width: 100%;
    }
    
    div#reader {
       display: table-cell;
       width: 74%;
       float: left;
    }
    
    div#comments {
       display: table-cell;
       width: 24%;
       float: left;
       padding: 1%;
    }
    

Adding the PDF.js Library

The PDF.js library allows us to add a custom PDF reader to the frontend of the application. First, we need to download the PDF.js library and place parts of it in our application's project directory.

  1. Download the Stable version of the PDF.js zip file from GitHub.
  2. From the zip file, extract the following files to public/javascripts/.
  3. build/pdf.js
  4. build/pdf.worker.js
  5. web/compatibility.js
  6. Open views/read.jade in your text editor.
  7. At the end of the file, add the following code:
        script(src='/javascripts/compatibility.js')
        script(src='/javascripts/pdf.js')
        script(src='/javascripts/pdf.worker.js')

Note: Jade uses significant whitespace, so make sure the script lines are indented two spaces so they will be included in the template code under the line that reads block content.

Adding the PDF Reader Application Route

The PDF reader application route reads document metadata from Orchestrate.

Create routes/read.js in your text editor and insert the following code:

    var express = require('express');
    var router = express.Router();
    var https = require('https');

    // Load private configuration
    var config = require('../config.js');

    // Orchestrate connection
    var oio = require('orchestrate');
    var db = oio(config.orchestrate_token, process.env.npm_package_config_datacenter);

    /* GET Document reader page. */
    router.get('/:key', function(req, res, next) {
      var tpl = { title   : 'Document Reader',
                  user    : req.user,
                  session : req.session,
                  key : req.params.key };

      db.get('documents', req.params.key)
        .then(function (result) {
          tpl.title = tpl.title + ': ' + result.body.title;
          tpl.document = result.body;

          res.render('read', tpl);
        })
        .fail(function (error) {
          console.log(error);
        });
    });

Add the New Route to the Application

  1. Open app.js in your text editor.
  2. Find the line that reads var documents = require('./routes/documents');.
  3. Below that line, add the following code:
        var reader = require('./routes/read');

Loading PDF Files into the Reader

The PDF.js library needs to circumvent Cross-Object Resource Sharing (CORS) to load PDFs from CenturyLink Cloud Storage. In this section, we will add a reverse proxy to our application that gets around this limitation, and then add the frontend reader code to our reader view.

CORS might seem like an annoying limitation, but it's actually an important element in preventing cross-domain exploits and other potential security problems. Read more about CORS in this article.

Adding a Reverse Proxy Handler The reverse proxy is implemented as Express middleware.

  1. Install the Request package by running the following command in your project directory:
     $ npm install request --save
  1. Open app.js in your text editor.
  2. Find the line that reads var busboy = require('connect-busboy');.
  3. Below that line, add the following code:
    var request = require('request');
  1. Locate the line that reads app.use(bodyParser.json());.
  2. Above that line, add the following code:

        // Reverse proxy to circumvent CORS
        app.use('/proxy', function(req, res) {  
          var url = req.url.replace('/?url=','');
          req.pipe(request(url)).pipe(res);
        });
    

Add Frontend PDF Reader The next step is to add the frontend reader to the document read view we created earlier in "Create the Reader View".

  1. Create the directory views/scripts off of your project directory.
  2. Create a file called views/scripts/pdfreader.js in your text editor.
  3. Edit the file to contain the following code:

        PDFJS.workerSrc = '/javascripts/pdf.worker.js';
    
        var pdfDoc = null,
            pageNum = 1,
            pageRendering = false,
            pageNumPending = null,
            scale = 1.5,
            canvas = document.getElementById('the-canvas'),
            ctx = canvas.getContext('2d');
    
        /**
         * Get page info from document, resize canvas accordingly, and render     page.
         * @param num Page number.
         */
        function renderPage(num) {
          pageRendering = true;
          // Using promise to fetch the page
          pdfDoc.getPage(num).then(function(page) {
            var viewport = page.getViewport(scale);
            canvas.height = viewport.height;
            canvas.width = viewport.width;
    
            // Render PDF page into canvas context
            var renderContext = {
              canvasContext: ctx,
              viewport: viewport
            };
            var renderTask = page.render(renderContext);
    
            // Wait for rendering to finish
            renderTask.promise.then(function () {
              pageRendering = false;
              if (pageNumPending !== null) {
                // New page rendering is pending
                renderPage(pageNumPending);
                pageNumPending = null;
              }
            });
          });
    
          // Update page counters
          document.getElementById('page_num').textContent = pageNum;
        }
    
        /**
         * If another page rendering in progress, waits until the rendering is
         * finished. Otherwise, executes rendering immediately.
         */
        function queueRenderPage(num) {
          if (pageRendering) {
            pageNumPending = num;
          } else {
            renderPage(num);
          }
        }
    
        /**
         * Displays previous page.
         */
        function onPrevPage() {
          if (pageNum <= 1) {
            return;
          }
          pageNum--;
          queueRenderPage(pageNum);
        }
        document.getElementById('prev').addEventListener('click', onPrevPage);
        document.getElementById('prev2').addEventListener('click', onPrevPage);
    
        /**
         * Displays next page.
         */
        function onNextPage() {
          if (pageNum >= pdfDoc.numPages) {
            return;
          }
          pageNum++;
          queueRenderPage(pageNum);
        }
        document.getElementById('next').addEventListener('click', onNextPage);
        document.getElementById('next2').addEventListener('click', onNextPage);
    
        /**
         * Asynchronously downloads PDF.
         */
        PDFJS.getDocument(url).then(function (pdfDoc_) {
          pdfDoc = pdfDoc_;
          document.getElementById('page_count').textContent = pdfDoc.numPages;
    
          // Initial/first page rendering
          renderPage(pageNum);
        });
    
  4. Open views/read.jade in your text editor.
  5. At the end of the file, append the following code:

        script
          | var url = '/proxy?url=#{document.url}';
          include scripts/pdfreader.js
    

Update the Document List Views Now that our reader view and route are ready, we need to change our document listing view to point to the new reader.

  1. Open views/list.jade in your text editor.
  2. Find the line that reads: a(href="#{doc.value.url}") #{doc.value.title}.
  3. Replace it with the following code:
        a(href="/read/#{doc.path.key}") #{doc.value.title}

Test the New Document Reader

Before we continue, let's test the new document reader to make sure everything is working properly.

  1. Run the following command in your project directory.

        $ npm start
    
  2. Visit http://localhost:3000/ in your web browser.
  3. Click Please sign in.
  4. Enter your username and password.
  5. Click Login.
  6. Click List Documents in the menu bar.
  7. Click a document title to get to the new PDF reader view.

    Test Reader

Add Document Comments List

Document comments are displayed in the right-hand panel on the PDF reader view. The comment engine requires both frontend and backend code.

Comments are stored in Orchestrate as "events". Events associate time-ordered data with other Orchestrate records. They are useful for audit trails, logs, and time lines. In this case, they are useful for recording comments on a document ordered by the time and date of the comment.

Document Comment Backend We only need two functions to handle the backend. The first handles adding comments via a form. The second handles listing comments.

  1. Open routes/read.js in your text editor.
  2. Find the line at the end of the file that says module.exports = router;.
  3. Just above this line, insert the following code:
        /* POST API for accepting new comments. */
        router.post('/add-comment/:key', function(req, res, next) {
          // If not logged in, that's an error.
          if (!req.user) {
            console.log("No user for comment.");
            res.status(403).json({ success: false, message: 'You must be logged in to post comments.'});
            return;
          }

          // Comments are stored as events, because that's the easiest.
          db.get('documents', req.params.key)
            .then(function () {
              console.log('So far so good.');

              db.newEventBuilder()
                .from('documents', req.params.key)
                .type('comment')
                .data({
                  user : req.user.username,
                  comment : req.body.comment
                })
                .create()
                .then(function (result) {
                  res.json({ success: true, documentKey: req.params.key });
                })
                .fail(function (err) {
                  console.log(err);
                  res.status(520).json({ success: false, message: "Unknown database error"});
                });
            });
        });

        /* GET API for listing comments. Spits out HTML for ease. */
        router.get('/list-comments/:key', function(req, res, next) {
          var tpl = {};

          db.get('documents', req.params.key)
            .then(function (results) {
              db.newEventReader()
                .from('documents', req.params.key)
                .type('comment')
                .list()
                .then(function (results) {
                  tpl.comments = results.body.results;

                  res.render('list-comments', tpl);
                })
                .fail(function (err) {
                  console.log(err);
                });
            })
            .fail(function (error) {
              console.log(error);
            });

        });

Document Comment Backend View The comment listing function in the backend uses a Jade template to properly display comments from the database.

  1. Create views/list-comments.jade in your text editor.
  2. Edit the file to contain the following code:
        each comment in comments
          div.comment(id="comment-#{comment.path.ref}")
            p
              strong From
              | #{comment.value.user}
              strong  on
              | #{new Date(comment.path.timestamp).toDateString()}
              strong  at
              | #{new Date(comment.path.timestamp).toTimeString()}
            p #{comment.value.comment}

Document Comment Frontend Code The document comment frontend uses jQuery to add comments and list comments without causing a whole page load.

  1. Create views/scripts/comments.js in your text editor.
  2. Edit the file to contain the following code:

        function loadComments () {
          var commentsUrl = '/read/list-comments/' + documentKey;
          $('#comments-content').load(commentsUrl);
        }
    
        $(document).ready(function () {
          // Handle posting comments.
          $(document).on('click', '#send-comment', function (event) {
            var commentText = $('#comment-text').val();
    
            $.post('/read/add-comment/' + documentKey,
                   { comment : commentText },
                   function (data) {
                     $('#comment-text').val('');
                   })
              .fail(function (err) {
                alert("Couldn't post comment. Are you logged in?");
              })
              .always(function () {
                loadComments();
              });
    
          });
    
          $(document).on('click', '#comments-reload', function (event) {
            loadComments();
          });
    
          loadComments();
        });
    

    Document Comment Frontend View

  3. Open views/read.jade in your text editor.
  4. Find the line that says div#comments.
  5. Directly below that line, add the following code:

        div#comments-form
          h3 Add a comment
          form
            textarea#comment-text(style="width:100%" rows="5")
            br
            input#send-comment(type="button" value="Submit")
            input(type="reset" value="Clear")
            input#comments-reload(type="button" value="Reload")
        div#comments-content
    
        script(src='https://code.jquery.com/jquery-1.11.3.min.js')
    

    Note: Remember that indentation is important in Jade. Make sure that this entire block is indented two spaces more than the line above it.

  6. Find the line near the end of the file that says script.
  7. Directly below that line, add the following code:
        | var documentKey = "#{key}";
        include scripts/comments.js
    

Testing Document Comments

Your application is now be ready to store and display document comments. Test it with the following steps.

  1. Run the following command in your project directory.
        $ npm start
    
  2. Visit http://localhost:3000/ in your web browser.
  3. Click Please sign in.
  4. Enter your username and password.
  5. Click Login.
  6. Click List Documents in the menu bar.
  7. Click a document title to display the PDF reader view.

    Test Comments

  8. Enter a comment in the text entry box.

  9. Click Submit.

Deploying the Application

Once you have confirmed that the application is working correctly, it's time to deploy it to the cloud.

Run the following command in your project directory to deploy to the cloud. Replace <your-project-name> with the name of your project.

        $ cf push <your-project-name>

Your updated application is now deployed.

The Next Step

So far, this tutorial has taken you through building and deploying a web application that provides a solid beginning to a document management solution. The application receives uploaded documents, storing them in CenturyLink Object Storage while storing metadata in CenturyLink Orchestrate. The application also provides an interface for commenting on documents, using Orchestrate events to store user comments. In addition, the entire application authenticates users against information stored in Orchestrate.

The next and last article of this tutorial will discuss more steps you can take to improve your application. We will also discuss integrating more cloud services into your application.

Links to the Complete Tutorial Series

Part 1 - Store and Authenticate User Credentials Part 2 - Build a Document Storage System Part 3 - Include Powerful Search Capabilities Part 4 - Add a Customized PDF Viewer and Comment System Part 5 - The Next Step for Your Web Application