Posted Nov 07 by Jason Ibrahim.
Updated Jun 09 by Marcin Pacyga.

In this tutorial, we utilize AppWorks to generate Content Server Web Reports on the fly from a mobile device.

3800 views. 0 comments.

Tutorial

We will write an HTML web app to trigger web reports from a mobile device running the OpenText AppWorks client. We will be utilizing the angularJS framework for our javascript, Twitter's Bootstrap for responsive design that works on a variety of screen sizes, and OpenText Content Server on the backend to accept images sent to its REST api and automatically create a Web Report based on the content.

If you are unfamiliar with angularJS, please take the time to look at some tutorials online. There are several. Here is one.

Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.
Linus Torvalds

This tutorial also assumes you have an instance of Content Server with Web Reports enabled, as well as administrator privileges on a running OpenText AppWorks Gateway, with the reverse proxy service installed and configured to point to your Content Server instance. The Content Server and Gateway need not be running on the same machine but it can make some things easier if they are.

Getting Started

In order to get started, clone the skeleton repository from git or download it as a zip.

$ git clone git@github.com:jasonaibrahim/ContentServerWorkflow.git

or Download Zip.

Structure

Our angular app will be composed of a single directive and a service. The directive will manage all user input – it will bind the user's tap to a function that opens the camera and allows a user to take a photo or select a photo from a list of photos. The service will take that image and send it to Content Server. In the case of this tutorial, Content Server will process the request and start a Web Report, but the possibiilites you have with it are endless. Finally, our angular service will send the response it receives from Content Server back to the directive, which will display a success message, letting the user know that a Web Report was started successfully.

Step One: The HTML

Open up index.html. This is the first page that gets launched when an AppWorks app is run from the mobile client. First we must define our angular app. We do this by adding “ng-app” as an attribute on any element, typically the outermost element. Here, we will put it on our “body” element and name the angular app “ContentServerWebReports”. Additionally, we will put our directive in this file. We will define both of these in our javascript file.

<body ng-app="ContentServerWebReports">
</body>    

A directive in angular is a way to give functionality to a particular element on a page. Think of writing a function for an element with a certain id in jquery; the concept here is similar, only there are no id's or class names you need to remember, and the power and flexibility achieved through a directive are far superior to what you can get with a jquery function alone.

There are three ways to create a directive: add it as a class, as an html attribute, or as an html element. In this tutorial, we will add it is an attribute on a div element. Here is our directive.

<div cs-file-upload>
    <img src="img/camera.png" width="100"></img>
    <p class="text-muted">Tap to upload a photo to Content Server and create a Web Report.</p>
</div>

As you noticed, we added an html attribute to the div and named it cs-file-upload. We will now define this in the javascript file that contains all of our application logic: app.js. Make sure your HTML looks like this before proceeding.

<!DOCTYPE html>
<html>
<head>
    <title>Enterprise World Content Server Demo</title>

    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/index.css"/>
    <script src="js/vendor/jquery-2.1.1.min.js"></script>
    <script src="js/vendor/angular.js"></script>
    <script src="js/vendor/angular-cookies.js"></script>
    <script src="js/vendor/angular-touch.js"></script>
    <script src="js/app.js"></script>

    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body ng-app="ContentServerWebReports">
    <div class="container">
        <div class="row text-center">
            <div class="col-xs-12 vertical-align">
                <img src="img/logo.png"></img>
                <h5>Mobile</h5>
                <br>
                <br>

                <div cs-file-upload>
                    <img src="img/camera.png" width="100"></img>
                    <p class="text-muted">
                        Tap to upload a photo to Content Server and create a Web Report.
                    </p>
                </div>

            </div>
        </div>
    </div>
</body>
</html>

Step Two: The App

All of our application logic will reside in a single file – app.js. Here we will configure authentication, utilizing the otagtoken in our request headers, we will define our directive, and we will create a service to interface with our Content Server backend. To begin, lets get started with authentication.

Authentication and Configuration

As with any OpenText product, security is a high priority for Content Server and AppWorks. As such, we will need to run some configuration before we can do anything useful with our app, otherwise we will get 401 unauthorized or 403 forbidden errors. To begin, check out the code below.

