Skip to main content

An Overview of Phobos

Introduction

Project Phobos is a lightweight, scripting-friendly, web application environment running on the JavaTM platform. With Phobos, developers can write web applications using any number of scripting languages. In doing so, they can take advantage of a very interactive development environment that does away with explicit compilation and deployment steps, all while retaining the power of the Java platform.

This document introduces some basic features of the Phobos architecture that are of particular interest to application developers. A complementary document, the introductory tutorial, shows how all these elements come together in a simple application. You might find it useful to refer to the tutorial while reading this document. We plan on publishing additional documents focusing on more advanced topics in the coming weeks.

Since we currently use JavaScript as the main scripting language in Phobos, this document assumes basic knowledge of the JavaScript language.

Contents

  Architectural Overview
  Platforms and Environments
  Resources and Directory Layout
  Scripts
  The JavaScript Engine
  The Embedded JavaScript Engine
  Views
  Paths
  Controllers
  Predefined Global Variables
  Life cycle Events
  Sessions
  Libraries

Architectural Overview

The following diagram shows the high-level components of Phobos.

diagram

Figure 1: Phobos Architectural Diagram

The parts in blue/green represent the infrastructure on top of which Phobos proper runs. As the next section makes clear, Phobos applications can be deployed on different runtime environments. Typically, you will deploy your Phobos application as regular web application in any compatible servlet container.

The parts in red are the main components of Phobos itself. They include the following:

  • the adapter code that interfaces between the runtime environment and Phobos
  • a set of JSR-223-compliant scripting engines
  • some scripting libraries, either bundled with the available scripting engines or written specially for Phobos
  • some pre-packaged AJAX libraries, like jMaki and the Dojo Toolkit, intended for use inside browser clients
  • some pre-packaged Java libraries, like JDOM and Rome (see the Libraries section for more information)

Finally, the parts in yellow represent elements of an application. These include:

  • scripts which can serve HTTP requests directly
  • controllers and views which work together according to the model-view-controller (MVC) pattern to serve incoming requests
  • other application-related content, including: static HTML pages, CSS files, and templates, to name a few

The following sections explain in more detail each of the concepts that appear in the diagram, plus other elements that we couldn't fit in a single picture but that are important to Phobos developers.

Platforms and Environments

Phobos can run on different platforms. Currently, we support two of them:

  • inside a regular web application
  • in standalone mode (used for IDE integration and unit testing)

The next section details how the platform on which Phobos is running affects the physical layout of an application. For example, it is possible that certain external resources (such as databases) are available only when Phobos is running on a particular platform. Applications running on an application server like GlassFish should use the networked database that ships as part of the server itself rather than using an embedded database instance.

Code that depends on a particular platform can use the globals.platform variable to discover the platform in use.  The values corresponding to the three platforms are as follows:

  • "webapp": inside a web application
  • "standalone": in standalone mode

In addition to platforms, Phobos defines the notion of an environment. An environment is a logical configuration for an application.  Examples of logical configurations are development, testing, and production. During its lifetime, an application will be deployed in different environments, in particular as it progresses from the development stage to the production stage. Environments help make these transitions painless by capturing all the information specific to each of these stages. For instance, developers might want to avoid enabling certain performance optimizations at development time (perhaps because they adversely affect the development cycle) and enable them only during system testing and in production instead.

Phobos expects developers to write applications with one or more environments in mind.  The person responsible for deploying an application can then select the environment it wants to use by using platform-specific means, such as by using system properties.  A running application can find out what environment it is running under by inspecting the globals.environment variable. The default environment is "development".

Resources and Directory Layout

In the context of Phobos, a resource is any file that is accessible to an application. This includes all the scripts and static content (such as HTML pages) that are part of an application.  It also includes all the scripts that compose the Phobos frameworks.

The resources of a Phobos application are organized according to a specific directory layout.
Phobos distinguishes between the virtual directory layout and the physical one. The virtual directory layout is identical across all platforms, whereas the physical directory layout varies with the platform.

