Navigation

RSS 2.0 New Entries Syndication Feed Atom 0.3 New Entries Syndication Feed

Show blog menu v

 

General

Use it

Documentation

Support

Sibling projects

RIFE powered

Valid XHTML 1.0 Transitional

Valid CSS!

Blogs : Latest entries

RIFE v0.7.0 has been released

This is primarily a bug-fix release. We believe most important bugs have been identified and been resolved, which justifies a new major release.

Below are the highlights of this release.

  • many bug-fixes and improvements [ more ]
  • additional constraints [ more ]
  • easy way to retrieve a RoleUsersManager [ more ]
  • Repository support for application-wide properties [ more ]
  • merged dedicated XML selectors [ more ]
  • added XmlSelectorProperty [ more ]

Many bugfixes and improvements

The following sections received a number of bugfixes and were improved:

  • continuations,
  • web application engine,
  • logout element,
  • user identification,
  • generic query manager,
  • in-template OGNL statements,
  • element implementations in Groovy,
  • repository.

[ top ]

Additional constraints

Two new constraints were added to ConstrainedProperty: persistent and saved.

Persistent allows you to indicate that a bean property shouldn't be stored in a back-end data storage. It will thus not be automatically persisted through, for example, a database. This makes it possible to still use a bean as a central data entity and have informational properties that should only be used to retrieve information from the user.

Saved allows you to indicate that a property shouldn't automatically save its value to a data store, but it will still participate in the definition of the structure of the data entity. For example, this makes it possible to continue to use one place to define the table structure and use database triggers to fill in the values instead.

[ top ]

Easy way to retrieve a RoleUsersManager

The RoleUsersManager class provides the functionalities to retrieve a RoleUsersManagers from a particular Authenticated element in a site.

Since you can have many authentication schemes and backends being active in a single web application. it's quite verbose to retrieve a RoleUsersManager when you want to perform some operations on its stored credentials. This class provides the functionalities to quickly perform this retrieval through the static getRoleUsersManager(Site site, String authElementId) method.

You can thus very easily update a user's attributes, for example:

public class BanUser extends Element
{
    public void processElement()
    {
        String login = getInput("login");
        
        RoleUsersManager credentials =
            RoleUsersManagerRetriever.getRoleUsersManager(getSite(), ".AUTH_USER");
        
        try
        {
            RoleUserAttributes attrs = credentials.getAttributes(login);
            attrs.getRoles().add("banneduser");
            credentials.updateUser(login, attrs);
        }
        catch (CredentialsManagerException e)
        {
            // handle the exception
        }
    }
}

[ top ]

Repository support for application-wide properties

Since Java allows the configuration of an application through the use of properties, many other sub-system have adopted a similar approach (for example servlet init parameters). Most of the time an application runs through several barriers of configuration that often function independently. Properties support in the Repository makes it possible for each sub-system to add their properties to the same pool. This makes it much more convenient to retrieve a property value afterwards since they're all available from a single source. In a RIFE web application these are for example, the system properties and the servlet init parameters.

[ top ]

Merged dedicated XML selectors

The dedicated XML selectors for Config and Datasources have been merged in the a single selector package since they provide the same functionalities. This also made it possible to easily add dynamic XML file selection to the MemoryScheduler.

So what was before ConfigSelectorHostname, ConfigSelectorOs, ConfigSelectorUser, DatasourceSelectorHostname, DatasourceSelectorOs, DatasourceSelectorUser is now available as XmlSelectorHostname, XmlSelectorOs and XmlSelectorUser.

[ top ]

Added XmlSelectorProperty

A new XML selector has been added to automatically select an XML file according to the rife.application property that is looked up from the Repository properties. You can use this selector for Config, Datasources and MemoryScheduler. It's typically used to easily store all the configuration for application variants in the same source repository and set this property during the installation to indicate which variant to use.

For example in tomcat's server.xml file by setting a context parameter:

<Host name="test.myhost.com" appBase="/home/tomcat/test_myhost_com">
    <Context path="" docBase="ROOT">
        <Parameter name="rife.application"
                   value="test"
                   override="false"/>
    </Context>
</Host>
<Host name="prod.myhost.com" appBase="/home/tomcat/prod_myhost_com">
    <Context path="" docBase="ROOT">
        <Parameter name="rife.application"
                   value="production"
                   override="false"/>
    </Context>
</Host>

[ top ]

posted by Geert Bevin in RIFE on Feb 22, 2004 6:16 PM : 1 comment [permalink]
 
Web continuations presentation

I'll be doing a presentation on web continuations with RIFE at Fosdem this weekend. It was fun doing this with Keynote under MacOSX. Really great results and very easy to use.

For those that want a sneak peak, head over to the download area where you can find the keynote files (1.7MB), a high-quality quicktime movie (22.7MB), a medium-quality quicktime movie (1.4MB) and a PDF file (0.7MB).

posted by Geert Bevin in RIFE on Feb 18, 2004 1:45 PM : 0 comments [permalink]
 
RIFE v0.6.59 has been released

Unless major problems are found, this is the last of the unstable releases.

The focus of the next release will be the addition of more documentation, more examples, and the creation of a Wafer weblog implementation.

Below are the highlights of this release.

  • much better support for web continuations [ more ]
  • added customization support to generic query manager [ more ]
  • fully integrated data type meta-data facility through constraints [ more ]
  • totally rewritten validation system [ more ]
  • automated form building [ more ]
  • addition of common database query execution patterns [ more ]
  • user identification facility built on top of the authentication [ more ]
  • OGNL support in template engine [ more ]
  • support for Groovy as element implementation language [ more ]
  • configurable state storage with support for server-side storage in sessions [ more ]
  • refactored the repository for easy mocking and testing [ more ]
  • added jump-start distribution [ more ]
  • miscellaneous web engine enhancements [ more ]
  • backwards incompatible changes [ more ]

Much better support for web continuations

Different continuation handling models

