Important note

The information in this HOWTO pertains to Turbine 2.2. Please refer to the Torque Security Service page for information on extending TurbineUser in Turbine 2.3 and beyond.

Introduction

This is a HOWTO on extending the TurbineUser and its functionality. The motivating factors for extending TurbineUser are:

  1. to be able to make use of TURBINE_USER.USER_ID as a foreign key in application tables; and
  2. to be able to represent additional user attributes by adding columns to TURBINE_USER.

The example herein uses a very simple object model with only one table. This table would be defined in your project-schema.xml file. To illustrate solutions to both of our motivators we will:

  1. Add a REVIEWED_BY_USER_ID column to the BOOK application table. This will also include a foreign key reference back to TURBINE_USER.
  2. Add a TITLE column to TURBINE_USER.

Important Note: This solution is functionally incomplete in that it does not support the ability to use TurbineUser as a commit point for a transaction. See the very end of this document for further details.

It is also important to note that this HOWTO is intended for use with the database implementation of the SecurityService. I will not address how to extend TurbineUser for use with any other implementation.

How does TurbineUser actually work?

The inplementation of TurbineUser and TurbineUserPeer is distributed with Turbine in the org.apache.turbine.om.security package. It is _NOT_ generated by Torque at this time. There are no corresponding Base* classes nor a TurbineUserMapBuilder either.

TurbineUser implements the User interface (from the same package). This interface is used by the Security Service and a few other services as well. This is the reason behind not generating TurbineUser through Torque. It would have no idea how to provide an implementation of the User interface leaving that inplementaion to you.

The MapBuilder for TurbineUser is org.apache.turbine.util.db.map.TurbineMapBuilder. This MapBuilder is also used by Turbine for the other Turbine* classes that are defined in the turbine-schema.xml file distributed with Turbine.

The turbine-schema.xml file is the source of a small problem. The ant task used to generate your OM layer will generate OM objects for all of the tables defined in this file along with the MapBuilders. These classes do not really hurt anything but they can be a source of confusion. There are not used by Turbine for anything!

One valid reason for leaving this file in place is for the ant task that will create your database schema for you. Since the task will not modify the schema for you after it is created (to add/remove columns, indexes, etc), it is of little use after the database is created other than to serve as a reference.

Later in this discussion, you will be given instruction to rename this file to prevent it from being used by the ant tasks as well as removing any objects generated as a result.

Another interesting fact about TurbineUser is the way in which data stored in the database is accessed. Instaed of using using private attributes for storage within the object, all attibutes are stored in a hashtable (known herein as the perm hashtable). Access to the perm hashtable is controlled through the getPerm/setPerm methods.

When the object is returned from the database, data from the columns are added to the perm hashtable for you. When you save the object, the data is removed from the perm hashtable and written to the appropriate database columns.

As you will read later, any data left in the perm hashtable that does not have a mapped database column (more on this later) during the save operation will be serialized and written to the TURBINE_USER.OBJECTDATA column.

Another very nice feature of TurbineUser is the getTemp/setTemp methods. This allows you to store data in TurbineUser for the duration of the session. It will not be written to your persistent storage. It is ONLY for the session. Keep in mind that the login action will replace the user object in the session with a user object from your persistent storage thus removing any data you might have stored in the user.

Modifications to project-schema.xml

Here is the sample project-schema.xml file before making modifications to reference TURBINE_USER.


<database defaultIdMethod="native" defaultJavaType="object">
    <table name="BOOK">
        <column name="BOOK_ID" required="true" primaryKey="true" type="INTEGER"/>
        <column name="TITLE" required="true" type="VARCHAR"/>
        <column name="AUTHOR" required="true" type="VARCHAR"/>
    </table>
</database>

      

First we update the schema to include an alias definition of TurbineUser as well as the desired foreign key references. Note how the TurbineUser definition refers to a pair of adapter classes (we will create these shortly) and how it is only necessary to define the columns we are referring to as foreign keys elsewhere in the application database. Note also the addition of REVIEWED_BY_USER_ID as a foreign key in the BOOK table.


<database defaultIdMethod="native" defaultJavaType="object">
    <table name="EXTENDED_USER" alias="TurbineUser"
            baseClass="org.mycompany.sampleapp.om.TurbineUserAdapter"
            basePeer="org.mycompany.sampleapp.om.TurbineUserPeerAdapter">
        <column name="USER_ID" required="true" primaryKey="true" type="INTEGER"/>
    </table>
    <table name="BOOK">
        <column name="BOOK_ID" required="true" primaryKey="true" type="INTEGER"/>
        <column name="TITLE" required="true" type="VARCHAR"/>
        <column name="AUTHOR" required="true" type="VARCHAR"/>
        <column name="REVIEWED_BY_USER_ID" type="INTEGER"/>
        <foreign-key foreignTable="EXTENDED_USER">
            <reference local="REVIEWED_BY_USER_ID" foreign="USER_ID"/>
        </foreign-key>
    </table>