In a virtual layout, an application has four directories at the top level:

  • application, which contains all non-content application files with the exception of configuration information.  Examples of files in this directory include scripts, templates, and views
  • environment, which contains configuration information for the environment in which an application is running
  • framework, which contains all framework-specific files
  • static, which contains all static content used by the application, including any pre-packaged client-side libraries, such as the Dojo Toolkit or jMaki widgets.

The reason for keeping static content separate is that it's often possible to use platform-specific mechanisms to serve static content with much higher-performance than any dynamic (i.e. script-generated) one. Segregating static content in its own directory simplifies this task. Environment information is separate from application information to make it easier to share these files across multiple applications.

Inside application code, you always use virtual names to refer to files. So, regardless of the platform in use, you can refer to a static file called index.html by using the name /static/index.html. Note also that virtual names are always absolute, meaning that they start with a slash character. In the future, Phobos might allow URI schemes to appear at the front of a virtual name, thereby permitting references to remote resources. An example could be scripts, templates, or documents stored in a database or generated on the fly by some external application.

Depending on the platform in use, the physical layout of an application might change.  The following sections describe the physical layout in each of the platforms referenced in section Platforms and Environments.

Physical Layout of an Application Running in a Web Application

When running as a standard web application, the application, environment and framework virtual directories are mapped to directories with the same name under the WEB-INF directory. All static content, which is in the static virtual directory, is mapped to the top-level directory of the web application.  Code Example 2 shows the physical layout of an application running in a web application.

  myApplication/
...static files...
WEB-INF/
application
environment
framework

Code Example 2: The Physical Layout of an Application Running in a Web Application

Physical Layout of an Application Running in a Standalone Platform

In this platform, used by the IDE when debugging an application, only one application can run at any given time, and so the physical layout is the same as the virtual one, meaning that no sharing or remapping occurs. The only exception is that the framework directory will most often come from a shared Phobos installation rather than being part of the application.

  application
environment
framework (* may not be present)
static

Code Example 4: The Physical Layout of an Application Running in a Standalone Platform

Scripts

The easiest way to write application code to serve an HTTP request in Phobos is to put it in a script.

A "hello, world" script in JavaScript would look like this:

response.setStatus(200);
response.setContentType("text/html");
writer = response.getWriter();
writer.println("Hello from Javascript!");
writer.flush();

Code Example 5: JavaScript Code that prints out Hello from JavaScript

If you save this file as application/script/hello.js, it will be invoked every time the client tries to access the URL /hello.js. Internally, Phobos compiles scripts on the fly before running them, provided that the scripting engine supports compilation. Any time that a change to a previously run script is detected, Phobos discards the out-of-date, compiled code and recompiles the script anew, without requiring the application to be redeployed. This makes development with Phobos very interactive.

The application/script directory is preconfigured to contain scripts, but you can override this setting by manipulating paths as described in Paths. Incidentally, the special URL, / is mapped to a script called index.js.

The request and response global variables are bound to the request and response objects respectively. Although the actual class of the objects depends on the platform in use, in most cases these objects will conform to the contract for the javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse classes, with the exception of any servlet container-specific methods. Note that sessions are handled differently in Phobos than in a regular servlet container. See the Sessions section for more details.

Phobos uses JSR-223 to invoke any scripts. As a consequence, scripts can be written in any language as long as there is a JSR-223-compliant scripting engine for it in the classpath. Phobos comes with two built-in scripting engines:

You can download more engines from the scripting project site on java.net.

At this stage, JavaScript is the language that we recommend for most application code. Phobos itself is written mostly in JavaScript, and the framework assumes that the language you use is capable of calling into JavaScript. At this time, the task of calling into JavaScript from another scripting language is a complicated one, requiring knowledge of the internals of the Mozilla Rhino scripting engine. Of course, it's possible to define a first-class interface to JavaScript in other scripting languages (much like the Java interfaces they already provide) in order to get easy access to Phobos libraries as a byproduct of that interface.

The JavaScript Engine