Since RIFE tries its best to make the end-user's experience as comfortable as possible, continuations have changed to fully support back-button presses in a browser. So someone can take different execution paths in an application, go back, take another path and even later resume from earlier paused location. To make this work, RIFE needs to clone the context of the executing code each time a continuation is resumed, thus creating a totally independent context that doesn't interfere with any previous ones. The downside is that to be able to do this, all variables in the scope need to be cloned too. For most commonly used JDK types, this has been automated and a lot of RIFE's classes are implementing Cloneable now too. However, you need to be aware that this is a requirement for any of the classes that you write yourself or that you use from an external library. To make your life easier, you can use the genericClone(Object) and deepClone(Object) methods in the com.uwyn.rife.tools.ObjectUtils class to quickly implement clone() methods, but it might be possible that you're unable to provide a cloning functionality to certain classes or that it requires too much work.

There are several options to solve this. First, you have to be aware of the fact that the scope for the continuations context consists out of the local variables of the processElement() method and the member variables of the element that is being executed. So, the easiest option to use uncloneable types, is to move them outside of this scope, a separate method being a perfect choice. For example:

public class Continuation extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("mytemplate");
        String result = getUncloneableResult();
        template.setValue("result", result);
        print(template);
        pause();
    }
    
    private String getUncloneableResult()
    {
        UncloneableType type = new UncloneableType();
        return type.getResult();
    }
}

Your other option is to indicate to RIFE that it shouldn't clone the continuation context at all, but always continue to use the same one. This largely removes any restrictions on the usable types, but it also prevents an end-user from back-tracking and taking another execution path. Only the last-used continuation will remain active and RIFE will enforce this by changing the continuation IDs at each request and invalidating old IDs. While this might seem like a nice quick solution if you run into trouble, you should think very carefully before using it since it results in making the back-button of the browser (or Safari's snapback function) throw the user back to the beginning of the element. A non-negligible benefit of this behaviour however, is that continuations will use much less resources. That being said, you have two options to indicate to RIFE that you want this behaviour for an element. Either you override the public boolean cloneContinuations() method and return false, or use setCloneContinuations(false) before using the first continuation. I prefer the first one which results in this, for example:

public class Continuation extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("mytemplate");
        UncloneableType type = new UncloneableType();
        template.setValue("result", type.getResult());
        print(template);
        pause();
    }
    
    public boolean cloneContinuations()
    {
        return false;
    }
}

Support for inter-element continuations (call/answer)

Continuations are now not limited anymore to one element. It's now also possible to activate an exit and pause the execution of the calling element. When the target element stops processing, the execution of the calling element will automatically resumed. You can do this very simply by using call("exitname") method instead of exit("exitname") method. All the behaviour of a regular exit is still available and the target element will be processed normally.

For example, consider the following element implementations that are linked together through a flowlink that starts at the "callexit" exit and points to the second element:

public class Call extends Element
{
    public void processElement()
    {
        print("before");
        call("callexit");
        print("after");
    }
}

public class CallTarget extends Element
{
    public void processElement()
    {
        print("-target-");
    }
}

The resulting output will be:

before-target-after

While this may seem very trivial and look like a regular method call, that is exactly its power. Consider that it is completely decoupled and adaptable through the site structure, that it will be remembered even when the target element uses a pause() call to suspend the execution while gathering user info and that the target element element could be written in one of the scripting languages like Groovy.

One typical use for inter-element continuations is the display of intermediate pages like dialogs or help pages without losing track of the main execution flow. To make this even more comfortable, it's possible to forcibly return control to the calling element and to pass a value along by using the answer(Object) method. This makes it possible to gather user information and to provide the result to the caller.

For example, the following element implementations are again linked together through a flowlink that connects the "dialog" exit to the Dialog element. The Delete element will first display a page to the user and wait for input. If the user presses the button that corresponds to the delete submission, it will activate the "dialog" exit and hold the execution until the call is answered. This makes the execution flow go into the Dialog element which displays a confirmation dialog. Depending on which button the user presses, the "yes" or "no" submission is sent to the element and the call is answered with either true or false. The execution in the Delete element is now resumed and according to the user's answer, a different action is taken.

public class Delete extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("pub.home");
        print(template);
        pause();
        
        if (hasSubmission("delete"))
        {
            Boolean answer = (Boolean)call("dialog");
            if (answer.booleanValue())
            {
                print("deleted");
            }
            else
            {
                print("not deleted");
            }
        }
    }
}

public class Dialog extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("pub.dialog");
        print(template);
        pause();
        
        if (hasSubmission("yes"))
        {
            answer(new Boolean(true));
        }
        else if (hasSubmission("no"))
        {
            answer(new Boolean(false));
        }
    }
}

[ top ]

Added customization support to generic query manager

In this release the GenericQueryManager recieved a major boost which greatly enhanced its flexibility. Deletions and restorations can now be customized with restricted queries. These queries have exactly the same interface as the regular Select, and Delete classes, accept that harmful methods have been removed. This leaves only methods that will not harm the integrity of the query and will work on full beans. Using the new customizations are quite easy, for example using a RestoreQuery:

public class RestoreQueryTest extends Element
{
    public void processElement()
    {
        // ...

        GenericQueryManager articles = GenericQueryManagerFactory.getInstance(datasource, Article.class);
    
        RestoreQuery query = articles.getRestoreQuery();
        query
            .where("title", "=", "Article Title");
    
        List articleList = articles.restore(query);

        // ...
    }
}

Another newly extended interface is DeleteQuery. An example of its usage:

public class DeleteQueryTest extends Element
{
    public void processElement()
    {
        // ...

        GenericQueryManager articles = GenericQueryManagerFactory.getInstance(datasource, Article.class);
    
        DeleteQuery query = articles.getDeleteQuery();
        query
            .where("title", "=", "Article Title");
        
        articles.delete(query);

        // ...
    }
}

When using subselects and other more advanced features you may need to get the name of the table that the GenericQueryManager uses to store its data. The new method getTable(), which returns a String, provides this functionality.

[ top ]

Fully integrated data type meta-data facility through constraints

Many parts of an application benefit a lot from an easy way to obtain the type, the accepted limits, and the behavioural patterns of a data entity. In RIFE this has now been centralized into a bean. You have the possibility to add a collection of constraints to its properties and retrieve this information later on. This neatly centralizes the addition of this meta-data.

What are constraints

The main purpose of a constraint is to alter the default behaviour of a data type and to clearly set the accepted limits. The meta-data that's provided through constraints can be used elsewhere to gather more information about how to correctly integrate the indicated data limits.