app.run(function ($cookies, $http, $rootScope, $window) {
    /** bind the url of the gateway to the $rootScope object */
    $rootScope._otagUrl = ($window.otag && $window.otag.serverURL) || '';

    /** configure the $http object to include the otag token in headers of all outgoing requests */
    if ($window.otag && $window.otag.auth) {
        $http.defaults.headers.common.otagtoken = $window.otag.auth.otagtoken;
    }
    else if ($cookies.otagtoken) {
        $http.defaults.headers.common.otagtoken = $cookies.otagtoken;
    }
});

Let's take a moment to talk about what is going on in this block of code. When an app is deployed through AppWorks, a javascript object named 'otag' gets injected and is made globally available. In this object are several key pieces of information we need in our apps. Among other things, it contains the url of the gateway the app is deployed from, and a unique token that gives us access to back end services.

The first thing we are doing is, we are grabbing the url of our gateway and attaching it to the root scope. This is so this app can be deployed on any gateway.

Second, we are checking for the otag object and grabbing the otagtoken from it, then configuring the $http object to include it as a default header. Now the header of every request that goes out will include this token as a value. If there is no otag object bound to the $window's scope, then we fallback to the cookie. This is typically reserved for running AppWorks apps on the browser.

Note: “run” blocks in angular are run before an application is initialized. We do our configuration inside of this block so we can be sure that our app is properly configured before we try to do anything that requires said configuration.

The Directive

The next piece we will crank out is the directive. Our directive will do several things. First, it will attach a hidden file input element to the DOM element we define in our directive. This file input element will do the magic of opening up the camera or photo gallery so the user can select a photo to upload.

Secondly, we will want to show the user some helpful messages indicating what is going on while their request is going through the various stages it can go through. We will define a variable to hold the image source of whatever image is appended to the directive element and a spinner to replace it while the app is busy. Furthermore, we will add a text element to indicate whether the request went through successfully.

The code is below. I will go through each block individually and explain what is going on.

Note: In case you get confused, the format I will follow is I will display the block of code, and then I will describe what that block is doing directly below it.

app.directive('csFileUpload', function (contentServerService, $timeout) {
    return {
        restrict: 'A',
        link: function (scope, element) {

            var input = angular.element('<input type="file">'),
                icon= element.find('img'),
                iconSrc= icon.attr('src');

            /**
             * define the actions that take place when this directive is clicked. this involves the following:
             *  - open the input for the user to select a file.
             *  - select file and pass it to contentServerService to upload to ContentServer
             *  - provide feedback while process is handled by showing a loading indicator
             *  - remove the loading indicator on success, or error
             *  - if error, explain why the error occurred.
             */
            element.bind('click', function () {
                input[0].click();
            });

            input.on('change', function (e) {
                var file = e.target.files[0],
                    textHelper = angular.element('<h3 class="text-success">Web Report started successfully</h3>');

                var reset= function () {
                    icon.attr('src', iconSrc);
                    textHelper.remove();
                };

                icon.attr('src', 'img/spinner.gif');

                contentServerService.uploadFileAndInitiateWebReport(file).success(function (data) {
                    element.append(textHelper);
                    $timeout(reset, 3000);
                    input.val('');
                }).error(function (err) {
                    textHelper.text('Something went wrong: ' + err);
                    textHelper.css('color', 'maroon');
                    element.append(textHelper);
                    $timeout(reset, 3000);
                    input.val('');
                });
            });

            /** styling for the element provides feedback on user touch */
            element.bind('touchstart', function () {
                element.css('background-color', '#9f9f9f');
            });
            element.bind('touchend', function () {
                element.css('background-color', 'transparent');
            });
        }
    }
});

At the top we register a directive named 'csFileUpload' with angular. The restrict option is set to 'A' for attribute. Angular will now search the DOM for a hyphen delimited attribute of the same name, aptly called 'cs-file-upload'. Then, we define a 'link' property for this directive; we will not need a controller, template or any other directive option. Let's get into the link function examine the first block.

var input = angular.element('<input type="file">'),
            icon = element.find('img'),
            iconSrc= icon.attr('src');