</database>

      

Notice the attribute on the database tag for defaultJavaType. I used "object" as the value. The default is "primative". You do not have to use "object"!!!

In the last version of Turbine (2.1), the only option was to use the primative types. However, this posed a small problem. It was not possible to have number or boolean types that contained null values. If a null value was found in the database for columns of these types, the value returned from the OM object was 0 or false, respectively. This also implies that you can not have non-required foreign key references back to TURBINE_USER. The primary key of TURBINE_USER is a number.

Deciding how additional data in Turbine User will be stored

There are two ways in which additional data in your extended TurbineUser object can be stored. The first, and simplest method, is to store it in the perm hashtable. As described earlier, any data in the perm hashtable not mapped to a database column will get serialized into the TURBINE_USER.OBJECTDATA column. The second, and perferred method, is to use additional database column(s) for storage.

Although storing the additional data in the TURBINE_USER.OBJECTDATA column is the simplest method, it has a few drawbacks.

  1. You will not have easy access to the data through SQL.
  2. You will not be able to specify criteria to select TurbineUser objects from the database by filtering on data stored in TURBINE_USER.OBJECTDATA.
  3. There is an upper limit to the amount of data that can be stored in the TURBINE_USER.OBJECTDATA column. A few users have reported on the mailing list that they ran into this problem. The limit will vary from one database to another. This limit is reduced by persistent pull tools (see the docs on the PullService for details), if you have any in your project, as they are stored using this method by the PullService.

If you do decide to store some of your additional attributes in the TURBINE_USER.OBJECTDATA column, you need to be careful of possible conflicts with the keys used in the perm hashtable by TurbineUser. When the object is saved to the database, this data is removed from the perm hashtable and written to the correct columns. The keys used in the perm hashtable are the uppercase names of the columns. The column names are not fully qualified with the names of the table like TURBINE_USER.LAST_NAME. Instead the key used is LAST_NAME. See org.apache.turbine.util.db.map.TurbineMapBuilder for a complete list of the keys used for the default columns.

If you choose to use additional database columns, you will not have any of the drawbacks mentioned above. Using the additional columns is the approach that should be taken unless you have a VERY good reason not to do so.

Implementing adapter and map builder classes

First, we will extend the map builder used by Turbine for the Turbine* classes to add the additional column(s) that we will be using in TURBINE_USER.


package org.mycompany.sampleapp.util.db.map;

import org.apache.torque.Torque;
import org.apache.torque.map.TableMap;
import org.apache.turbine.util.db.map.TurbineMapBuilder;

/**
 * Used to add the mapping of additional columns into any of the
 * TURBINE_* tables.  This can be implemented as an empty class
 * if you are not adding any additional database columns.
 */
public class TurbineMapBuilderAdapter extends TurbineMapBuilder
{
    /*
    * Note: The getUser_*() methods in this class should be static, but
    * getTableUser() and the initial field level methods are incorrectly
    * declared in TurbineMapBuilder and hence we must use non-static methods
    * here.
    */

    /**
     * Gets the name of the database column that we are adding to
     * TURBINE_USER.
     * @return The name of the database column.
     */
    public static String getTitle()
    {
        return "TITLE";
    }

    /**
     * Gets the fully qualified column name (TABLE.COLUMN) that will be
     * used to store the Title attribute in the extended TurbineUser object.
     * @return The fully qualified database column name.
     */
    public String getUser_Title()
    {
        return getTableUser() + '.' + getTitle();
    }

    /**
     * Creates the map used by Torque to persist the Turbine* objects
     * to the TURBINE_* tables.
     * @throws Exception generic error
     */
    public void doBuild() throws Exception
    {
        // the superclass version of the MapBuilder must be called to create
        // the mappings for the default columns.
        super.doBuild();

        // When you add a column to the database map, the map must know
        // what type of data will be stored in the column.  For that
        // purpose, we will create a few dummy objects to serve as
        // data type indicators.  Not all of them are used in our example.
        Integer dummyInteger = new Integer(0);
        Date dummyDate = new Date();
        String dummyString = new String();

        // Add extra column.
        TableMap tMap = Torque.getDatabaseMap().getTable(getTableUser());
        tMap.addColumn(getTitle(),dummyInteger);
    }
}

      

Now we will implement the pair of adapters we referred to in our schema. First we implement TurbineUserAdapter to provide access to the primary key as well any additional attibutes we are adding.


package org.mycompany.sampleapp.om;

import org.apache.turbine.om.security.TurbineUser;
import org.apache.torque.om.NumberKey;
import org.apache.turbine.util.ObjectUtils;