The JavaScript engine in Phobos is based on the one in the Java Platform, Standard Edition 6 (Java SE 6), code-named Mustang, and it uses Mozilla Rhino for its operations. Compared to the JavaScript engine in Mustang, the one in Phobos has these additional capabilities:

  • Support for the JavaScript language extensions for XML (E4X)
  • Compilation to bytecode
  • Ability to subclass Java classes (subject to any security limitations specific to a platform)
  • Extensible top-level functionality

E4X allows the use of XML literals and XPath-like expressions inside JavaScript code. For example, the "hello, world" example in Code Example 5 can be rewritten as follows.  Note the lack of double quotes around the HTML text in the first line of code.

var text = <html><head><title>Hello</title></head><body>Hello from Javascript!</body></html>;
response.setStatus(200);
response.setContentType("text/html");
writer = response.getWriter();
writer.println(text);
writer.flush();

Code Example 6: Hello JavaScript Example using XML literals

The Embedded JavaScript Engine

The embedded JavaScript engine allows you to embed JavaScript statements and expressions inside an HTML file, an XML file, or a text file, much like PHP does.

The extension that identifies Embedded JavaScript files is .ejs. The body of an .ejs file is copied verbatim to the standard writer.  In most scripting languages, you can obtain this writer (defined by JSR-223) by evaluating context.writer.

Statements and expressions surrounded by special markers, on the other hand, are evaluated at runtime.  This facility is analogous to scriplets in JSP.  Code Example 7 shows the syntax of an expression.  Code Example 9 shows the syntax of a statement.

 <%= ... a JavaScript expression ... %>

Code Example 7: Expression Syntax

When encountering the preceding code, the Embedded JavaScript engine evaluates the expression and prints its result as a string to the writer. For example,  the following script prints the string "Two and two is 4.0" to the writer.
Two and two is <%= 2 + 2 %>

Code Example 8:  An Example Expression

The following code shows the syntax of a statement in a .ejs page.

 <% ... JavaScript statements ... %>

Code Example 9: Statement Syntax

When it encounters the preceding code, the Embedded JavaScript engine executes the quoted statements without sending anything to the writer. For example, the following code prints the string "Two and two is 4.0" to the writer and it silently assigns the string "silent assignment" to local variable a.
Two and two <% var a = "silent assignment" %> is 
<% context.getWriter().print(2 + 2) %>

Code Example 10: An Example Statement

Although .ejs files are scripts in their own right, they are especially well-suited to defining the views of an application.  The following section explains views, as defined by Phobos.

Views

It is good practice to separate the code that processes a request from that which renders a page to be sent back to the browser. The job of scripts and controllers is to process a request; rendering a page is the domain of the view.

The following script makes a simple demonstration of separating a page request for the rendering of that page.  The path to the page is  /application/view/status.ejs.  The script itself might be located in /application/static/status.js.

model = { message: "everything is fine" };
library.view.render("/application/view/status.ejs");

Code Example 11: A Script that Renders a Page

The preceding script renders a view using the library.view.render function. The argument is the name of a view resource, in this case an embedded JavaScript file, which is shown in Code Example 12.
<html>
<head>
<title>Status</title>
</head>
<body>
<%= model.message %>
</body>
</html>

Code Example 12: A Sample Embedded JavaScript View

By convention, the script and the view communicate using a global variable called model, but you are free to use any other mechanism.

If you compare this script and view with a straightforward script from the  Scripts section, you'll see that the code to set the status code and content type for the response is gone. This task is performed automatically by the library.view.render function.

Paths

It would be tedious to write a long string like "/application/view/status.ejs" every time you want to refer to a given view. As a shorthand, Phobos defines several paths to commonly used resources. Library functions that accept a resource name as a parameter will resolve any relative names (those not starting with a slash character) using the path appropriate to the resource they are trying to resolve.

For instance, the library.view.render function takes the name of a view resource as an argument. Therefore, Code Example 11 can be rewritten as follows:

model = { message: "everything is fine" };
library.view.render("status.ejs")

Code Example 13: A Script That Renders a View Without Requiring a Full Path

The render function will try to locate a resource called status.ejs on the path for view resources, which includes /application/view.   As a result, the render function  resolves  to /application/view/status.ejs.