For example, a constraint specifies that a certain text's length is limited to 30 characters, parts of the system can query this information and act accordingly:

  • a HTML form builder can create a field that doesn't allow the entry of longer text,
  • a SQL query builder can limit the size of the column in which the text will stored when the table creation SQL is generated,
  • a validation system can check if the text isn't longer than 30 characters and provide appropriate information when the length is exceeded.

Several types of constraints will be available, but currently only constrained bean properties are supported through the ConstrainedProperty class..

Constrained properties

A ConstrainedProperty object makes it possible to easily define all constraints for a named property of a bean.

The property name refers to the actual name of the bean property. However, this sometimes doesn't correspond to its conceptual usage. It can be handy to receive constraint violation reports with another conceptual name: the subject name. Notice that this corresponds to the subject that is used in a ValidationError. If no subject name is specified, the property name will be used instead.

It's possible to add constraints to a ConstrainedProperty instance through regular setters, but chainable setters are also available to make it possible to easily define a series of constraints, for example:

ConstrainedProperty constrained = new ConstrainedProperty("password");
constrained
    .maxLength(8)
    .notNull(true);

Constrained properties are typically added to a Constrained bean in its constructor. These are the static constraints that will be set for each and every instance of the bean, for example the following Credentials class extends Validation which contains a Concrete implementation of the Constrained interface:

public class Credentials extends Validation
{
    private String mLogin = null;
    private String mPassword = null;
    private String mLanguage = null;
    
    public Credentials()
    {
        addConstraint(new ConstrainedProperty("login").maxLength(6).notNull(true));
        addConstraint(new ConstrainedProperty("password").maxLength(8).notNull(true));
        addConstraint(new ConstrainedProperty("language").notNull(true));
    }

    public void setLogin(String login)       { mLogin = login; }
    public String getLogin()                 { return mLogin; }
    public void setPassword(String password) { mPassword = password; }
    public String getPassword()              { return mPassword; }
    public void setLanguage(String language) { mLanguage = language; }
    public String getLanguage()              { return mLanguage; }
}

It's also possible however to add constraints to a single bean instance whenever they cannot be determined beforehand. These are then dynamic constraints which can be populated at runtime, for example:

Credentials credentials = new Credentials();
credentials.addConstraint(new ConstrainedProperty("language").inList(new String[] {"nl", "fr", "en"}));

To retrieve constrained properties after they have been added, you can use the getConstrainedProperties() and getConstrainedProperty(String propertyName) methods.

[ top ]

Totally rewritten validation system

The previous validation system was much too rudimentary, requiring too much code to be written and too much to be done manually by the developer. It has therefore been completely rewritten, becoming very flexible, powerful and unintrusive.

Incremental validation

Validation is bound to subjects that have distinct names. Each subject corresponds to a different variable, for example a property of a bean. When a subject is found to be invalid, a corresponding instance of ValidationError has to be registered.

ValidationErrors indicate in detail why a Validated object doesn't contain valid data. They should be stored internally and can be manipulated by other classes that are able to work with Validated objects. This makes it possible to collect errors incrementally in one central place and to allow each component in a system to perform its own part of the validation.

A Validated object has a validate() method which should be used to perform mandatory validation on subjects and data that the object itself knows about. This validation has to perform all checks that guarantee a coherent, consistent internal state for the data. Note that this method should not reset the validation, but rather add new validation errors to an already existing collection

Since it is possible that subjects generate multiple ValidationErrors, it's possible to limit their number and only store the first error that occurs for a particular subject.

Automatic rule creation from constraints

Validation is now able to automatically create and add validation rules according to constraints. The example Credentials class above thus already contains all information to be able to validate the data that is stored in its properties. If the constraints system is not sufficient for your need, you're of course still able to create ValidationRule objects yourself and add them to the bean explicitly.

Validation groups

Since it's quite common to validate different collections of rules one after the other (in a wizard or a multi-paged form for instance) you can now define validation groups. They are identified by a unique name and it is possible to validate only the rules that are included in a particular group. To define a validation group you simply use the addGroup(String name) method which create a new ValidationGroup instance and registers it in the Validation object. A validation group contains the same methods as the Validated interface to add rules and constraints: addRule(ValidationRule rule) and addConstraint(ConstrainedProperty constrainedProperty). These however return the ValidationGroup instance they are called upon, which makes them chainable. For example:

public class Credentials extends Validation
{
    public Credentials()
    {
        addGroup("step1")
            .addConstraint(new ConstrainedProperty("login").maxLength(6).notNull(true))
            .addConstraint(new ConstrainedProperty("password").maxLength(8).notNull(true));

        addGroup("step2")
            .addConstraint(new ConstrainedProperty("language").notNull(true));
   }

   // the properties code is trivial, see above

}

To actually validate against the groups you have two options.

You start from scratch and validate only the rules of the group like this:

Credentials credentials = new Credentials();
credentials.validateGroup("step1");
// do something if errors occurred
credentials.resetValidation();
credentials.validateGroup("step2");
// do something if errors occurred

You can also validate normally and focus down to only the subjects that are validated by the groups, like this:

Credentials credentials = new Credentials();
credentials.validate();
credentials.focusGroup("step1");
// do something if errors occurred
credentials.resetValidation();
credentials.validate();
credentials.focusGroup("step3");
// do something if errors occurred

Error message generation, decoration, specification, positioning

The previous ValidationFormatter class is now deprecated and replaced by ValidationBuilder implementations. Currently the only concrete implementation is ValidationBuilderXhtml, but there are plans to support other template types.

Fallback message positioning and decoration

The template value in which validation error messages are collected by default is now the following:

<!--V 'ERRORS:*'--><!--/V-->

You can set a custom message in the area with the setFallbackErrorArea(Template template, String message) and thus use it also for general error reporting. To fill it with error messages, you have to use the generateValidationErrors(Template template, Collection validationErrors, String prefix) method.

If you need decoration for this error area that you only want to appear when content is assigned to it, you can specify it like this:
<!--B 'ERRORS:*'--><p><!--V 'ERRORS'/--></p><!--/B-->

The ERRORS value will then contain the content.

If you want to decorate each error message individually, you should specify it like this:

<!--B 'ERRORMESSAGE:*'--><b><!--V 'ERRORMESSAGE'/--></b><br /><!--/B-->

The ERRORMESSAGE value will then contain the error message.

Considering all the above template directives, the following code:

ValidationBuilderXhtml builder = new ValidationBuilderXhtml();
builder.setFallbackErrorArea(template, "my message");

