The Kupo software package is not meant for productive use. Right now it is just a proof-of-concept implementation supporting my diploma thesis.
Kupo should run on all Unix systems. It was tested with MacOS X 10.5.7 and Debian 5.0. To use Kupo you need to have a running MongoDB Server >= 0.9.6 on your machine. (http://www.mongodb.org). You also should have git installed.
To install Kupo, just place the source directory somewhere and make sure the
correct versions of the Jack and Narwhal frameworks are in
place under the
packages directory. The best way to install Kupo is to
clone the repository using git:
git clone git://github.com/janv/kupo.git
git submodule update --init
After having installed Kupo, run it using the
The Kupo repository is laid out as a Narwhal package. The root directory
contains the application that is developed (a sample application at the
moment), all required frameworks are located as packages in the
directory, so they're automatically discovered by
narwhal. These packages
are Jack, Narwhal itself and the Kupo framework.
main.js is executed by
narwhal to run the package.
jackup executable of the Jack framework, passing it the current
directory as a Jack application. Jack applications are initialized using the
jackconfig.js in which a the app (a simple function adhering to the
Jack protocol) is exported in the
To load a file, the function
require is used.
Require provides the
exports variable to the module, runs the file and returns the contents of
exports. In order to export
functionality, modules can simply manipulate this variable. All other
identifiers in the file are not exported and unaccessible from outside.
The app defined in
jackconfig.js is simply a delegation to Kupo's
dispatcher which acts inside the current app by using the global variable
$KUPO_HOME to locate files and sources.
handle method adheres to the Jack protocol and acts as the
app for Jack to run in a Kupo application. Everything the dispatcher needs to
know to control the application is the
$KUPO_HOME global variable pointing
to the root of the application.
handle method is called for every incoming request. Inside, the
requested URL is analyzed. Based on the first part of the path, a model or
controller is looked up (via the
instantiated (via the
requestInstance method in the controller) and used to
handle the request.
First, the Dispatcher looks for a controller with the given name. If such a
controller does not exist, a model is looked up and handed to the generic
ResourceController. Looking up and loading controllers and models is done in
the Fetcher (and encapsulated in the dispatcher's
The fetcher contains methods to locate the files in the current Kupo
application, load them and make them available to the dispatcher. Its main two
check, both are encapsulated by four corresponding
methods which are exported. The
check method checks for the presence of a
fetch method loads and returns them.
An application's controllers are located in
app/controller/<controllername>.js. A controller is a module in the
SecurableModules-sense. To declare a controller, the module has to define it
CustomController.define('<controllername>'). The resulting object has
to be exported as a property of the exports object by the name
FooController, the controller name has to
To define actions on the created controller, the developer should create
functions in the controller's
actions property. Inside these functions,
this.session are available to access
details of the request. The action should return a Jack response array
[<status>, <headers-object>, <array of strings for the body>]).
The request object is created by Jack and defined in
packages/jack/lib/jack/request.js. The cookies object is created in Kupo's
controller.js. The Controller object defined in this file is a common
ancestor for both the ResourceController and the CustomControllers. It
handle method which is called by the dispatcher to handle the
handle, the request, cookies and session objects are set up
before the current controller instance's process method is called. To interact
with the cookie or the session object, simply add/remove/change its
properties. They're serialized into the HTTP response automatically.
Controller.js further exports the
JRPCRequest constructor for JSON-RPC
Requests. The two methods
the ones actually used to contruct JRPCRequests.
JRPCRequest objects contain
getMethodName()returns the name of the method that should be called
getParameters()returns the parameters for the call
call()is used on an object to call the requested method on the object, passing the provided parameters
JRPCRequest provides two additional helper methods:
buildResponsetakes a HTTP status code and a result object and constructs a Jack response array containing a JRPC reponse object.
buildErrorworks analogous and constructs a JRPC error response.
If the Dispatcher doesn't find a controller to handle a request, it looks for
a model with a matching name. If one is found, an instance of the
ResourceController is instantiated with it.
The ResourceController has a few standard ways to deal with requests. Its
process method analyzes the request to determine wether to treat the request
as a JSON-RPC request or as a simple GET request. The GET requests are
processed by the controller's
show actions. First, it is made
sure that the
find method of the model are callable, then they are
called and their result is sent to the client as a JRPCResponse. The actual
method call is surrounded by before/after filters defined on the model. They
are executed in the context of the controller by the model's
method, so they have the opportunity to manipulate the request (via
this.request) or the response (via
In all other cases, the ResourceController's
process method constructs a
JRPCRequest object from the request and processes it in the
The models Kupo operates with are defined in
Their declaration is similar to to the controllers. To define a model,
Model.define is called, passing the model's name and an object (called the
specialization object internally) that describes the model. The
specialization object contains four properties:
instanceis an object containing several properties in itself:
methodsis an object containing instance methods for instances of this model
callablesis an array of method names that can be called remotely on the instance
callablesis an array of method names that can be called remotely on the model
callbacksis an object containing several properties having certain names. Each of these properties contains an array of functions that are executed at points in the instances lifecycle that is provided by the property's name. All possible execution points are given in
- validations: an array of functions that are used to validate the instance
- associations: an object defining associations this model has to other models
Only one aspect of a model is defined outside of this specialization object and that's class methods. Those are simply created as properties on the defined model.
ClassPrototype is defined in
model.js and exported as
Model. As its
name indicates, the ClassPrototype is a prototype for all model classes. It is
derived into concrete models via the
Define clones the
prototype, processes the specialization object and opens a connection to this
model's database collection. This connection is stored in a closure and can be
accessed by calling the
collection() method on the model.
initSpecialization method processes the specialization object to create
instancePrototype. This instancePrototype is derived from the
CommonInstancePrototype and acts as a prototype for all instances of a
model. Most aspects in the specialization object however aren't actually
evaluated at this point but looked up later during the model's lifecycle.
create method in the ClassPrototype are
involved in aspects of object persistence and database connectivity. All of
them are described in model.js.
CommonInstanceProtoype is similar to the ClassPrototype. It contains
several initialization methods and persistence methods. All are described in
model.js. Here only some aspects will be listed that are missing from the
save method, responsible for saving changes to an instance to the
database shows nicely how saving is performed depending on the instance's
state property of the instance can take one of four values:
- New objects that haven't been saved to the database are 'new'.
- Objects that represent their (last known, concurrency issues aside) state in the database are 'clean'.
- Objects that have been changed since their last save or load operation are marked 'dirty'.
- Finally if an object is removed from the database, it's marked 'removed' and can't be saved anymore.
Depending on which state the instance was in, the callbacks are called on it.
Although an instance's underlying data is openly accessible in its
property, it should not be changed there. Instead the CommonInstancePrototype
set methods to access the data. Using
set ensures that
the instance's state does not become corrupted. Likewise, the
should never be overwritten manually. If an instance has to be marked
for saving, the
taint method should be used.
Validations are described in the DocComment for
CommonInstancePrototype.validate. Whenever an object isn't valid, it can't
be saved to the database anymore.
The last method of the CommonInstancePrototype is
newInstance. Once an
instancePrototype has been derived from the CommonInstancePrototype,
newInstance creates actual model instances. Notice that this method should
never be called manually, it is only used internally by
Associations provide automated contruction and use of relationships between
models. To declare an association, create a property with the name of the
association in the
associations object inside the
(examples are available in the test suite in
packages/kupo/tests/association-tests.js). The value of that property is an
association descriptor as returned by
Association.belongs_to/belongs_to_many/has_one/has_many. Each of these
associations is declared in a file in
/packages/kupo/lib/kupo/model/associations/ and consists of two parts.
The Association Proxy is an object that is accessed under the assication name
on the final model instance (eg.
task.user) and contains various methods to
manipulate the association.
The association generator returns an association descriptor object containing
registerCallbacks is called when the instancePrototype for a model is
derived from the CommonInstancePrototype. It installs a callback in the
instancePrototype that in turn executes a callback method in the association
proxy of the actual instance.
installProxy is called whenever an instance of a model is created. It
initializes an AssociationProxy for the association and installs it in the
MongoAdapter provides access to a MongoDB database by wrapping MongoDB's
MongoAdapter.Connection can be used to create a new database connection. A
default database connection is provided by
This method always returns the same connection, which by default connects to
kupo database. To change the default connection call
getCollection on a Connetion returns a new Mongo collection. These
are roughly corresponding to tables in SQL databases, so one collection is
used for every model in the app. Collections are never created explicitly,
they are simply accessed and the first write operation creates them.
For collection methods that return muliple items, a wrapper around the Mongo Cursor has been written. It provides basic means to iterate over the items. Mongo Cursors can not be rewound.
Whenever a single MongoDB object leaves the MongoAdapter, it is converted to a
fromDoc method at the end of
being processed by the Java driver's methods using the
Kupo has tests. To run them you need to:
- Start mongod using
./mongod --dbpath /path/to/kupo/db run
- Change to
The tests are covering the persistence and model aspects of Kupo, mainly everything in mongo_adapter.js and model.js.
Kupo is licensed under the MIT License
Copyright (c) 2009 Jan Varwig
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.