Resources
In Prudence, "resources" are encapsulated operational units similar to "objects" in object-oriented architectures. Resources, however, have a stricter structure than objects and define a messaging system more suitable for distributed environments such as the Internet and the World Wide Web. The idea of a "resource-oriented architecture," often called "REST," is covered in depth in "The Case for REST".
Files
All files under /resources/ are assumed to be source code files, and are mapped to URLs using the directory and file path, but without the filename extension. For example, the file "/resources/thread/list.js" would map to a URL like "http://mydomain.org/forum/thread/list/". You can also "capture" many URLs to be handled by a single file.
The filename's extension will be used to determine the language of the source code.
The above is a very brief overview; see routing for an in-depth explanation of how files become URLs.
Life
Files are only compiled once, when they are first requested by a client. The compiled code is kept in a cache, under your Prudence /cache/ directory. From then on, each request is handled by the compiled version, which is updated only if the original file changes. This cache is maintained even if you restart Prudence.
The file itself is executed only once, like a script, upon the first request. If it executes successfully, the remaining state, which contains all the entry points and global variables, is kept in memory. From then on the entry points are invoked in that state. The file will be executed again only if it changes. Note that this runtime state is not cached to disk, like the compiled code, so that restarting Prudence will mean that all your resources will have to be executed before their entry points can be invoked. For this reason, global variables are not kept or shared between such file updates.
This, then, is the difference between "defrosting" and "preheating" for resources: the former involves making sure that code is compiled and cached, and the latter actually executes the code, and additionally invokes the handleInit and handleGet entry points.
Note that many threads might be invoking entry points concurrently: make sure you understand the concerns of concurrent programming, as detailed in "Managing State".
What if you edit the file? Prudence will automatically pick up your changes and recompile. This happens on the fly, while the application is running. Are you worried that this check would happen for every client request? You can easily control the minimum time interval in which Prudence assumes the file is unchanged, and will not check the file for validity.
You can use document.addDependency to associate your own life to that of another file. If a dependency is updated, it will force your file to be reloaded.
Source Code
Global Source Code
As is explained in resource "life", the first time your source code is loaded, at the time of the first user request, it is executed from start to finish. That means that any code outside of the entry points is run immediately.
Global source code can thus be a good place to initialize various services you need for handling requests. Just be very careful about two things:
- Global variables are not kept or shared between file updates, which cause the file to be reloaded, recompiled, and thus re-executed. Use application.globals instead.
- The re-execution mentioned above can cause your services to be re-initialized. This can be very undesirable, because the service previously initialized may not have been released. It can even lead to errors. It's thus a good idea to first check if the service has already been initialized. Storing references in application.globals is a great way to keep track of this.
Consider using the handleInit entry point instead of global source code for service initialization. Because handleInit is called for every request, you can make sure to check there if the services you need are up or not.
You may also want to run maintenance tasks occasionally to check for and clean up unused services.
API
Beyond what's available to your programming language, resources have access to Prudence's elaborate, powerful set of services via its API. Note that the "conversation" service is provided to entry points as an argument, while the other services are global variables.
Code Libraries
Code files you put in your application's /libraries/ subdirectory can be accesses using your language's code inclusion mechanism. Here are examples in various languages. Let's assume that there is a file named "/libraries/util/data.*", which defines a function called getData().
-
Python:
from util.data import getData data = getData()
-
Ruby:
require 'util/data.rb' $data = get_data
-
Clojure:
(use 'util.data) (def data (get-data))
-
PHP:
require 'util/data.php'; $data = get_data();
-
JavaScript and Groovy do not have a built-in code inclusion mechanism, but they can use Prudence's document.execute API:
document.execute('/util/data/') data = getData()
Entry Points
What Are Entry Points?
Each file is a regular source code file, but a few special "entry points" are used by Prudence to connect user requests to your resource. All entry points except one (handleInit) are optional. We'll document each entry point below, but first, here's what an "entry point" means for each flavor of Prudence:
-
Python uses global functions:
def handle_get(conversation): return 'Hello, world!'
-
Ruby uses global methods:
def handle_get conversation return 'Hello, world!' end
-
Clojure uses functions in the current namespace:
(defn handle-get [conversation] 'Hello, World!')
-
JavaScript uses global functions:
function handleGet(conversation) { return 'Hello, world!' } -
PHP uses global functions:
function handle_get($conversation) { return 'Hello, World!'; } -
Groovy uses closures tied to global variables (there are no global methods in Groovy):
handleGet = { conversation -> return 'Hello, World!' }
Note that the name of the entry point follows typical conventions for each language. In this documentation, we'll use "camel case" (handleGet), but be sure to replace the names following the examples above.
All entry points accept the "conversation" service as the sole argument. The other services ("application", "executable", etc.) are available as global variables.
handleInit
This is the only required entry point. It is called once for every user request, and always before any of the other entry points.
The main work is to initialize supported media types via conversation.addMediaType, in order of preference. For example:
function handleInit(conversation) {
conversation.addMediaTypeByName('application/json')
conversation.addMediaTypeByName('text/plain')
}
Prudence handles content negotiation automatically, choosing the best media type according to list of acceptable and preferred formats sent by the client and this list.
You might wonder why we add these supported media types dynamically for each request via a call to handleInit, since they are usually always the same for a resource. The reason is that sometimes they may not be the same. In handleInit, you can check for various conditions of the conversation, or even external to the conversation, to decide which media types to support. For example, you might not want to support XHTML for old browsers, but you'd want it at the top of the list for new browsers. Or, you might not be able to support PDF in case an external service is down. In which case, you won't want it on the list at all, in which case content negotiation would choose a different format that the client supports, such as DVI.
So, this gives you a lot flexibility, at no real expense: adding media types per request is extremely lightweight.
handleGet
Handles HTTP "GET" requests.
In a conventional resource-oriented architecture, clients will not be expecting the resource to be altered in any way by a GET operation.
What you'll usually do here is construct a representation of the resource, possibly according to specific parameters of the request, and return this representation to the client. See the "conversation" API documentation for a complete reference. Note especially that if you've captured URI segments, they'll be available in conversation.locals.
The following return types are supported:
- Numbers: Returns the number as an HTTP status code to the client, with no other content. Usually used for errors. For example, 404 means "not found." Note that error capturing can let you take over and return an appropriate error page to the client.
- Arrays of bytes: Used for returning binary representations. Note that some languages (JavaScript, for example) have their own implementations of arrays, which are not exactly compatible with JVM arrays. In such cases, you have to make sure to return JVM arrays. Internally, Prudence represents these values with a ByteArrayRepresentation.
- Representation instances: You can construct and return a Restlet representation directly.
- Other return values: If the conversation.mediaType is "application/java" the value will be wrapped in an ObjectRepresentation instance. Otherwise, it will be converted into a string if it isn't a string already, and returned to the client as a textual representation.
Beyond the return value, you can affect the response sent to the client by the response attributes in the "conversation" service. In particular, the conversation.modificationDate and conversation.tag can be used to affect conditional HTTP requests. For these, you may also consider implementing the handleGetInfo entry point for more scalable handling of conditional requests.
handlePost
Handles HTTP "POST" requests.
In a conventional resource-oriented architecture, POST is the "update" operation (well, not exactly: see note below). Clients will expect the resource to already exist for the POST operation to succeed. That is, a call to GET before the POST may succeed. Clients expect you to return a modified representation, in the selected media type, if the POST succeeded. Subsequent GET operations would then return the same modified representation. A failed POST should not alter the resource.
Note that the entity sent by the client does not have to be identical in format or content to what you return. In fact, it's likely that the client will send smaller delta updates in a POST, rather than a complete representation.
What you'll usually do here is fetch the data, and alter it according to data sent by the client. See the "conversation" API documentation for a complete reference. Note especially that if you've captured URI segments, they'll be available in conversation.locals.
See handleGet for supported return types. In fact, you may want handlePost to share the same code path as handleGet for creating the representation.
POST is the only HTTP operation that is not "idempotent," which means that multiple identical POST operations on a resource may yield different results from a single POST operation. This is why web browsers warn you if you try to refresh a web page that is the result of a POST operation. As such, POST is the correct operation to use for manipulations of a resource that cannot be repeated. See this blog post by John Calcote for an in-depth explanation.
handlePut
Handles HTTP "PUT" requests.
In a conventional resource-oriented architecture, PUT is the "create" operation (well, not exactly: see note below). Clients will expect whatever current data exists in the resource to be discarded, and for you to return a representation of the new resource in the selected media type. A failed PUT should not alter the resource.
Note that the entity sent by the client does not have to be identical in format or content to what you return. In fact, it's likely that you will return more information to the client than what was sent.
What you'll usually do here is parse and store the data sent by the client. See the "conversation" API documentation for a complete reference. Note especially that if you've captured URI segments, they'll be available in conversation.locals.
See handleGet for supported return types. In fact, you may want handlePut to share the same code path as handleGet for creating the representation.
PUT, like most HTTP operations, is "idempotent," which means that multiple identical PUT operations on a resource are expected to yield the same result as a single PUT operation. If you are implementing a "create" operation that cannot be repeated, then you should use POST instead. See note in POST.
handleDelete
Handles HTTP "DELETE" requests.
In a conventional resource-oriented architecture, clients expect subsequent GET operations to fail with a "not found" (404) code. A DELETE should fail with 404 if the resource is not already there; it should not silently succeed. A failed DELETE should not alter the resource.
What you'll usually do here is make sure the identified resource exists, and if it does, remove or mark it somehow as deleted. See the "conversation" API documentation for a complete reference. Note especially that if you've captured URI segments, they'll be available in conversation.locals.
The following return types are supported:
- Numbers: Returns the number as an HTTP status code to the client, with no other content. Usually used for errors. For example, 404 means "not found." Note that error capturing can let you take over and return an appropriate error page to the client.
- Null: Signifies success.
Note: It's good practice to always explicitly return null in handleDelete. Some languages return null if no explicit return statement is used. Others, however, return the value of the last executed operation, which could be a number, which would in turn become an HTTP status code for the client. This can lead to some very bizarre bugs, as clients receive apparently random status codes!
handleGetInfo
Handles HTTP "GET" requests before handleGet.
This entry point, if it exists, is called before handleGet in order to provide Prudence with information required for conditional HTTP requests. Only if conditions are not met—for example if our resource is newer than the version the client has cached, or the tag has changed—does Prudence continue to handleGet. Using handleGetInfo can thus improve on the gains of conditional requests: not only are you saving bandwidth, but you are also avoiding a potentially costly handleGet call.
The following return types are supported:
- Numbers: Considered as Unix timestamps, and converted into the modification date. See conversation.modificationDate.
- JVM Date instances: The modification date. Refer to the Java API documentation for details.
- Strings: Considered as HTTP tags. See conversation.tag.
- Tag instances: You can construct and return your own Restlet tag.
- RepresentationInfo instances: You can construct and return your own Restlet representation info.
Note that even if though you can only set either the modification date or the tag by the return value, you can set the other one using conversation.modificationDate and conversation.tag.
If you implement handleGetInfo, you should be returning the same conditional information in your handleGet implementation, so that the client would know how to tag the data. The return value from handleGetInfo does not, in fact, ever get to the client: it is only used internally by Prudence to process conditional requests.
You might be tempted to go ahead and provide a handleGetInfo entry point for every resource you create. This is not necessarily a good practice, for three reasons:
- It could be that you don't need this optimization. Make sure, first, that you've actually identified a problem with performance or scalability, and that you've traced it to handleGet on this resource.
- It could be that you won't gain anything from this optimization. Caches and other optimizations along the route between your data and your client might already be doing a great job at keeping handleGet as efficient as it could be. If not, improving them could offer far greater benefits overall than a handleGetInfo.
- It could be that you'll even hurt your scalability! The reason is that an efficient handleGetInfo implementation would need some mechanism in place to track of data modification, and this mechanism can introduce overhead into your system that causes it to scale worse than without your handleGetInfo.
See "Scaling Tips" for a thorough discussion of the problem of scalability.
Conditional Requests
HTTP clients, such as web browsers, store downloaded content in their local cache, marking it with a modification date and tag according to HTTP headers in the response. Subsequent requests to the same URL will be "conditional," meaning that the client will tell the server what the latest modification date it has. If the server does not have new data, then it returns an empty response with the 304 "not modified" HTTP status, letting the client know that it is safe to use its cached version. This saves both bandwidth and processing resources on the server.
To support conditional requests, you have to explicitly set at least one of conversation.modificationDate and conversation.tag. If you implement handleGetInfo, you should be returning one of these values instead.
Note that these attributes are ignored in case you are constructing and returning your own Representation instance.
Though clients rely on a local cache for conditional requests, you can provide them with additional caching directives. In Prudence, you can control the expiration of the client's cached entry with conversation.maxAge or conversation.expirationDate.
Explicitly setting cache directives has an important side effect: most clients will not send conditional HTTP requests for the cached data until the cache entry expires. This allows you to save bandwidth and improves client performance, but at the expense of not being able to update the client upon changes. Use with care.
Resources As API
A "resource-oriented architecture" is purposely more minimal than object-oriented, transactional architectures. As discussed in "The Case for REST", there are many far-reaching advantages to this minimalism. While it's possible for you to maintain multiple interfaces to your service, it might make more sense to standardize around REST principles throughout your application. A single, uniform API is more maintainable, and less prone to bugs.
Prudence lets you access your resources internally, without having to go through HTTP, using document.internal. You can furthermore avoid special representations and pass JVM objects through directly by setting conversation.mediaType to "application/java".
For example, here's "/resources/testme.js" that nicely supports both external and internal clients:
function handleInit(conversation) {
conversation.addMediaTypeByName('application/json')
conversation.addMediaTypeByName('application/java')
}
function handleGet(conversation) {
var data = getData()
if(conversation.mediaTypeName == 'application/java') {
return data
}
else {
return JSON.to(data)
}
}
function getData() {
return {name: 'test', description: 'This is some test data'}
}
We can then access the resource directly, without any JSON encoding/decoding:
var data = document.internal('/testme/', 'application/java').get().object
print(data.description)
// Just for comparison, here's how we would to it using JSON encoding/decoding
var data = eval('(' + document.internal('/testme/', 'application/json').get().text + ')')
print(data.description)
Note: The "application/java" media type can be used externally, too. In such a case, the object would be serialized/unserialized over HTTP. This does mean that your data class has to properly support JVM serialization, via the Serializable or Externalizable interfaces.