will generate the following result:

<p><b>my message</b><br /></p>

If you generate a collection of validation errors:

ValidationBuilderXhtml builder = new ValidationBuilderXhtml();
builder.generateValidationErrors(template, errors, null);

the following result could be generated with the same template directives:

<p><b>Invalid login.</b><br /><b>MANDATORY:password</b><br /><b>WRONGLENGTH:language</b><br /></p>

Selective error area positioning and decoration

Often you want to position error messages near the specific form fields they describe, and a global error area is not appropriate. You can do this by adding the subject name to the ERRORS value ID, like this:

<!--V 'ERRORS:language'--><!--/V-->

Dependent on your design, you might even want to display the error messages of several subjects in a certain location. This is also possible, like this:

<!--V 'ERRORS:login,password'--><!--/V-->

Note that it's possible to use the same subject name in several value IDs. To select in which value a message will appear, RIFE uses a fallback mechanism that selects values according to the number of matching erroneous subjects. For each subject it goes over all the value IDs that contain it, the order of traversal is from the value with the most subject names to the one with the least. The first value ID in which all specified subject names are invalid will be used.

For example, consider the following error area values:

<!--V 'ERRORS:login,password,language'--><!--/V-->

<!--V 'ERRORS:login,password'--><!--/V-->
<!--V 'ERRORS:login'--><!--/V-->
<!--V 'ERRORS:password'--><!--/V-->
<!--V 'ERRORS:*'--><!--/V-->

Now, examine the following situations where each time different subjects are invalid:

  • login, password, language
    all error messages will appear in 'ERRORS:login,password,language',
  • login, password
    all error messages will appear in 'ERRORS:login,password',
  • password, language
    all error messages will appear in 'ERRORS:*',
  • login, language
    login error messages will appear in 'ERRORS:login' and
    language error messages will appear in 'ERRORS:*',
  • login
    all error messages will appear in 'ERRORS:login',
  • password
    all error messages will appear in 'ERRORS:password',
  • language
    all error messages will appear in 'ERRORS:*'.

On its own this mechanism isn't very handy, but it becomes interesting when you combine it with the selective decoration for these error areas. It is based on the same principle, but uses an inverted mechanism to determine which block to use. This means that it will first start with the blocks that specify the least amount of subject names and end with those that specify the most. As soon as all subject names of a used error area are present in the decoration block, the block will be used.

For example, consider the following decoration blocks together with the error areas from above:

<!--B 'ERRORS:login,password'--><!--V 'ERRORS'/--><!--/V-->
<!--B 'ERRORS:login'--><!--V 'ERRORS'/--><!--/V-->
<!--B 'ERRORS:password'--><!--V 'ERRORS'/--><!--/V-->
<!--B 'ERRORS:*'--><!--V 'ERRORS'/--><!--/V-->

Now, examine the following situations where each time another error area value is used:

  • 'ERRORS:login,password,language'
    the 'ERRORS:*' decoration block will be used,
  • 'ERRORS:login,password'
    the 'ERRORS:login,password' decoration block will be used,
  • 'ERRORS:login'
    the 'ERRORS:login' decoration block will be used,
  • 'ERRORS:password'
    the 'ERRORS:password' decoration block will be used,
  • 'ERRORS:*'
    the 'ERRORS:*' decoration block will be used.

You now probably ask yourself why you would ever use something like this. The typical usage is when you have a design that changes according to the error messages that are displayed. For example, you have a firstname and a lastname field in different columns on the same table row. You want the messages for each field to be displayed above it and when they both are invalid, you just the whole row above. Below is an example template excerpt that demonstrates this:

<!--B 'ERRORS:firstname,lastname'-->
<tr class="error_messages">
    <td colspan="2"><!--V 'ERRORS'/--></td>

</tr>
<!--/B-->
<!--B 'ERRORS:firstname'-->
<tr>
    <td class="error_messages"><!--V 'ERRORS'/--></td>
    <td>&nbsp;</td>

</tr>
<!--/B-->
<!--B 'ERRORS:lastname'-->
<tr>
    <td>&nbsp;</td>
    <td class="error_messages"><!--V 'ERRORS'/--></td>

</tr>
<!--/B-->

<table cellspacing="0" cellpadding="5" border="0">
<tr>
    <td><em>Firstname</em></td>
    <td><em>Lastname</em></td>

</tr>
<!--V 'ERRORS:firstname,lastname'/-->
<!--V 'ERRORS:firstname'/-->
<!--V 'ERRORS:lastname'/-->
<tr>
    <td><input type="text" name="firstname" size="30" /></td>
    <td><input type="text" name="lastname" size="30" /></td>

</tr>

Selective error message decoration

Instead of using the same decoration for all error messages, you can specify different ones according to the erroneous subjects. The syntax is similar to what is used for the selective error area decoration, but you use the ERRORMESSAGE block ID as prefix instead. You can again specify several subject names for each block, but the selection mechanism is much simpler. For each subject of an error message, RIFE will just take the block that specifies it first.

For example, consider the following error message decorations:

<!--B 'ERRORMESSAGE:*'--><!--V 'ERRORMESSAGE'/--><br /><!--/B-->
<!--B 'ERRORMESSAGE:login,password'--><p><!--V 'ERRORMESSAGE'/--></p><!--/B-->

<!--B 'ERRORMESSAGE:login,language'--><div><!--V 'ERRORMESSAGE'/--></div><!--/B-->
Now, examine the following situations where each time another error message subject is displayed:
  • login
    the 'ERRORMESSAGE:login,password' message decoration block will be used,
  • password
    the 'ERRORMESSAGE:login,password' message decoration block will be used;
  • language
    the 'ERRORMESSAGE:login,language' message decoration block will be used,
  • firstname
    the 'ERRORMESSAGE:*' message decoration block will be used.

Error message content specification

As before, the content of the displayed error messages is determined according to the identifier and subject name of the error messages. For example, the validation error with the identifier 'MANDATORY' and the subject 'login' will look for a block with the ID 'MANDATORY:login'. When this block is available, its content will be used for the display of the error message. However it is now also possible to define more general blocks that will be used as fallbacks. Following is a list of the order of evaluation of the block IDs.

  • IDENTIFIER:subject (as explained before),
  • ERROR:subject (this is the ERROR literal, will be used when no block with a specific identifier is available),
  • IDENTIFIER:* (will be used for an identifier when no block for the specific subject is available),
  • ERROR:* (will be used as last resort when no other block matches).

