| <?xml version="1.0"?> |
| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you 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/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. Instead 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 "primitive". You do not have to |
| use "object"!!! |
| </p> |
| <p> |
| Turbine Version 2.1 only: In this version of Turbine, the only option was to use the |
| primitive 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> |