Here, we create a file element (var input), we search for an image attached as a child of the directive element, and we pull the 'src' field from that image. This is so later we can replace the 'src' attribute with the url of a spinner while the app is loading a request, and change it back to whatever it is originally when the response is received.

element.bind('click', function () {
    input[0].click();
});

With this block, we are taking the user's tap input, (effectively a click) and we are triggering a jQuery click() action on the input element we defined earlier. This will open up the camera or photo gallery. Pretty simple.

input.on('change', function (e) {
    var file = e.target.files[0],
        textHelper = angular.element('<h3 class="text-success">Web Report started successfully</h3>');

    var reset = function () {
        icon.attr('src', iconSrc);
        textHelper.remove();
    };

    icon.attr('src', 'img/spinner.gif');

    contentServerService.uploadFileAndInitiateWebReport(file).success(function (data) {
        element.append(textHelper);
        $timeout(reset, 3000);
        input.val('');
    }).error(function (err) {
        textHelper.text('Something went wrong: ' + err);
        textHelper.css('color', 'maroon');
        element.append(textHelper);
        $timeout(reset, 3000);
        input.val('');
    });
});

This block of code is where the magic happens for this directive. When we triggered the 'click()' function earlier we opened up the camera for the user. This 'change' function will get fired when the user has actually selected a photo.

The first thing we do is we take the file from the input element. Then we create an element that will display the text “Web Report started successfully” when the response is received.

After this, we have a function that will remove the text element we created above and replace the spinner back to its original image. This won't be called yet.

The next line replaces the image with that of a spinner.

The line after this is where we invoke our service (which we have yet to create) to upload the file to Content Server. The service function will return a promise, so we use '.success' to append the text element we created above (the one that says “Web Report started successfully”), and call the reset() function we defined earlier after a 3 second timeout. To recap what this is doing: user selects a photo –> spinner gets displayed –> image gets uploaded –> success message displayed –> three seconds pass by –> success message and spinner are removed –> file input is cleared for another file.

If the server happens to return an error, we display the error message returned by the server in lieu of the success message we predefined. The reset occurs after three seconds as in the previous case.

element.bind('touchstart', function () {
    element.css('background-color', '#9f9f9f');
});
element.bind('touchend', function () {
    element.css('background-color', 'transparent');
});

This block is purely aesthetic. It takes a touchstart event and changes the background color. It takes a touchend event and removes the background color by setting it to transparent. This is only to give the user some feedback when they are tapping the icon, and to showcase some of the cool things you can do with angular with relatively little effort.

That is the directive. It is fairly straightforward, and I think those with a jQuery background will feel right at home with the code here. Next up is the service.

The Service

Our service will do most of the heavy lifting for our app. It is what will process the file, create the proper form parameters and finally, send the file to Content Server. The code is below.

app.service('contentServerService', function ($http, $rootScope) {
    var self = this;

    self.node = {
        parent_id: 3430,
        type: 144,
        description: 'Initiating a web report from the mobile client (appworks)'
    };

    /**
     * generate a hashcode from the current time. used to force file names to be unique, since photos are all named
     * image.jpg when added from ios devices.
     * @returns {string}
     */
    var dateSnapshotToHashCode = function () {
        var date = new Date().toString(), hash = 0, i, chr, len;
        if (date.length === 0) return hash;
        for (i = 0, len = date.length; i < len; i++) {
            chr   = date.charCodeAt(i);
            hash  = ((hash << 5) - hash) + chr;
            hash |= 0;
        }
        return Math.abs(hash).toString();
    };
    /**
     * private method that takes in a file object and creates the form parameters necessary to
     * upload this file to content server
     */
    var makeFormFromFile = function (file) {
        var formData = new FormData();
        formData.append('name', dateSnapshotToHashCode() + '-' + file.name);
        formData.append('parent_id', self.node.parent_id);
        formData.append('type', self.node.type);
        formData.append('description', self.node.description);
        formData.append('file', file);
        return formData;
    };

    return {
        uploadFileAndInitiateWebReport: function (file) {
            var form = makeFormFromFile(file);
            return $http.post($rootScope._otagUrl + '/contentserver/api/v1/nodes', form, {
                transformRequest: angular.identity,
                headers: {'Content-Type': undefined}
            });
        }
    };
});

