Project

General

Profile

Velocity Web client

Introduction

Starting with v2, VelocityReport is exposing a new Velocity plugin and a servlet on the server side.
This servlet will help you serve web pages and web sites built with velocity templates and Servoy as well as web services of all sorts.
It is using Headless clients to communicate with Servoy, which are instantiated based on 3 levels of usage, configured in a config.json file.

Installation

To test the latest version, download the the zip package from the Files section
Unzip and copy the various folder contents:
  • /plugins into your application_server/plugins
  • /beans into your /application_server/beans (optional, used by the smart client demo solution)
  • /server/webapps/eastwood.war into your /application_server/server/webapps/ (optional, only needed if you use charts and barcodes)

NB: for users of previous versions of VelocityReport (pre v2.0), it's safer to replace everything (including the eastwood.war), since a lot have changed in this version.

Also unzip the reports.zip folder somewhere on your hard drive and remember that location.
The new report folder contains a www subfolder which is where all the web templates for all the solutions live.
You will find in this folder a VelocityWebClient, which contains the templates for the demo site, and a config.json file which will allow to define various parameters per solutions.

Launch Servoy Developer, and browse to the servoy-admin/plugin-settings set the value of your velocityreport.reportfolder to the location of your reports folder. Also set the velocitreport.serverURL to the external URL of your servoy server.

Demo solution

In Developer, import the VelocityWebClient.servoy solution (it's making use of the udm database, if you don't have it, the solution includes sample data)

From then on, you will be able to browse to the sample web pages. On a localhost default install, this will be:
http://localhost:8080/servoy-service/velocity/VelocityWebClient/

Of course on a real server with real solutions you will be wise to use the url_rewrite filter and/or have an Apache httpd front server that would serve as a facade to hide that complex url and make it http://www.yourserver.com/ for example

You will see a few demo pages, including simple list/details pages (completely functional tables and forms), a full featured grid (sortable, with paging, ajax loading and inline editing), and also some other types of output, that are automatically there depending on the extension you choose: PDF, Excel, JSON or XML.

New plugin's method

You can have a look at the Servoy forms. They have no UI at all and only a few methods.

They all have at least a single entry method:

function vr_getContext(request)

You can also have a global vr_getContext(request) method that can act as a master controller and fallback method

This method must return a ResponseObject, which will be created from the plugin using that method:
ResponseObject = plugins.Velocity.createResponse(contextObject, [template], [inline])

You can also set an alternate template depending on the request using this ResponseObject's template property.

The ResponseObject will wrap the context to use to fill your template.
Just like when you create report with VelocityReport the context is a simple JS Object, which can contain any Servoy JavaScript objects.

The ResponseObject can also be used to redirect, forward, serve binary content directly, stream a file from server and manage HTTP error response:
ResponseObject = plugins.Velocity.createRedirectResponse(url)
ResponseObject = plugins.Velocity.createForwardResponse(url)
ResponseObject = plugins.Velocity.createBytesResponse(byte[], [mimeType])
ResponseObject = plugins.Velocity.createFileResponse(file, [mimeType])
ResponseObject = plugins.Velocity.createErrorResponse(errorCode, [errorMessage], [errorLink], [errorException])

request Object parameter

The vr_getContext() method will always receive a request JS object, which will contain ALL the information about the HTTP request, as a simple JavaScript object.

You can ask the type of request using

request.method

It can be GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH

You will be able to retrieve the parameters (for any method), using:
request.parameters (it's an object, with property and value for each parameters received)

There are also other useful object collections you can use:
  • request.headers
  • request.session
  • request.cookies
  • request.extraPaths
  • request.parts (if the request was multipart)
  • request.userGroups (if the user is authenticated)
  • request.query (for parameters passed in the url of the request / if splitQueryAndBody is true)
  • request.bodyParameters (for parameters passed in the body of the request / if splitQueryAndBody is true)
And also single values, like:
  • request.authentication
  • request.body
  • request.characterEncoding
  • request.clientID
  • request.contentLength
  • request.contentType
  • request.extension
  • request.headlessType
  • request.locale
  • request.multipart
  • request.pathInfo
  • request.port
  • request.protocol
  • request.queryString
  • request.remoteAddress
  • request.remoteHost
  • request.requestedForm
  • request.requestURI
  • request.requestURL
  • request.scheme
  • request.sessionID
  • request.userName
  • request.userUID
  • request.receivedAt

Have a look at how all the information in the request Object by putting a break-point in your vr_getContext(request) function and look at the request object in the Developer Variables panel, you'll see that it's a simple and natural JavaScript object like any other objects in Servoy, but it contains everything you need to analyze a client's request (and act accordingly by returning a ResponseObject).

ResponseObject

The ResponseObject holds the response you want to return to a request.

You can create various types of response:
- Context this is a response holding a context that will be used to fill a template
- Redirect - used to redirect (301) to another page
- Forward - used to forward (internal redirect) to another page
- Bytes - used to serve direct data, as bytes, optionally setting a mime type
- File - used to stream a file back to the client
- Error - used to return an HTTP error, with optional message, link and exception

The ResponseObject has many useful properties:
- URL: in case of responseType=Redirect|Forward, allow you to get/set the url of the page
- bytes: in case of ResponseType=Bytes, allow you to get/set the bytes of that response
- file: in case of ReponseType=File, allow you to get/set the file of that response (a JSFile or java.io.File or String path of a file to be streamed back to the client)
- context: for responseType=Context, this property holds the context object you've set
- inline: for responseType=Context, this property allows you to tell the plugin that the template to use is plain html data and not a file name
- mimeType: for responseType=Context|Bytes, this property allows you to set the mime type of the response
- template: for responseType=Context, this property allow you to get/set a template, either a file name or plain html as a String
- errorCode: for responseType=Error, this property allow you to get/set the HTTP error code
- errorMessage: for responseType=Error, this property allow you to get/set a message attached to the error
- errorLink: for responseType=Error, this property allow you to get/set a link attached to the error
- errorException: for responseType=Error, this property allow you to get/set an exception attached to the error
- xmlRootNode: for responseType=Context, this property allow you to get/set the Root node to be used for XML response

The responseObject also has useful methods, that allows you to manipulate headers, cookies and session attributes:
- addHeader(key, value), getHeader(key), removeHeader(key)
- addCookie(key, cookie), getCookie(key), removeCookie(key)
- addSessionAttribute(key, object), getSessionAttribute(key), removeSessionAttribute(key), invalidate()

Cookie object

For cookies, you will first need to construct a Cookie object. You can do so using:
var cookie = plugins.Velocity.createCookie(name, value);

Then the Cookie object can be tweaked using these properties:
- domain: set the domain of the cookie (by default it is the server domain)
- expiryDate: you can set a date in the future to expire this cookie
- maxAge: you can set a duration (in seconds) to expire this cookie
- secure: get/set the Secure flag
- httpOnly: get/set the cookies HttpOnly flag
- path: the path for which this cookie applies (by defaukt it is the requested path)
- sessionBased: set to true if the cookie should be a session cookie (will expire when the client's browser shutdown)
- value: the value of this cookie
- getName(): you cannot change the name of the cookie, if you want to create a new one, remove that one from the ResponseObject first and then use the plugin's createCookie method and add it to the ResponseObject

config.json

You can configure many web applications based on your solutions using this simple method and configured with a simple config.json file.

You can set the "type" of instantation of Headless clients PER solutions in the config.json file.

There are 3 types:
  • application: one headless client is used for all web clients connecting to the solutions pages (in Developer, whatever the type set in config.json, this is the type used, since only one headless client is available).
  • pool: the same mechanism that is used with the REST plugin (allowing you to set a pool size and pool action), but PER solutions, you can then use the session methods of the ResponseObject and the session collection from the request to set/get variables specific to a client.
  • session: one headless client is used per web client session. This last option is by far the most powerful because it allows stateful clients.

NB: session is only really useful on a real server. In developer, you can only instantiate one headless client, so you will not be able to have a real state for more than one client. But if you try it from an application server, you will see that each web client session will have its own state (and each will consume a license!)

There are also a bunch of other things you can configure in your config.json file, like the pages, resources, default mimeType, default ajax response type (JSON or XML), default number format, default date format, pool size and pool action, and for each pages, a template, the ajax type and a mime type.

Also note that pages accept * wildcards.

If you don't define templates for a page, the servlet will try to match automatically with the name of the form you requested, for example, if you requested form1, it will look for form1.html, form1.xhtml, form1.htm, form1.tmpl, form1.vtl

Typically, in the pages object you will define a form as the property, and either a template as a string or an object like this:

SolutionName: {
  type: "sesssion",
  pages: {
    page1: "myTemplate.html",
    pages2 : {ajax: "json", template: "jsonTemplate.json"},
    "*" : {ajax: "xml"}
  }
}

Authentication

If your solution is using the 'mustAuthenticate' flag, then an authentication method will be needed.
There are 3 modes of authentication, and they are configured per solution in the config.json file:
  • authentication: "none" (this is the default)
  • authentication: "basic"
  • authentication: "form"

When authentication is set to "basic", a BASIC HTTP exchange is automatically generated by the Velocity plugin, meaning that a standard login/password dialog will be presented to the user. The values provided by the users are then used to create a headless client with these credentials.

Note that the password will be send in clear text using that method, so you should really only use this using an SSL (https) connection.
Usually the easiest way to create https connection is to have an Apache httpd proxy in front of your Servoy server (this is also useful to easily rewrite complex URLs and is the recommended way to setup a Servoy server in production)

When authentication is set to "form", you will need to provide 3 other properties:
  • loginPage: the name of a login template to use (there's not default, you must provide your own)
  • userParam: the name of the parameter that will contain the user (login/name) in your login form (the name of the input field on your form)
  • passParam (or passwordParam): the name of the parameter that will contain the password in your login form (the name of the input field on your form)

Velocity will automatically forward to this page until the user is authenticated.

Note that authentication only really makes sense when the headless type is set to 'session'.
With 'application' or 'pool', the headless clients are shared, so the first one to identify will be the user set to the headless client until it is released.

With 'application' or 'pool', you might want to define a 'defaultUser', which is equivalent to what some http servers define as the 'anonymous' user.
You can set the solutions's property in config.json:
  • defaultUser: set the the userName of a user defined in users.properties

users.properties

To store users credentials (login/password pairs), Velocity uses a users.properties file.
This allows you to define many login/password pair in a very easy way, like a key/value pair.
Typically the users.properties file will contain:

user1=pass1
user2=pass2
...

When Velocity read this file (all changes will be picked up automatically), the password will automatically be encrypted, the same way your passwords are encrypted within the servoy.properties file.

When using the 'defaultUser' property in a config.json solution's definition, the value of 'defaultUser' will be used to retrieve the related password.
This user/password will then be used to authenticate a client.

Note that when using the 'defaultUser', the user will be automatically authenticated, so you will need to roll your own security (based on sessionID and redirecting to your own login form with a createRedirectResponse or createForwardResponse) to finally identify the user and tie it to a HTTP session.

config.json

The changes in the config.json file will be picked up automatically without the need to restart your application.
As of version 2.2, the config.json file can use #include and #parse to include external files (which must be relative to the config.json location).

You only need to make sure this is valid JSON, because if it is not valid, your configutation will not be loaded!

NB: one limitation of the parser I use is that you cannot put comment into that json file. If you do so, your config will not be loaded.

You an setup many solutions settings in the config.json file. The structure can be:

{
   solution1: {
      ...
   },
   solution2: {
      ...
   },
   etc.
}

You can also omit the outer object, like this:

solution1: {
   ...
},
solution2: {
   ...
},
etc.

Each properties have sensible default, that you can of course override, so that you can define as little as:

solution1: {}

And your solution1 will work fine with the default values.

config.json properties

The set of available properties per solution is:
  • type: one of "application"|"pool"|"session" (see above) - default is "application"
  • poolSize: if type is "pool" you can set the size of the pool (similar to the settings in the REST plugin, except that this is valid PER solution) - default is 5
  • poolAction: if type is "pool" you can set the action to take when the maximum clients is already taken (similar to the REST plugin, it can be one of "fail"|"grow"|"block", default is "block"
  • ajax: set the default ajax response (one of "json"|"xml") default is "json"
  • mimeType: set the default mimeType, default is "text/html"
  • numberFormat: set the default numberFormat to use in your templates, no default
  • dateFormat: set the default dateFormat to use in your templates, no default
  • extractBody: (boolean) allow to define whether you want the whole body of the request to be passed in the request object to Servoy, default is true
  • jsonDateFormat: set the default format for Date Serialization in JSON, one of "internal", "internal+offset", "iso8601", "iso8601+offset", "dotnet", "dotnet+offset", "none" (not case-sensitive), default is "internal", see the FAQ for details on the produced format
  • serializeNull: (boolean) set this to true to serialize null values, default is false and will not serialize null values.
  • oneBasedLoops: (boolean) set this to true to use the Servoy way of iterating on foundsets and datasets, starting with 1, instead of the default 0 based.
  • allowAccept: (boolean) set this to false to by-pass the automatic mimetype response based on the "Accept" header, default is true
  • errorPage: set a default template to use to display errors
  • defaultForm: set a default form to use if one is not found, no default
  • defaultTemplate: set a default template to use if no template is found or set in the response
  • resources: an array paths for files or folders to be served as static resources. (can use * wildcard) The plugin will not attempt to call vr_getContext() to serve these, default is null
  • noCache: an array paths for resources files or folders that should have a Cache-Control: no-store header set (preventing cache of static resources)
  • deny: an array of paths to be denied (returning a 403 Forbidden error by default or a denyCode if defined) to secure some resources (can use * wildcard), default is null
  • pages: an object defining the pages to serve. each property of this object can be a form (can use * wildcard, default is null: meaning the plugin will try to match the requested form with a template of that name (with various extensions, see above)
  • authentication: one of "none"|"basic"|"form" (default to "none"), see above
  • loginPage: name of the login form to use, see above
  • userParam: name of the user input field in your login form, see above
  • passParam (or passwordParam): name of the password input field in your login form, see above
  • defaultUser: default user to use for headless client authentication (must match one of the user defined in users.properties), see above
  • resourcesHeaders: an object allowing to define default headers that will be set when serving static resources
  • services: you can define external services to be used, for more information on how to use and configure this part, see the Velocity Services project Wiki
  • solutionAliases: (String[]) an array of string allowing to define aliases for the solution, to respond to various routes/url
  • mode: (String|String[]) one of "api"|"stateless"|"normal" where normal (which is the default) is the normal mode of the servlet, which will attempt to find a template to fill with the context of the response, where "api" will not attempt it, also "stateless" will not use a session (so the sessionID and session object will not be part of the request object and the addSessionAttribute()/getSessionAttribute()/removeSessionAttribute() will have no effect.
  • maxRequestsPerIP: (positive integer) maximum allowed per IP within the timeFramePerMaxRequests time frame for the solution - default 0 = no maximum.
  • timeFramePerMaxRequests: (positive integer) the time frame (in milliseconds) that defines the period for the maxRequestsPerIP, allowing up to maxRequestsPerIP (and resetting after that amount of time) - default 0 = no timeframe. Both maxRequestsPerIP and timeFramePerMaxRequests are required to define a limit of requests. A rejected requested (that went over the limit) will return a HTTP Error 429, with a "Retry-After" header of timeFramePerMaxRequests (in seconds).
  • denyCode: (integer >= 400 - default = 403), used to define the HTTP error code returned when a url/route is denied
  • strict: (boolean - default=false) when using strict mode, only declared routes will be accepted, any other url will return a 403 or the denyCode (if defined). Accepted routes are the ones defined in the pages collection (as the key itself or the 'form' property) + any wildcard value defined in the 'acceptedRoutes' array
  • acceptedRoutes: (String[]) an array of string (wildcards with * defining no/any characters) used in conjunction of strict mode to define extra routes not declared in the pages collection
  • deserializeDate (boolean) true by default, meaning JSON received from a service will be parse, and in particular the plugin will attempt to parse date strings into Date objects. If you do not want this behavior, so that you can validate and parse the JSON value yourself, you can set this property to false
  • splitQueryAndBody (boolean) true by default, set whether the parameters received will also be added as "query" and "bodyParameters" separate objects to distinguish between parameters passed in the url or in the body of the request.

Solutions pages collection

Each solution object in the config.json can contain a pages collection, which can contain pages with properties.
You can define it in various ways, for example:

solution {
   ...
   pages : {
      form1: "myTemplate.html",
      "other*": "otherGeneric.html",
      "*": "allTheRest.html" 
   },
   ...
}

If you use a * wildcard, remember to put the property in double quotes, otherwise JSON will not understand that property name.

You can also set for each form requested a few properties, like:
  • template: the name of the template to use, the default will be derived from the form name
  • ajax: set the type of the ajax response, one of "json"|"xml", default is the same as the parent solution, or "json" if not defined at the solution level
  • mimeType: set the mimeType to use for this form, default is the same as the parent solution, or "text/html" if not defined at the solution level
  • form: set the form property to override the default form to use(which is the same as the name of the object itself), allowing you to define more complex routing (available with v2b3+)
  • jsonp: name to be used as a function to wrap json object for JSON-P calls (available with v2b4+), default is "jsonp"
  • allowAccept: boolean (default, same as solution), allows to change the global behavior at solution level for the allowAccept behavior

so you can have (for example):

solution {
   ...
   pages : {
      form1: "myTemplate.html",
      form2: { template: "anotherOne.html", mimeType: "application/vnd-excel" },
      form3: { form: "form2", template: "specialTemplate.xhtml", mimeType: "application/pdf" },
      "ajax*": { ajax: "xml" },
      "*": { mimeType: "application/xml" }
   },
   ...
}

Note that by default a page's mimeType will be deduced by the extension of the request.
Also note that for .json extensions, a JSON response will be returned (unless defined otherwise by a mimeType), for .xml it will be an XML response, for .jsonp it will be JSON-P and will try to use the value you've provided to encapsulate json in a function of that name.

Multipart requests

When receiving a multipart request, the multipart boolean will be true and each part received will be parsed into the request.parts array (one object per part), so there's nothing special with receiving multipart requests.

To send multipart requests, 2 methods have been added to the Velocity plugin, and one internal object.

First you create a multipart object, using the following:

MimeMultipart plugins.Velocity.createMultipart()

Will return a MimeMultipart object, allowing to add any number of parts, either String based, or File based.

Once you have a MimeMultipart object, you can add body or file parts, using these 2 methods:

addBodyPart(content String, [header Object])

or
addFilePart(file JSFile, [header Object])

The header object is a JavaScript object with properties/values that will be used as-is in the header of the part.

A related property of the MimeMultipart object can be used to set the MimeMultipart request to use "multipart/related" format or "multipart/form-data" format. By default that value is true, so the request will be a "multipart/related" one.

Once you've filled your MimeMultipart object with all the parts you want, you can issue a post request using:

var result = plugins.Velocity.postMimeRequest(url String, parts MimeMultipart, [callback Function], [String user], [String pass]);

- url should be the url of your service
- parts should be the MimeMultipart object you've already filled,
- callback is optional and used if you want to create an asychronous call, where your script will not be blocked to wait for a response, if not provided the call is synchronous and will return a response directly (or throw and exception if something was wrong).
- user is an optional login String to use to access secure services
- pass is an optional password String to use to access secure services

The callback function if provided will receive either a result or an error, so the signature of your callback function should be something like:

function callbackFromMimeRequest(result, error) {
   // result and/or error can be null/undefined so check them before usage. See the sample MimeMultipartTest solution to see how it works.
}

Note

As usual with Velocity, you will see that it lives up to its name: it renders pages so quickly you will hardly notice it's refreshing a page :)

Please use the forum or create new issues to give me feedback on this stuff.