All predefined paths live inside the application.path object. There are paths for controllers, libraries, modules, scripts and views. The corresponding variables are:

  application.path.controller
application.path.library
application.path.module
application.path.script
application.path.view

Code Example 14: Paths Used by the Phobos Libraries to Resolve Resources

A path is simply a list of strings. Application code can change or extend predefined paths by using the usual array functions. Since this changes the configuration of the application, it is typically done at startup time. For instance, to add the application/myscripts virtual directory to the script path, you can write:

  application.path.script.push("/application/myscripts");

Code Example 15: Code to Add a Path to a Set of Resources

Controllers

Because scripts are mapped to URLs that contain the script name, the scripts themselves cannot be encapsulated like an object.  Furthermore, the task of creating lots of different scripts to handle multiple related actions, like displaying and updating data on a form can become tedious. For this reason, Phobos defines controllers.

A controller is an instance of a controller class located in a sub-namespace of the controller namespace. The linking between request URLs and controllers is done by naming convention, but is customizable by the developer. For example, if the Phobos framework receives a request for the following URL, /form/update, it looks for a controller class named Form in the form namespace of the controller hierarchy.

/form/update

Code Example 16: A URL to a Function on a Controller Class

Upon finding this class, the framework instantiates it and looks for an update method. If it finds one, it invokes it.

JavaScript supports a prototype-based object system but no built-in notion of a packaging namespace.  To ensure that the controller mechanism works well with JavaScript, the Phobos framework introduces some special naming conventions and library functions.  To illustrate, let's take a more in-depth look at how Phobos invokes the form controller's update method:

  1. A controller is part of a controller namespace.  This namespace is represented by files in the controller path, which by default is  /application/controller. Therefore, when looking for the form namespace, Phobos tries to load a script called /application/controller/form.js.
  2. After the Phobos framework loads the script, the namespace is accessible as an object under the name controller.form, in which controller is a global variable that represents the root of the controller hierarchy.
  3. To instantiate the Form class in the form namespace in the controller hierarchy, Phobos tries to resolve the expression controller.form.Form. To be identified as a class, the controller class must be of type function. This function is the constructor, and the newly created object will be the controller instance for this invocation.
  4. To invoke the update method on a controller instance, Phobos looks for a function-valued property called update on that object. If it finds one, it invokes it with the controller instance bound to the this variable.

The following code shows the piece of the form.js file that shows how to define a constructor for a controller called form. 

library.common.define(controller, "form", function() {
this.Form = function() {
this.update = function() {
// ... code to update the form goes here ...
}
}
});

Code Example 17: Code to Define a Controller Called form

The library.common.define library function is used to define a namespace as a child of another.  In this case, it is defining form as a child namespace of  controller. The properties assigned to the this object inside the body of the library.common.define are the externally visible members of the namespace. The anonymous function that appears as the third element in the call to define is necessary to delay evaluation of its body to a later time so that define can avoid uselessly redefining a namespace multiple times.

It is worth pointing out that this is bound to different objects at different times:

  • In this.Form, this is bound to the object that represents the namespace being defined.
  • In this.update, this is bound to the object being constructed, which is the controller instance.
  • Inside the body of a function such as this.update, this is bound to a fully constructed controller instance.

Phobos enforces a convention that class names are uppercase.  Technically, a class name is really the name of a function that is used as a constructor.  That is why the second line of code in Code Example 17 must be written the way it is.

Because of the extreme malleability of the JavaScript object model, there are other ways to obtain the same result. Code examples 18 and 19 are two other ways to do what code example 17 does.

library.common.define(controller, "form", function() {
this.Form = function() {};

this.Form.prototype.update = function() {
// ... code to update the form goes here ...
}
}
});

Code Example 18: Another way to Define a Controller

library.common.define(controller, "form", function() {
this.Form = function() {};

with(this.Form) {
prototype.update = function() {
// ... code to update the form goes here ...
}
}
});

Code Example 19: A Third Way to Define a Controller

The difference between the three versions is mostly a matter of style and convenience.