/**
 * This class extends TurbineUser for the purpose of adding get/set methods
 * for accessing additional attributes.
 */
public class TurbineUserAdapter extends TurbineUser
{
    /** Used as the key in the perm hashtable.  */
    public static final String TITLE = "TITLE";

    /**
     * Gets the userId of the user.  This is also the primary key
     * of the TURBINE_USER table.  The return type of Integer is
     * valid only if the javaType for the ExtendedUser table
     * is "object".  If it is "primative", the return type
     * should be changed to int.
     * @return the user id
     */
    public Integer getUserId()
    {
        return new Integer(((NumberKey)getPrimaryKey()).intValue());
    }

    /**
     * Sets the title of the user.
     * @param title the user's title
     */
    public void setTitle(String title)
    {
        setPerm(TITLE, title);
    }

    /**
     * Gets the user's title
     * @return the user's title
     */
    public String getTitle()
    {
        String tmp = null;
        try
        {
            tmp = (String) getPerm(TITLE);
            if ( tmp.length() == 0 )
            tmp = null;
        }
        catch (Exception e)
        {
        }
        return tmp;
    }
}

      

Note: This class uses the setPerm and getPerm methods to access the data even though we are going to use the TURBINE_USER.TITLE column for storage. As mentioned earlier, the data is removed from the hashtable when the object is saved to the database and written to the correct database column. Likewise, the when the object is retrieving data from the database, the data from the TURBINE_USER.TITLE column is added to the perm hashtable with the TITLE key.

Next comes TurbineUserPeerAdapter to which we add details of the new database columns.


package org.mycompany.newapp.om;

import org.mycompany.sampleapp.util.db.map.TurbineMapBuilderAdapter;
import org.apache.torque.om.ObjectKey;
import org.apache.torque.util.Criteria;
import org.apache.turbine.om.security.peer.TurbineUserPeer;

/**
 * This class extends TurbineUserPeer for the purpose of mapping additional
 * database columns.  You can implement this as an empty class if you are not
 * using any additional database columns.
 */
public class TurbineUserPeerAdapter extends TurbineUserPeer
{
    /** the default database name for this class */
    public static final String DATABASE_NAME = "default";

    /** Used to build the map for the extended Turbine User */
    private static final TurbineMapBuilderAdapter mapBuilder =
        (TurbineMapBuilderAdapter)getMapBuilder("org.mycompany.sampleapp.util.db.map.TurbineMapBuilderAdapter");

    /** The fully qualified name of the database table */
    public static final String TITLE = mapBuilder.getUser_Title();

    /**
     * Builds a criteria object to select by a primary key value.  Of course,
     * it could also be used for an update or delete.
     * @param pk Primary key to select/update/delete
     * @return A Criteria object built to select by primary key
     */
    public static Criteria buildCriteria(ObjectKey pk)
    {
        Criteria crit = new Criteria();
        crit.add(TurbineUserPeer.USER_ID, pk.getValue());

        return crit;
    }
}
      

Generating the OM layer

Before generating the OM layer, see if you have a turbine-schema.xml file in your WEB-INF/conf directory. If you do, rename this file to turbine-schema.xml.nogenerate. Also, the only Turbine* classes that you should have in your om package are the two adapters that you created earlier. Any other Turbine* or BaseTurbine* class in your om package should be removed. You should also remove any om.map.Turbine* classes that might have been generated.

Warning: Do not run the init ant task after doing this. That task will drop and recreate your database. Without the turbine-schema.xml file present, the TURBINE_* tables will not be recreated!

These classes were generated from the turbine-schema.xml file. The project-om ant task will process any file in your WEB-INF/conf directory ending in -schema.xml and generate the appropriate classes from that definition. Renaming the turbine-schema.xml file stops these extra classes from being generated.

Next, you will need to manually create any additional database columns that you will be using in TURBINE_USER. Make sure that the name of the columns match what you speificied in TurbineUserPeerAdapter.

We can now use project-om ant task to generate our OM layer using the adapter classes we defined above.

With any luck everything will compile okay and we are only a small step away from being able to use the new OM layer.

Modifications to TurbineResources.properties

The last step is to tell Turbine about the new classes we are using for Users and MapBuilder. To do this we need to update the following entries in TurbineResources.properties:


database.maps.builder=org.mycompany.sampleapp.util.db.map.TurbineMapBuilderAdapter
services.SecurityService.user.class=org.mycompany.sampleapp.om.ExtendedUser
services.SecurityService.userPeer.class=org.mycompany.sampleapp.om.ExtendedUserPeer

      

That is basically it. We can now modify our application to utilise the new columns via the methods defined in the OM objects we have modified. Note that in order to access the new methods in ExtendedUser we need to cast from TurbineUser thus:


ExtendedUser user = (ExtendedUser) data.getUser();

      

Enjoy.

Additional Information

For those of you that are attempting to extend TurbineUser to make use of TURBINE_USER.USER_ID as a foreign key in your application tables here is one small problem.

Torque generates some pretty useful code that enables you to add related objects to a common parent object and then save them all to the database in one transaction. You can do something like this:


OrderDetail od1 = new OrderDetail(500, "item1");
OrderDetail od2 = new OrderDetail(100, "item2");
Order o = new Order(clientId);
o.addOrderDetail(od1);
o.addOrderDetail(od2);
o.save();

        

This is very helpful - you don't need to worry about the ids that connect the child rows to the parent rows and you don't need to worry about the transaction necessary to insert these into the database as one atomic operation. It also provides:


o.save(dbConn);

        

which can be used when you have other data you want to commit in a transaction you are managing yourself.

Now here is the problem. After extending TurbineUser in the manner described above, the methods are generated to add records related the extended user class, but the data itself is ignored when ExtendedUser.save() is invoked. Here is an example:


Book book = new Book();
data.getParameters().setProperties(book);
ExtendedUser user = (ExtendedUser) data.getUser();
user.addBook(book);
user.save(); // !!!! book is not saved !!!!

        

Of course replacing the last two lines with:


book.setReviewedByUserId(user.getUserId());
book.save();

        

will in fact save the Book, this is beside the point - the other method addBook() shouldn't exist if it doesn't work and this is a trivial example.

In addition to this, there is no equivalent save(dbConn) method provided to allow this update to be combined with others within a single database transaction.

There are a few solutions to this problem. One would be to fix the torque template responsible for generating the code. Another would be to implement the methods yourself. Before exploring either of these options, you need to ask yourself if this problem will even affect you. If not, don't worry about it.

Implementing the methods yourself might be the simpliest route for someone new to Turbine and Torque. This simply involves overriding the addBook() method in the example above with the two lines shown previously. Of course, You would have to do this for each and every one that Torque generates for you.

The same idea applies to the save(dbConn) method. Simply override that method in your ExtendedTurbineUser class (or whatever you called your class) to perform the operation correctly. You can get sample code from one of the other classes that Torque generated for you.

If you want Torque to generate to code for you, you will have to modify the Object.vm template that Torque uses.

The other solution involves altering a torque template so that the code that generates the aspects of the save() method that handle related tables is allowed to execute for the TurbineUserAlias class. You need to apply the following patch (most likely to the version of Object.vm copied to the WEB-INF/build/bin/torque/templates/om directory for your application):


Index: jakarta-turbine-2/conf/torque/templates/om/Object.vm
===================================================================
RCS file: /home/cvspublic/jakarta-turbine-2/conf/torque/templates/om/Object.vm,v

retrieving revision 1.3
diff -u -r1.3 Object.vm
--- jakarta-turbine-2/conf/torque/templates/om/Object.vm        24 Oct 2001 18:1
2:21 -0000      1.3
+++ jakarta-turbine-2/conf/torque/templates/om/Object.vm        16 Jan 2002 14:0
0:43 -0000
@@ -787,7 +787,7 @@
#end     ## ends the if(addGetByNameMethod)


-#if (!$table.isAlias() && $addSaveMethod)
+#if ((!$table.isAlias() || $table.Alias == "TurbineUser") && $addSaveMethod)
/**
* Stores the object in the database.  If the object is new,
* it inserts it; otherwise an update is performed.
@@ -880,6 +880,9 @@
{
alreadyInSave = true;
#end
+    #if ($table.isAlias() && $table.Alias == "TurbineUser")
+        super.save();
+    #else
if (isModified())
{
if (isNew())
@@ -892,6 +895,7 @@
${table.JavaName}Peer.doUpdate(($table.JavaName)this, dbCon);
}
}
+    #end

#if ($complexObjectModel)
#foreach ($fk in $table.Referrers)

        

You will need to manually remove the line wrapping from the above patch. This patch is only included here to give you an idea of what would need to be changed in Torque. The patch can not be applied to the current version of Torque simply because it is so outdated.

Notice how although you can go user.save(dbCon); the TurbineUser record will not be part of the transaction.

I would suggest that you override the methods that do not get generated correctly in your ExtendedTurbineUser class. Do so ONLY if this problem will affect you.

The real solution to this problem is in modifing Turbine in such a way to allow Torque to generate the TurbineUser object. This would eliminate the need to extend TurbineUser altogether. There would still be the need to add additional columns and/or create FK references to the TURBINE_USER table. It would would only involve modifing the schema files and letting Torque do all of the work instead of creating the adapter classes and modifing the map builder.

Note: The last solution mentioned should be ready for Turbine 2.3. This should make life easier for everyone!!!