If no block could be found to obtain the error message content from, 'IDENTIFIER:subject' will be displayed instead.

Error marking

Whenever validation errors occur, it's nice to be able to clearly mark the parts of the page where the errors have to be corrected. RIFE supports this by looking for 'MARK:subject' values, and these will be replaced by the content of the 'MARK:ERROR' block. Typically, this block will contain the HTML attribute to use a different CSS style.

For example:

<!--B 'MARK:ERROR'--> class="error_mark"<!--/B-->

<label[!V 'MARK:login'][!/V] for="login">login</label>
<div[!V 'MARK:login'/]><input type="text" name="login" id="login" size="18" /></div>

When the markings are generated:

ValidationBuilderXhtml builder = new ValidationBuilderXhtml();
builder.generateErrorMarkings(template, errors, null);

the result is the following when an error occurs for the login subject:

<label class="error_mark" for="login">login</label>
<div class="error_mark"><input type="text" name="login" size="18" /></div>

and if the subject is valid, this will be generated:

<label for="login">login</label>

<div><input type="text" name="login" size="18" /></div>

Selective mark positioning

Like for the error area and error message values, you can declare several subjects after the MARK prefix. The selection of which marking to fill in is done in the same way as the selection of the error area values. For each subject it goes over all the value IDs that contain it, the order of traversal is from the value with the most subject names to the one with the least. The first value ID where all specified subject names are erroneous, will be used. So the 'MARK:login,password' value will be used before the 'MARK:login' value.

Alternative markings

Instead of using the content of the 'MARK:ERROR' block for all the error markings, you can choose to differentiate them by adding a specific suffix, like this:

<!--B 'MARK:ERROR:MYALT'--> class="error_alternative"<!--/B-->

<label[!V 'MARK:MYALT:password'][!/V] for="password">password</label>
<div[!V 'MARK:MYALT:password'/]><input type="password" name="password" id="password" size="18" /></div>

This allows you to use, for example, different marking layouts for different forms on a same page or even for different sections of the same form.

[ top ]

Automated form building

HTML forms can now be automatically generated from beans. This is fully integrated with the constraints mechanism and the validation facility. To generate a form, you simply have to call the generateForm(Template template, Object beanInstance) method. If the bean is Constrained and contains constrained properties, RIFE will interpret them and convert them to corresponding HTML attributes. If the bean is Validated and contains validation errors, they will be properly processed: the error areas will be populated with appropriate error messages and the markings will be indicated where needed. If you have declared submission beans in your element, forms will be automatically generated when you print the template and previously submitted values will be displayed in the form fields.

Empty forms can also be generated from bean classes through the generateEmptyForm(Template template, Class beanClass) method.

Forms are constructed from fields and it's those fields that you have to specify in your template through specific value tags. According to each value ID prefix, RIFE knows which type of field to generate. The prefix is, as usual, followed by the property name. The following fields are supported:

<!--V 'FORM:INPUT:property'/-->

Generates a regular text field.

<!--V 'FORM:SECRET:property'/-->

Generates a text field where the entered text is not displayed.

<!--V 'FORM:TEXTAREA:property'/-->

Generates a text area for the input of multi-lined text.

<!--V 'FORM:RADIO:property'/-->

Generates radio buttons for the selection of one value from a set of possibilities.

<!--V 'FORM:CHECKBOX:property'/-->

Generates checkbox buttons for the selection of several values from a set of possibilities. It can also be used with just one value as a boolean switch.

<!--V 'FORM:SELECT:property'/-->

Generates a selection list.

<!--V 'FORM:HIDDEN:property'/-->

Generates a hidden field for implicit data submission.

Collection fields

The radio, checkbox and select fields allow the user to select from a collection of values. However, these values are rarely those that you want to display to the user in the interface. Most of the time a more descriptive label is shown for each option so that the interface becomes less cryptic. You can specify these labels in blocks in the template, the format of the label block ID is: 'FORM:LABEL:property:value'.

However, before being able to generate all these options, RIFE has to know what the possible values are for the field. This is currently only possible through the use of a ConstrainedProperty and its inList(String[]) method.

For example, consider the following constrained property:

bean.addConstraint(
    new ConstrainedProperty("colors").inList(new String[] {"black", "red", "blue"}));

and the following template excerpt:

<!--V 'FORM:SELECT:colors'/-->
<!--B 'FORM:LABEL:colors:blue'-->blue spots<!--/B-->
<!--B 'FORM:LABEL:colors:red'-->red spots<!--/B-->
<!--B 'FORM:LABEL:colors:green'-->green spots<!--/B-->

will generate:

<select name="colors">

<option value="red">red spots</option>
<option value="blue">blue spots</option>
<option value="green">green spots</option>
</select>

It's also possible that your values are dynamically added to a constrained property and that the labels can't be defined statically in your template through blocks. You therefore have the option to add resource bundles to a template instance. When looking for a label, RIFE will first check if 'property:value' keys are present in a registered ResourceBundle and use their values instead.

For example, consider the same constrained property as above (whose list could have been dynamically populated) and the following code that registers a ResourceBundle in the template (which could also have been dynamically populated):

template.addResourceBundle(new ListResourceBundle() {
public Object[][] getContents()
{
    return new Object[][] {
        {"colors:blue", "blue spots"},
        {"colors:red", "red spots"},
        {"colors:green", "green spots"}
    };
}});

with only this line in the template:

<!--V 'FORM:SELECT:colors'/-->

will generate exactly the same HTML code as above.

Displaying values

If you're not generating an empty form, RIFE will look at the actual values of your bean's properties and include them in the template generation. Text fields will thus be populated, checkboxes ticked, radio buttons selected, and select options highlighted. This makes it very easy to create forms where the user has to correct invalid data or where existing data can be edited.

Custom field attributes

While all relevant constraints of the properties will be examined and the corresponding HTML attributes generated, you'll often want to add other attributes that have nothing to do with the limits that are imposed on the data type. This is very easily done, all you have to do is write these custom attributes inside the field value tag as a default value. They will be added as attributes that are generated automatically.

For example, consider the following ConstrainedProperty:

bean.addConstraint(
    new ConstrainedProperty("login").maxLength(8);

and the following field value tag:

<!--V 'FORM:INPUT:property'>size="10"<!--/V-->

they will generate the following HTML:

<input type="text" name="login" size="10" maxlength="8" />

[ top ]

Addition of common database query execution patterns

The DbQueryManager class has been extended to become a convenience class that makes it very easy to control the queries that handle the retrieval, storage, update and removal of data in a database. All queries are executed in a connection of the Datasource that is provided to the constructor of the DbQueryManager.

A collection of convenience methods have been provided to quickly execute queries in a variety of manners without having to worry about the logic behind them and without having to remember to close the queries at the appropriate moment. These methods optionally interact with the DbPreparedStatementHandler and DbResultSetHandler classes to make it possible to fully customize the executed queries. The following categories of worry-free methods now exist:

  • execute an update query directly, eg.:
    executeUpdate(Query),
  • execute a customizable update query, eg.:
    executeUpdate(Query, DbPreparedStatementHandler),
  • execute a customizable select query, eg.:
    executeQuery(Select, DbPreparedStatementHandler),
  • check the result rows of a customizable select query, eg.:
    executeHasResultRows(Select, DbPreparedStatementHandler),
  • obtain the first value of a customizable select query, eg.:
    executeGetFirstString(Select, DbPreparedStatementHandler),
  • fetch the first row of a customizable select query, eg.:
    executeFetchFirst(Select, DbRowProcessor, DbPreparedStatementHandler),
  • fetch the first bean of a customizable select query, eg.:
    executeFetchFirstBean(Select, Class, DbPreparedStatementHandler),
  • fetch all rows of a customizable select query, eg.:
    executeFetchAll(Select, DbRowProcessor, DbPreparedStatementHandler),
  • fetch all beans of a customizable select query, eg.:
    executeFetchAllBeans(Select, Class, DbPreparedStatementHandler),

Lower-level methods are also available for the sake of repetitive code-reduction. To obtain a prepared statement that corresponds to a specific SQL command, use the getPreparedStatement(Query) methods and to execute regular statements directly, use the executeQuery(Query) method.

Finally, since DbStatement and DbPreparedStatement instances preserve a reference to their resultset, it is easy to iterate over the rows of a resultset with the methods fetch(ResultSet, DbRowProcessor) or fetchAll(ResultSet, DbRowProcessor).

You can look at the javadocs of the DbQueryManager class to get detailed information about all the available methods and their usage.

Customizing prepared statements

With a child class of DbPreparedStatementHandler, you are able to set the parameters of a DbPreparedStatement before the actual execution of any logic by overriding the setParameters(DbPreparedStatement) method.

For example:

DbQueryManager manager = new DbQueryManager(datasource);
Insert insert = new Insert(datasource);
insert.into("person").fieldParameter("name");
final String name = "me";
int count = manager.executeUpdate(insert, new DbPreparedStatementHandler() {
        public void setParameters(DbPreparedStatement statement)
        {
            statement
                .setString("name", name);
        }
    });

If you need to customize the entire query execution, you can override the performUpdate(DbPreparedStatement) and performQuery(DbPreparedStatement) methods. Note that these methods are actually responsible for calling the setParameters(DbPreparedStatement) method, so if you override them you either have to call this method yourself or include the code in the overridden method.

The DbPreparedStatementHandler class has both a default constructor and one that can take a data object. This can be handy when using it as an anonymous inner class, when you need to use variables inside the inner class that are cumbersome to change to final in the enclosing class.

Customizing results

With a child class of DbResultSetHandler, you are able to perform custom logic with the resultset of a query by overriding the concludeResults(DbResultSet) method and returning an object.

For example:

DbQueryManager manager = new DbQueryManager(datasource);
Select select = new Select(datasource);
select
    .field("first")
    .field("last")
    .from("person");
String result = (String)manager.executeQuery(select, new DbResultSetHandler() {
        public Object concludeResults(DbResultSet resultset)
        throws SQLException
        {
            return resultset.getString("first")+" "+resultset.getString("last");
        }
    });

The result string will contain the full name of the person that was returned by the SQL query.

[ top ]

User identification facility built on top of the authentication

It's becoming quite common in websites to not force users to be authenticated before they are able to access a section. Instead, pages show different content for and offer other functionalities to users that can correctly be identified. An anonymous visitor simply sees alternative content and is probably restricted in the actions that he can perform.

RIFE now offers an easy way to develop websites with these functionalities. User identification is tied to authentication and the identity of a user is obtained from the authentication managers that are used by a specific authentication element. Similar to authentication, RIFE uses element inheritance for the identification.

An identification element, element/identified.xml, can for example be declared like this:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element extends="rife/authenticated/identified.xml">
    <childtrigger name="authid"/>

</element>

The child trigger name should be the same as the one that is used by the authentication element. Most probably this will be authid, unless you're using different parallel authentication schemes.

To include an identification element in the site structure, you have to tell it which authentication element should be used to perform the identification. This is done by setting the authentication element ID as the value of the property with the name authElementId.

For example:

<element id="AUTH" file="element/authentication.xml"/>
<element id="IDENTIFIED" file="element/identified.xml">
    <property name="authElementId">AUTH</property>

</element>

All the elements that depend on the identification now simply have to inherit from the IDENTIFIED element. To gain access to an identified user, you should obtain the request attribute with the name 'identity'.

For example:

public class SomeElement extends Element
{
    public void processElement()
    {
        RoleUserIdentity identity = (RoleUserIdentity)getRequestAttribute("identity");
        if (null == identity)
        {
            // perform anonymous logic
        }
        else
        {
            // perform identified logic
        }
    }
}

Note that all authentication elements also provide identification functionalities. So if your element already inherits from such an element there is no use to make it inherit also from an identified element.

[ top ]

OGNL support in template engine

RIFE now contains OGNL support for templates. Contrary to how OGNL is used in other web application frameworks, RIFE doesn't allow it to retrieve values and fill them into the template since that would make templates more active than we like. OGNL is used to provide boolean expressions that will be evaluated at runtime to automatically assign the content of blocks to values.

For example:

<!--V 'OGNL:name'-->Evaluated to false<!--/V-->

<!--B 'OGNL:name:[[ true ]]'-->
    Evaluated to true
<!--/B-->

In this example, the expression in between the [[ and ]] will be evaluated. If the expression returns true, the value will be replaced with 'Evaluated to true'. If the expression retuns false however, the value will be not be replaced by any block but keep its default content: 'Evaluated to false'. This allows for on-the-fly template changes with or without developer intervention.

For the expressions to be useful, they have to be evaluated against a context. Therefore it's possible to set variables from an element that can be easily accessed from within the OGNL expression.

For example:

In an element

public class OGNLTest extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("test.ognl");
        
        template.setExpressionVar("show_block", "yes");
        
        print(template);
    }
}