A future version of Phobos might introduce some syntactic sugar to make namespace and class definitions simpler. In this case, we'll likely create a new scripting language for a JavaScript dialect extended with keywords such as package and class.

Given the form controller, what happens if a client tries to access a URL like /form/foo, for which there is no action method? In this case, the framework will then look for a method called onRequest, which acts as a catch-all action for this controller class. If that method is not present, the framework will return a 404 NOT FOUND error to the client.

Predefined Global Variables

Until now, we haven't explained how expressions like library.view.render work. Phobos defines a number of global variables that are the root of distinct namespaces. In addition to library and controller, there is also a module global variable. Each namespace has a corresponding path, as explained in Paths.

Modules are similar to libraries, but they are intended exclusively for application use. As a guideline, you should define your own library (such as application/library/mylibrary.js) only if you plan on adding that code to the framework virtual directory at some point.

Moving a library to the framework is completely transparent because the default path for libraries includes both /framework/library and /application/library directories.  As a result you can freely move a file between them.

Code that is not meant to become part of the framework should be defined in a module. For example, you might want to have a "helpers" module containing useful functions that you plan to invoke from multiple controllers in your application. You can accomplish this by defining a file called application/module/helpers.js of this form:

library.common.define(module, "helpers", function() {
this.myUsefulFunction = function() {
// ... useful code goes here ...
}
});

Code Example 20: Defining a Module

This function can be invoked from anywhere inside the application using the following expression:
module.helpers.myUsefulFunction( ... )

Code Example 21: An Expression That Invokes a User-Defined Module Function

Just as for controllers, multiple equivalent styles of module definitions are possible, as shown in the following code.

library.common.define(module, "helpers", function() {
function myUsefulFunction() {
// ... useful code goes here ...
}

// export the function
this.myUsefulFunction = myUsefulFunction;
});

Code Example 22: Another Way to Define a Module

In addition to the variable associated with namespaces and the request and response variables described earlier in  Scripts, Phobos defines some additional global variables as follows:

  • application contains a number of properties describing the application itself, including configuration information, such as paths or different namespaces
  • invocation is a JavaScript proxy to a map object that holds data with a lifetime of one request-response cycle.  This map is destroyed once an HTTP request has been handled
  • globals is a JavaScript proxy to a map object that holds data with a lifetime equal to that of the application itself.
To developers, all these objects, including those that are proxies appear as regular JavaScript objects with readable and writable (but currently non-enumerable) properties. Thus, the following expression should be read as defining a new request-scoped variable myDatum with a value of 5.
invocation.myDatum = 5;

Code Example 23: An Expression That Defines a New Request-Scoped Variable

Options, such as paths, used to configure an application are located under application.options. Another commonly used option is application.options.datasource, which indicates the datasource that  database access libraries use by default.

Life cycle events

Phobos applications have a simple life cycle. The two most important events are startup and shutdown. Except for background tasks, which will be described in a future document, all a Phobos application does in between startup and shutdown is to serve incoming requests.

You can have application code that is executed when startup or shutdown occurs. At startup, Phobos attempts to run a number of scripts and functions in a fixed order. Let's assume that the platform Phobos is running on is identified by the string "PLATFORM" and the environment by the string "ENVIRONMENT". Then the scripts and functions executed at startup are the following:

  • The /environment/ENVIRONMENT.js script
  • The /environment/startup-PLATFORM.js script
  • The /application/startup.js script
  • The onStartup function of the application module, resolved using the module path current at that time

The order is established according to the expected behavior of each script or function as described in the following list.

  • Environment-specific scripts should simply set some application options
  • Platform-specific scripts should acquire or define resources based on the platform in use
  • The startup.js script and onStartup function should do any actual startup work for the application like connecting to a database and checking for the presence of some tables
The difference between the startup.js script and onStartup function is minimal. The onStartup function is recommended, but the startup.js script is necessary if you need to change the module path.

At shutdown, the following steps are taken:

  • The onShutdown function of the application module is executed and resolved using the module path current at that point in time
  • The /application/shutdown.js script is executed
At this point, we have not identified a need for environment- or platform-specific shutdown scripts yet.

