blob: 78bd6a4f66d639e49de735931a761eaf3ce3f138 [file] [log] [blame]
<?xml version="1.0"?>
<!--
/*
* Copyright 2001-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<document>
<properties>
<title>Extend User Howto</title>
</properties>
<body>
<section name="Important note">
<p>
The information in this HOWTO pertains to Turbine 2.2. Please refer
to the <a href="../services/torque-security-service.html">Torque
Security Service</a> page for information on extending TurbineUser
in Turbine 2.3 and beyond.
</p>
</section>
<section name="Introduction">
<p>
This is a HOWTO on extending the TurbineUser and its
functionality. The motivating factors for extending TurbineUser
are:
<ol>
<li>
to be able to make use of TURBINE_USER.USER_ID as a foreign key in
application tables; and
</li>
<li>
to be able to represent additional user attributes by adding columns
to TURBINE_USER.
</li>
</ol>
</p>
<p>
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:
<ol>
<li>
Add a REVIEWED_BY_USER_ID column to the BOOK application table. This will
also include a foreign key reference back to TURBINE_USER.
</li>
<li>
Add a TITLE column to TURBINE_USER.
</li>
</ol>
</p>
<p>
<b>Important Note:</b> 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.
</p>
<p>
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.
</p>
</section>
<section name="How does TurbineUser actually work?" >
<p>
The inplementation of TurbineUser and TurbineUserPeer is distributed
with Turbine in the <code>org.apache.turbine.om.security</code>
package. It is _NOT_ generated by Torque at this time. There are
no corresponding Base* classes nor a TurbineUserMapBuilder either.
</p>
<p>
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.
</p>
<p>
The MapBuilder for TurbineUser is
<code>org.apache.turbine.util.db.map.TurbineMapBuilder</code>. This
MapBuilder is also used by Turbine for the other Turbine* classes
that are defined in the turbine-schema.xml file distributed with Turbine.
</p>
<p>
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!
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
</section>
<section name="Modifications to project-schema.xml">
<p>
Here is the sample project-schema.xml file before making modifications
to reference TURBINE_USER.
</p>
<source>
<![CDATA[
<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>
]]>
</source>
<p>
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.
</p>
<source>
<![CDATA[
<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>
]]>
</source>
<p>
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"!!!
</p>
<p>
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.
</p>
</section>
<section name="Deciding how additional data in Turbine User will be stored">
<p>
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.
</p>
<p>
Although storing the additional data in the TURBINE_USER.OBJECTDATA
column is the simplest method, it has a few drawbacks.
<ol>
<li>
You will not have easy access to the data through SQL.
</li>
<li>
You will not be able to specify criteria to select TurbineUser
objects from the database by filtering on data stored in
TURBINE_USER.OBJECTDATA.
</li>
<li>
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.
</li>
</ol>
</p>
<p>
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
<code>TURBINE_USER.LAST_NAME</code>. Instead the key used is
<code>LAST_NAME</code>. See
<code>org.apache.turbine.util.db.map.TurbineMapBuilder</code> for a
complete list of the keys used for the default columns.
</p>
<p>
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
<strong>VERY</strong> good reason not to do so.
</p>
</section>
<section name="Implementing adapter and map builder classes">
<p>
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.
</p>
<source>
<![CDATA[
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);
}
}
]]>
</source>
<p>
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.
</p>
<source>
<![CDATA[
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;
}
}
]]>
</source>
<p>
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.
</p>
<p>
Next comes TurbineUserPeerAdapter to which we add details of the new
database columns.
</p>
<source>
<![CDATA[
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;
}
}]]>
</source>
</section>
<section name="Generating the OM layer">
<p>
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.
</p>
<p>
<b>Warning: Do not run the init ant task after doing this.</b>
That task will drop and recreate your database. Without the
turbine-schema.xml file present, the TURBINE_* tables will not
be recreated!
</p>
<p>
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 <code>-schema.xml</code>
and generate the appropriate classes from that definition. Renaming the
turbine-schema.xml file stops these extra classes from being generated.
</p>
<p>
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.
</p>
<p>
We can now use project-om ant task to generate our OM layer using
the adapter classes we defined above.
</p>
<p>
With any luck everything will compile okay and we are only a small step
away from being able to use the new OM layer.
</p>
</section>
<section name="Modifications to TurbineResources.properties">
<p>
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:
</p>
<source>
<![CDATA[
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
]]>
</source>
<p>
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:
</p>
<source>
<![CDATA[
ExtendedUser user = (ExtendedUser) data.getUser();
]]>
</source>
<p>
Enjoy.
</p>
</section>
<section name="Additional Information">
<p>
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.
</p>
<p>
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:
</p>
<source>
<![CDATA[
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();
]]>
</source>
<p>
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:
</p>
<source>
<![CDATA[
o.save(dbConn);
]]>
</source>
<p>
which can be used when you have other data you want to commit in a
transaction you are managing yourself.
</p>
<p>
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:
</p>
<source>
<![CDATA[
Book book = new Book();
data.getParameters().setProperties(book);
ExtendedUser user = (ExtendedUser) data.getUser();
user.addBook(book);
user.save(); // !!!! book is not saved !!!!
]]>
</source>
<p>
Of course replacing the last two lines with:
</p>
<source>
<![CDATA[
book.setReviewedByUserId(user.getUserId());
book.save();
]]>
</source>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
If you want Torque to generate to code for you, you will have to modify the
Object.vm template that Torque uses.
</p>
<p>
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):
</p>
<source>
<![CDATA[
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)
]]>
</source>
<p>
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.
</p>
<p>
Notice how although you can go <code>user.save(dbCon);</code> the
TurbineUser record will not be part of the transaction.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
Note: The last solution mentioned should be ready for Turbine 2.3. This
should make life easier for everyone!!!
</p>
</section>
</body>
</document>