In a template

<!--V 'OGNL:name'-->Block is NOT being displayed<!--/V-->

<!--B 'OGNL:name:[[ #show_block == "yes" ]]'-->
    Block is being displayed
<!--/B-->

Apart from the expression variables, each OGNL expression is evaluation against a current root object whose methods and properties you can access using the regular OGNL syntax. This root object is by default the template instance that you are processing.

To make it easy to write expressions against commonly used contexts, RIFE also provides specialized OGNL tags that set different root objects. Currently the OGNL:ROLEUSER and OGNL:CONFIG tags have been provided.

OGNL:ROLEUSER

The root object will be the RoleUserAttributes of an identified user and it will be null of no identification could be performed. Since the user attributes don't contain any login property; the login of the user is set as an expression variable. For consistancy all the other user attributes are also provided through variables (#login, #password, #userId, #roles).

This specialized OGNL tag makes it very easy to conditionally show parts of an interface according to the credentials of a user.

For example:

<!--V 'OGNL:ROLEUSER:role1'-->User is not in role "admin"<!--/V-->
<!--V 'OGNL:ROLEUSER:login1'-->User in named "moderator"<!--/V-->

<!--B 'OGNL:ROLEUSER:role1:[[ isInRole("admin") ]]'-->
    User is in role "admin"
<!--/B-->

<!--B 'OGNL:ROLEUSER:login1:[[ #login == "moderator" ]]'-->
    User is named "moderator"
<!--/B-->

OGNL:CONFIG

The root object will be the default configuration instance: Config.getRepInstance(). This tag can for example be used to create variants of an application and conditionally show parts of the interface according to configuration values. You can thus maintain a single codebase and just modify the configuration for different installations.

For example:

<!--V 'OGNL:CONFIG:bool'>DISPLAY_BLOCK is false<!--/V-->

<!--V 'OGNL:CONFIG:string'>STRING_VALUE is 'do not match'<!--/V-->

<!--B 'OGNL:CONFIG:bool:[[ getBool("DISPLAY_BLOCK") ]]-->
    DISPLAY_BLOCK is true
<!--/B-->

<!--B 'OGNL:CONFIG:string:[[ getString("STRING_VALUE") != "do not match" ]]-->
    STRING_VALUE is not 'do not match'
<!--/B-->

Evaluation order

OGNL blocks are always evaluated late (i.e. at time of template printing). If there is a need to evaluate early or repetitively, you can use the following three methods:

public class OGNLTest extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("test.ognl");
        
        t.evaluateOgnl("ognl_block");
        t.evaluateOgnlConfig("ognl_config_block");
        
        evaluateOgnlRoleUser(t, "ognl_role_user_block");
        
        print(t);
    }
}

Please note that the method to evaluate an OGNL:ROLEUSER block is not a member of Template, but an inherited method of Element since it needs to access the request context.

More information on OGNL syntax can be found at the OGNL User's Guide.

[ top ]

Support for Groovy as element implementation language

When Groovy is present in the classpath of the web application, it's now possible to implement elements in this very nice scripting language. The only requirement is that the source files have to have the .groovy extension.

For example, Simple.groovy:

import com.uwyn.rife.engine.Element

class Simple extends Element
{
    void processElement()
    {
        if (hasSubmission("login"))
        {
            print(getParameter("login")+","+getParameter("password"))
        }
        else
        {
            switch (getInput("input1"))
            {
                case "form":
                    print(<<<EOS

<html><body>
<form action="${getSubmissionQueryUrl("login")}" method="post">
<input name="login" type="text">
<input name="password" type="password">
<input type="submit">
</form>
</body></html>
EOS)
                    break
                default:
                    print(getInput("input1")+","+getInput("input2"))
                    break
            }
        }
    }
}

[ top ]

Configurable state storage with support for server-side storage in sessions

The use of conventional sessions have always been disabled in RIFE and data has always been passed along the datalinks by using the query string or form post parameters. It has been explained in detail why regular sessions are discouraged and how we managed to offer many of their benefits with a minimum of the drawbacks.

RIFE already provides facilities to handle the data flow of an application by setting up the required datalinks in the site structure. You're not actually interested in storing data explicitly in the session, you're however interested in the fact that the data and the state isn't transferred through the client-side but remains on the server. Therefore, we added a configurable state storage mechanism and implemented one for the query string and one for the session; others can easily be added later (database, LDAP, ...). To indicate that you want data to be stored elsewhere, you simple declare state boundaries in the site structure.

For example:

<state store="session">
    <element id="SOURCE" file="element/source.xml" url="/source">
        <flowlink srcexit="exit1" destid="DESTINATION"/>

        <datalink srcoutput="output1" destid="DESTINATION" destinput="input2"/>
        <datalink srcoutput="output2" destid="DESTINATION" destinput="input1"/>
    </element>
    <element id="DESTINATION" file="element/destination.xml" url="/destination"/>
</state>

When you generate an URL for this exit you'll not see query string parameters for the transferred data, but a stateid parameter instead. This refers to a unique identifier that RIFE will use to look up the corresponding state. The identifier changes at each request, so people can still use the back button and follow alternative application paths. This ensures also that each URL still corresponds to one particular point in the application and the state can never get out of sync.

The default state store is still in the query, but you have the possibility to use it explicitly when you're inside a session storage part and still want to have some data transferred through the client-side. You can freely nest state storage specifications and they will even work across groups and sub-sites.

[ top ]

Refactored the repository for easy mocking and testing

The repository has been totally refactored to make it possible to easily plug-in other repository implementations or other instances. The Rep class has dramatically reduced in size and now only acts as a central access point to interface with the application-wide default repository. The repository and the participants are now defined through their respective Repository and Participant interfaces. The standard implementation has been extracted into separate classes that are called BlockingRepository and BlockingParticipant.