There are several pieces so we will go through each section piece by piece.

var self = this;

self.node = {
    parent_id: 3430,
    type: 144,
    description: 'Initiating a webreport from the mobile client (appworks)'
};

In this block, we define our Content Server specific detaiils. We add in the id of the folder we will be uploading files to as 'parent_id', as well as the type of file we will be uploading (144 for document), and a generic description. This solution will work for a static Content Server folder. However, if the folder you will be uploading files to is dynamic, then you will have to come up with a different solution.

var dateSnapshotToHashCode = function () {
    var date = new Date().toString(), hash = 0, i, chr, len;
    if (date.length === 0) return hash;
    for (i = 0, len = date.length; i < len; i++) {
        chr   = date.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0;
    }
    return Math.abs(hash).toString();
};

This block simply takes the current time and generates a unique hash. This is done to preserve uniqueness in filenames when uploading photos from a device to Content Server. Every photo taken on an iOS device is named “image.jpg”, and as you may know, Content Server does not allow duplicate entries with the same name. As such, we will invoke this function before uploading the photo.

var makeFormFromFile = function (file) {
    var formData = new FormData();
    formData.append('name', dateSnapshotToHashCode() + '-' + file.name);
    formData.append('parent_id', self.node.parent_id);
    formData.append('type', self.node.type);
    formData.append('description', self.node.description);
    formData.append('file', file);
    return formData;
};

This block will take the file object as provided by the DOM and turn it into a form, so that it can be submitted to the server. It will be utilizing JavaScript's built in FormData object to create this form. The name field uses the function we described earlier to generate a unique name based on the current time, the parent_id, description and type fields are the ones we defined at the top of this service, and the file field is the file object passed in to this function. After creating this form, we return it to the callling function.

return {
    uploadFileAndInitiateWebReport: function (file) {
        var form = makeFormFromFile(file);
        return $http.post($rootScope._otagUrl + '/contentserver/api/v1/nodes', form, {
            transformRequest: angular.identity,
            headers: {'Content-Type': undefined}
        });
    }
};

This block is the only part of the service that is exposed. Any controller that this service is injected into will only be able to call uploadFileAndInitiateWebReport. Everything we have done up to this point has been a helper method for this.

In it, we simply create the form using the makeFormFromFile() function we defined earlier, and send the request. The url is the otag url we configured earlier, along with our reverse proxy mapping (content server) plus the api prefix, plus the resource we are sending the request to, in this case '/nodes'. Make sure you have your reverse proxy configured correctly – here is an example:

Allowed path patterns:

contentserver/api/*

Proxy Mappings:

contentserver=my-content-server/otcs/cs.exe

Note: $http calls in angular always return promises. By returning the $http call, we are effectively returning a promise. This means that the function this promise is returned to can listen for the success event and do the appropriate thing there. This keeps this service generic enough to use anywhere uploading a file to content server is required.

Dive Deeper: If you are wondering why we have set the Content Type to undefined, it is because rather than trying to figure out the content type for any file that comes in, we can let angular figure it out for us. This is accomplished by setting Content Type to undefined and transforming the request. Angular will take the file and automatically add the proper value in the header.

Conclusion

We have gone through the steps required to build a simple AppWorks app from scratch using the angularjs framework.

To recap, we have created a directive that takes the user's tap and allows her to select a photo. That directive displays a spinner while a request is loading, and a success message when the response is received. That directive also calls a service method which uploads a file to Content Server. This brings us to the service.

We have written a service that creates a form object from a file, creates a unique filename based on the current time, and sends the file to Content Server. This service acts as a blackbox for any other object wishing to upload a file to content server; as such, we use it in our directive to upload files the directive pulls from the user to Content Server.

If you have any questions, please drop a line in the AppWorks Developer Q&A forum.

Screenshots

AppWorks springboard
The view
Choose a photo or take a photo
Success!

External links


Table of Contents

Your comment

To leave a comment, please sign in.