Sessions and the "flash"

Phobos manages sessions using cookies.

A session is represented by an object containing some name/value properties. You can access this object using invocation.session. Don't get misled by the fact that it lives under invocation: Although the object itself is recreated at each invocation, its contents will persist. (For the curious, session data is currently stored under application.sessionStore. This might change in the future, perhaps when we add file- or database- session persistence.)

You can write, read, and delete session-scoped properties using the usual JavaScript syntax, as shown in the following code:

invocation.session.userName = "Fred";
// ... some code here ...
model = { name: invocation.session.userName };
// ... more code here ...
delete invocation.session.userName;

Code Example 24: Writing, Reading, and Deleting Session-Scoped Properties

All conversions to and from strings happen automatically. In the case of objects, the default conversion is not very useful.  For example, an object {a:1, b:2} will be serialized as [Object object], causing it to lose all state. If you decide to use complex JavaScript objects as values, you might want to convert them to and from JSON notation first using the library.json.serialize and library.json.deserialize functions.

The predefined property invocation.session.id returns a string that uniquely identifies the session.  One use for this is as as a database key. A property whose name starts with __ (double underscore) are reserved for framework use.

An application can disable session management to avoid the performance overhead that goes with it by setting the following property in a startup script.

application.options.session.enabled = false;

Code Example 25: Disabling Session Management

First introduced by Ruby on Rails, the flash is a specially-scoped property bag. Its lifetime is exactly two request-response cycles: Data inserted in the flash during processing of one request will be visible during the next one and then automatically collected. The flash is useful when you want to pass information from one action to the following one, such as after a redirect.

In Phobos, you access the flash with invocation.flash. In reality, the flash is part of the session state.  Therefore, you need to be careful to avoid using the same property name for a session property and a flash property.

invocation.flash.message = "Login failed";
library.httpserver.sendFound("... redisplay the login page ...");

// then in the action we redirected to:
model = { message: invocation.flash.message };
library.view.render("... some view ...");

Code Example 26: Writing and Reading from the Flash

Libraries

Phobos comes with a growing number of JavaScript libraries, accessible under the library namespace.
The most commonly used libraries are the following:

  • httpserver contains functions to help with HTTP-level functionality, like redirects and errors;
  • json contains JSON serialization/deserialization functions;
  • lang contains language-related functions, e.g. to test the type of an object, extend an object with the properties of another or create a class-like object extending multiple base classes;
  • log contains functions to write informational, warning and error messages to a log;
  • template contains functions to use the FreeMarker template engine from JavaScript;
  • view contains functions typically invoked by a view to render parts of itself, either by including other resources or by programmatically creating new markup;

At this time, there is no JavaScript equivalent of the JavadocTM tool, but we hope to get one soon enough. Until then, the only way to see what functions are defined in a particular library is to browse its source code under framework/library/LIBRARYNAME.js.

Currently, Phobos does not add any properties to any predefined JavaScript objects. There is a well-defined place to add these extensions though.  Based on initial user feedback, we plan to add more convenience functions there, much like the Prototype JavaScript library does.

Finally, since JavaScript and most other scripting languages running on the Java platform have an excellent interface to Java APIs, scripts can easily call any Java libraries. A standard distribution of Phobos includes the JDOM and ROME utility libraries:

Additionally, you can directly access any class that is part of the Java platform.

When using a Java class library from JavaScript, you can simply refer to a qualified class name using the Packages global variable. For example, you can create a JDOM SAXBuilder object using the following code.

  var builder = new Packages.org.jdom.input.SAXBuilder();

Code Example 26: Creating a JDOM SAXBuilder Object

Wrapping Up

We hope this document helped you grasp the most important concepts in Phobos. If you haven't done it yet, work through the tutorial and try writing some simple web applications with Phobos.

Follow-up documents will detail more advanced functionality like AJAX (using technologies like jMaki), persistence, RSS/Atom, customizing the URLs used by your application and much more. Since Phobos in work in progress, we invite you to join the mailing lists on the Phobos project on java.net and contribute to its development.

 
 
Close
loading
Please Confirm
Close