It's now very easy to create a custom Repository implementation for testing purposes and to plug it in as the default repository. This example below creates a mock repository with a single mock participant to setup some specific configuration settings that might be needed for testing. The following class is all that is needed:

public class MockRepository implements Repository
{
    class MockParticipant extends SingleObjectParticipant
    {
        public Object getObject()
        {
            Config config = new Config();
            config.setParameter("key", "value");
            return config;
        }
    }
    
    private Map mParticipants = new HashMap();
    
    public MockRepository()
    {
        mParticipants.put("ParticipantConfig", new MockParticipant());
    }
    
    public boolean hasParticipant(String name)
    {
        return mParticipants.containsKey(name);
    }

    public Participant getParticipant(String name)
    {
        return (Participant)mParticipants.get(name);
    }
    
    public Collection getParticipants(String name)
    {
        return mParticipants.values();
    }
    
    public void cleanup() {}
}

Whenever you need to use this repository instead of the current default one, you just have to add this line of code:

Rep.setDefaultRepository(new MockRepository());

If you want to restore the previously active repository afterwards, you can retrieve it beforehand with

Rep.getDefaultRepository();

and set it back later.

[ top ]

Added jumpstart distribution

RIFE now provides a jumpstart archive that makes it easy for developers to set up a new application quickly and start developing immediately. The jumpstart contains everything to get started and provides some very common structures as a recommendation for the lay-out of a RIFE application.

The enclosed readme contains all the information that needed to use the jumpstart.

[ top ]

Miscellaneous web engine enhancements

Support for automatic web application root URL substitution

It's often a problem to develop web applications that are completely relocatable when reusing common template parts. You can now use the WEBAPP:ROOTURL template value ID which will be automatically substituted by the root URL of the web application. You typically use this in the base HTML tag and then write all the URLs in the templates as relative URLs according to the root directory of the web application, like this:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
    <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type" />
    <base href="[!V 'WEBAPP:ROOTURL'/]" />
    <title>The title</title>
    <link rel="stylesheet" type="text/css" href="style/pub.css" />

</head>

<body><img src="images/rife-logo.png" width="251" height="105" border="0" alt="RIFE logo" /></body>
</html>

More information about the HTML base element can be found on the w3c site.

Added support for default implementation directory

Similar to the default directories that are available for elements, sites and templates, you can now put element implementations in an "implementations/" directory. This allows implementation source files to reside in a seperate hierarchy from the other application files. Note that this has no influence on the loading of already compiled class files.

Automatic element ID generation

When you declare an element in the site structure, the definition of the element ID is now not required anymore. If you omit it, RIFE will look at the XML filename of the element declaration and use the base part for the element ID.

For example, the following definition:

<element file="element/engine/simple.xml" url="/simple"/>

is exactly the same as:

<element id ="simple" file="element/engine/simple.xml" url="/simple"/>

Element instance property definition

When you define elements in the site structure, you can set values for named properties that will only be set for that particular element instance. For example:

<element id="PROPERTIES1" file="element/engine/properties.xml" url="/properties1">

    <property name="property1">property1a</property>
    <property name="property2">property2a</property>
</element>
<element id="PROPERTIES2" file="element/engine/properties.xml" url="/properties2">
    <property name="property1">property1b</property>

    <property name="property3">property3b</property>
</element>

where the element is implemented as follows:

public class Properties extends Element
{
    public void processElement()
    {
        print("Property 1 = "+getProperty("property1"));
        print("Property 2 = "+getProperty("property2"));
        print("Property 3 = "+getProperty("property3"));
    }
}

When you visit the "/properties1" URL, the output will be:

Property 1 = property1a
Property 2 = property2a
Property 3 = nul

and for the "/properties2" URL, the output will be:

Property 1 = property1b
Property 2 = null
Property 3 = property3b

This is currently really a "poor-man's" version of IoC where only literal values can be injected. In a later version we are planning to extend this to a much more flexible system.

Element implementations through interfaces

It's now also possible to create elements by implementing the ElementAware interface. You have to implement one additional method public void noticeElement(ElementSupport element) which should be used to store the actual element instance that you receive from the engine in a member variable. You can then use this member variable to perform your logic in the processElement() method.

For example:

public class SimpleInterface implements ElementAware
{
    private ElementSupport    mElement = null;
    
    public void noticeElement(ElementSupport element)
    {
        mElement = element;
    }
    
    public void processElement()
    {
        mElement.print("Just some text "+
            mElement.getRemoteAddr()+":"+
            mElement.getRemoteHost()+":"+
            mElement.getPathInfo());
    }
}

[ top ]

Backwards incompatible changes

Unfortunately it was not possible to keep everything fully backwards compatible with this release and that's mainly due to the new validation system. Below are the steps you need to undertake to migrate your application to the new release:

implements ValidationRule

becomes=>

extends AbstractValidationRule

checkNotNull(int)

becomes=>

checkNotEmpty(int)

RepParticipant

becomes=>

BlockingParticipant

Error message block IDs change from upper-cased property names to lower-cased property names.

You should certainly check your login forms since the error block IDs change there too. Of course, it would be even better to migrate all your forms to the new form builder and validation facility.


IDENTIFIER:SUBJECT

becomes=>

IDENTIFIER:subject

NOTNUMERIC:SUBJECT

becomes=>

NOTNUMERIC:subject

All validity checks have moved from the com.uwyn.rife.site.ValidationRule class to the com.uwyn.rife.site.ValidityChecks class.


check*()

becomes=>

ValidityChecks.check*()

[ top ]

posted by Geert Bevin in RIFE on Feb 9, 2004 6:22 PM : 2 comments [permalink]
 
Codeguide Amethyst build 809 released

Omnicore released another update to their awesome IDE. More information about it here. The changelog is available here.

posted by Geert Bevin in Java on Feb 5, 2004 7:51 AM : 0 comments [permalink]
 
Released RelativeLayers 0.9.7

Oh my, it's been almost two years since I've updated RelativeLayers. This release just adds support for Safari and to my surprise, this neat newcomer supports everything that's needed to run all features of RelativeLayers without any problems. Apple really did a great job on this browser.

Anyway, get the new release here or gather more information at the project's homepage.

Have fun!

posted by Geert Bevin in Computing on Feb 3, 2004 12:02 AM : 0 comments [permalink]
 

 
 
 
Google
rifers.org web