This document contains important information about item
classes for the core Base developer. Item classes are classes that
handles the business logic for the data classes in the
net.sf.basedb.core.data
package. In general there is one
item class for each data class.
When extending the database and creating new classes it is important that it follows the design of the already existing code. It is not very difficult or complicated.
Contents
See also
CommonItem
:s
To simplify the development of items, we have created a set of abstract classes and interfaces. A real class for an item must inherit from one of those classes and may implement any of the interfaces if needed. The strucure is similar to the structure found in the data package.
Each item class must be prepared to handle the access permissions for the logged in user. The base classes will do most of the required work, but not everything. There are four cases which the item class must be aware of:
initPermissions()
method
set
method.
getQuery()
method returns only items with at least
read permission
The permissions for an item are initialised by a call to the initPermissions()
method. This method is called as soon as the item becomes attached to a
DbControl
object, which is responsible for managing items in the database.
This method shuld be overridden by subclasses that needs to grant
or deny permissions that is not granted or denied by default.
When overriding the initPermissions()
method it is important
to:
Permission.grant()
and Permission.deny()
methods
to do this.
super.initPermissions()
method. Otherwise,
no permissions will be set all, resulting in an PermissionDeniedException
almost immediately.
Here is an example from the OwnedItem
class. If the currently logged
in user is the same as the owner of the item, DELETE
, SET_OWNER
and SET_PERMISSION
permissions are granted. Remember that delete permission
also implies read, use and write permissions.
// OwnedItem.java void initPermissions(int granted, int denied) { UserData owner = getData().getOwner(); // owner may be null for new items if (owner != null && owner.getId() == getSessionControl().getLoggedInUserId()) { granted |= Permission.grant(Permission.DELETE, Permission.SET_OWNER, Permission.SET_PERMISSION); } super.initPermissions(granted, denied); }
Here is another example for News
items, which grants read permission to anyone
(even if not logged in) if today is between the start and end date:
// News.java void initPermissions(int granted, int denied) throws BaseException { Date today = new Date(); Date startDate = getData().getStartDate(); Date endDate = getData().getEndDate(); if (today.after(startDate) && (endDate == null || today.before(endDate))) { granted |= Permission.grant(Permission.READ); } super.initPermissions(granted, denied); }
A third example from the Help
class which is a child item
to Client
. Normally you will get read permission on all child
items if you have read permission on the parent item, and create, write and
delete permissions if you have write permission on the parent item.
In this case you don't have to override the initPermissions
method
IF the child class extends the ChildItem
class. Instead,
it should implement the getSharedParent
method. The
ChildItem.initPermissions
will take care of checking the permissions
on the parent instead of on the child. Note that this only works if the parent
itself hasn't overridden the initPermissions
method, since that
method is never called in this case.
// Help.java public class Help extends ChildItemimplements Nameable ... SharedData getSharedParent() { return getData().getClient(); }
initPermissions()
method needs to be overridden.
DELETE
, SET_OWNER
and
SET_PERMISSION
permissions. Delete permission
also implies read, use and write permissions. Subclasses to this class
usually doesn't have to overide the initPermission()
method.
The only item so far is Project
:s.
ItemKey
and/or ProjectKey
.
Subclasses to this class usually doesn't have to overide the
initPermission()
method.
An item class is required to check for WRITE
permissions in each
method that modifies it from a public method. Example:
public void setName(String name) throws PermissionDeniedException { checkPermission(Permission.WRITE); // ... rest of code }
WRITE
permissions are not
checked in any other central place in the core code.
Place the permission check on the first line in the method,
before any data validation. This will make it easier to spot places
where the permission check is forgotten.
An item class is required to check for USE
permission
on associated objects in each method that modifies the association
from a public method. Example from the Procotol
class:
// Protocol.java public void setProtocolType(ProtocolType protocolType) throws PermissionDeniedException, InvalidUseOfNullException { checkPermission(Permission.WRITE); if (protocolType == null) throw new InvalidUseOfNullException("protocolType"); protocolType.checkPermission(Permission.USE); getData().setProtocolType(protocolType.getData()); }
USE
permissions are not
checked in any other central place in the core code.
Place the permission check as early in the method as possible after it has
been validated that the value isn't null.
getQuery()
method only returns items with read
permission
This method can one of the most complex ones of the entire class.
The query it generates must always be compatible with the
initPermissions()
method. Ie. it must not return any items for
which the initPermissions()
method doesn't grant read permission.
And the other way around, if the initPermission()
method
grants read permission to and item, the query should be able to return it.
The simplest case is if you doesn't override the
initPermissions()
method in such a way that it affects read
permissions. In this case you can just create a query and return it as
it is. The query implementation will take care of the rest.
// Client.java public static ItemQuery<Client> getQuery() { return new ItemQuery<Client>(Client.class); }
A common case is when an item is the child of another item.
Usually the parent is a Shareable
item which means that
we optimally should check the item and project keys on the parent when returning
the children. But, this is a rather complex operation, so in this case we have
choosen a different approach. The getQuery()
method of child items
must take a parameter of the parent type. The query can the safely return all
children of that parent, since having a reference to the parent item, means that
read permission is granted. A null value for the parent is allowed, but then
we fall back to check for role permissions only (with the help of a
ChildFilter
object).
// Help.java private static final QueryRuntimeFilter RUNTIME_FILTER = new QueryRuntimeFilterFactory.ChildFilter(Item.HELP, Item.CLIENT); ... other code public static ItemQuery<Help> getQuery(Client client) { ItemQuery<Help> query = null; if (client != null) { query = new ItemQuery<Help>(Help.class, null); query.restrictPermanent( Restrictions.eq( Hql.property("client"), Hql.entity(client) ) ); } else { query = new ItemQuery<Help>(Help.class, RUNTIME_FILTER); } return query; }
There are many other variants of the getQuery()
method, for
example all items having to with the authentication, User
,
Group
, Role
, etc. must check the logged in user's
membership. We don't show any more examples here. Take a look in the source
code if you want more information. You can also read the
Query API documentation for
more examples.
An item class must validate all data that is passed to it as parameters. There are three types of validation:
For each of these types of validation we have choosen a strategy that is as simple as possible and doesn't force us to complex requirements on the code for objects. First, we may note that case 1 is very common, case 2 is very uncommon, and case 3 is just a bit more common than case 2.
For case 1 we choose to make the validation in the set method for each property. Example:
void setName(String name)
throws InvalidDataException
{
checkPermission(Permission.WRITE);
if (name == null) throw new InvalidUseOfNullException("name");
if (name.length > MAX_NAME_LENGTH)
{
throw new StringTooLongException("name", name, MAX_NAME_LENGTH);
}
getData().setName(name);
}
// Note! In this case we should actually use NameableUtil
instead
This will take care of all case 1 validation except that we cannot check properties that doesn't allow null values if the method never is called. To solve this problem we have two strategies:
This case requires interception of saves and updates and a call to the
validate()
method on the item. This automatically done on items
which implements the Validatable
interface at commit time.
Internally this functionality is implemented by the DbControl
object, which keeps a "commit queue" which holds all loaded items that
implements the Validatable
interface. When commit()
is called, the queue is iterated and the validate()
method is called for
each item.
Here is an example from the News
class which must validate
that the three dates are in proper order:
// News.java void validate() throws InvalidDataException, BaseException { super.validate(); Date startDate = getData().getStartDate(); Date newsDate = getData().getNewsDate(); Date endDate = getData().getEndDate(); assert startDate != null : "startDate == null"; assert newsDate != null : "newsDate == null"; if (startDate.after(newsDate)) { throw new InvalidDataException("Invalid date. startDate is after newsDate."); } if (endDate != null && newsDate.after(endDate)) { throw new InvalidDataException("Invalid date. newsDate is after endDate."); } }
We do not bother with checking for this case, but delegates to the database to do the check. However, SQL exceptions for unique constraints violations are caught by the core and be rethrown as BASE exceptions (ItemAlreadyExistsException).
The reason that we do not bother to check for this case is that we can't be sure to succeed even if we first check the database. It is possible that during the time between our check and the actual insert or update, another transaction has already inserted another object into the database that violates the check.
This is not perfect and the error messages are a bit ugly, but under the circumstances it is the best we can do.
Sometimes it is neccessary for an item to intercept certain events.
For example, the File
object needs to know if a transaction
has been completed or rollbacked so it can clean up temporary files that
have been used.
We have created the Transactional
interface, which is a
tagging interface that tells the core to call certain methods on the
item at certain events. The interface doesn't contain any methods,
the item class needs to override methods from the BasicItem
class.
The following events/methods have been defined:
File
object may need to cleanup temporary files.
This method should not use the database and it must not fail, since it is
impossible to rollback anything that has already been written to the database.
If the method fails, it should log an exception with the Application.log
method.
onAfterCommit()
method applies to this method.
Internally this functionality is implemented by the DbControl
object, which keeps a "commit queue" which holds all loaded objects that
implements the Transactional
interface. When commit()
is called, the queue is iterated and onBeforeCommit()
is called for
each item, and then either onAfterCommit()
or onRollback()
.
The Action
parameter is of an enumeration type which can hae three
different values:
CREATE
: This is a new item which is saved to the database for
the first time.
UPDATE
: This is an existing item, which has been modified.
DELETE
: This is an existing item, which is now beeing deleted
from the database
The AnyItem.txt
and
AChildItem.txt
files contains two complete
item classes with lots of template methods. Please copy and paste as much
as you want from these, but do not forget to change everything.
An item class should extend one of the four classes: BasicItem
,
OwnedItem
, SharedItem
and CommonItem
.
Which one depends on what combination of interfaces are needed for that item.
The most common situation is probably to extend the CommonItem
class.
/* $Id: index.html 4477 2008-09-05 15:15:25Z jari $ Copyright (C) Authors contributing to this file. This file is part of BASE - BioArray Software Environment. Available at http://base.thep.lu.se/ BASE is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. BASE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package net.sf.basedb.core; import net.sf.basedb.core.data.AnyData; /** This class is used to represent an AnyItem in BASE. @author Your name @version 2.0 @see AnyData @base.modified $Date: 2008-09-05 17:15:25 +0200 (Fri, 05 Sep 2008) $ */ public class AnyItem extends CommonItem<AnyData> { // Rest of class code... }
The corresponding dataclass for the item is specified as a generics parameter of the superclass.
DbControl.newItem()
.
/** Create a newThe method must initialise all not-null properties to a sensible default values or it may take values as parameters:AnyItem
item. @param dc TheDbControl
which will be used for permission checking and database access @return The newAnyItem
item @throws BaseException If there is an error */ public static AnyItem getNew(DbControl dc) throws BaseException { AnyItem a = dc.newItem(AnyItem.class); a.setName("New any item"); return a; }
// User.java public static User getNew(DbControl dc, String login, String password) throws BaseException { User u = dc.newItem(User.class); u.setName("New user"); u.setLogin(login); u.setPassword(password); int defaultQuotaId = SystemItems.getId(Quota.DEFAULT); org.hibernate.Session session = dc.getHibernateSession(); QuotaData default = HibernateUtil.loadData(session, QuotaData.class, defaultQuotaId); u.getData().setQuota(default); return u; }When the default value is an association to another item, use the data object (
QuotaData
) not the item object (Quota
)
to create the association.
The reason for this is that the logged in user may not have read permission to
the default object. Ie. The logged in user may have permission to create users,
but not permission to read quota.
DbControl.loadItem()
method
to load the item. If the item is not found an
ItemNotFoundException
should be thrown.
/** Get anAnyItem
item when you know the id. @param dc TheDbControl
which will be used for permission checking and database access. @param id The id of the item to load @return TheAnyItem
item @throws ItemNotFoundException If an item with the specified id is not found @throws PermissionDeniedException If the logged in user doesn't have {@link Permission#READ} permission to the item @throws BaseException If there is another error */ public static AnyItem getById(DbControl dc, int id) throws ItemNotFoundException, PermissionDeniedException, BaseException { AnyItem a = dc.loadItem(AnyItem.class, id); if (a == null) throw new ItemNotFoundException("AnyItem[id="+id+"]"); return a; }
Each item class needs only one constructor, which takes an object of
the corresponding data class as a parameter. The constructor should never be
invoked directly. Use the DbControl.newItem()
method.
AnyItem(AnyData anyData) { super(anyData); }
BasicItem
class. The default
implementation only checks AnyToAny links between items. A subclass must
override this method if it can be considered to be used by other items
by regular links. A subclass should always call super.isUsed()
as a last check if it is not used by any other item. The method should check
if it is beeing used (referenced by) some other item. For example, a protocol type
is used if there is a protocol of that type. The simplest way to check if
the item is used is to use a predefined query that counts the number
of references.
// SoftwareType.java /** Check if: <ul> <li>there are any {@link Software} using this type </ul> */ public boolean isUsed() throws BaseException { org.hibernate.Session session = getDbControl().getHibernateSession(); org.hibernate.Query query = HibernateUtil.getPredefinedQuery(session, "GET_SOFTWARE_FOR_SOFTWARETYPE", "count(*)"); /* SELECT {1} FROM SoftwareData sw WHERE sw.softwareType = :softwaretype */ query.setEntity("softwaretype", this.getData()); return HibernateUtil.loadData(Integer.class, query) > 0 || super.isUsed(); }Sometimes it may be harder to decide what counts as using an item or not. Some examples:
BasicItem
class which provides a default implementation that
loads all item linked with an AnyToAny
link which has
the usingTo
flag set to true. A subclass must
override this method if it can be considered to be used by other items
by regular links. A subclass should always call super.getUsingItems()
first and then add extra items to the Set
returned by the
call to the superclass. For example, a protocol type
should load all protocols of that type.
// SoftwareType.java /** Get all: <ul> <li>{@link Software} of this type <ul> @since 2.2 */ @Override public Set<ItemProxy> getUsingItems() { Set<ItemProxy> using = super.getUsingItems(); org.hibernate.Session session = getDbControl().getHibernateSession(); // Protocols org.hibernate.Query query = HibernateUtil.getPredefinedQuery(session, "GET_SOFTWARE_FOR_SOFTWARETYPE", "sw.id"); /* SELECT {1} FROM SoftwareData sw WHERE sw.softwareType = :softwaretype */ query.setEntity("softwaretype", this.getData()); addUsingItems(using, Item.SOFTWARE, query); return using; }
Validatable
items when
DbControl.commit()
is called. See section 3
Transactional
items
(see section 4)
and also on regular items if the action is CREATE
or
DELETE
when DbControl.commit()
is called. If you override
this method you must call super.onBeforeCommit()
to allow the superclass
to do whatever it needs to do. Here is an example from the OwnedItem
class
which sets the owner to the currently logged in user, if none has been specified:
void onBeforeCommit(Transactional.Action action) throws NotLoggedInException, BaseException { super.onBeforeCommit(action); if (action == Transactional.Action.CREATE && getData().getOwner() == null) { org.hibernate.Session session = getDbControl().getHibernateSession(); int loggedInuserId = getSessionControl().getLoggedInUserId(); UserData owner = HibernateUtil.loadData(session, UserData.class, loggedInuserId); if (owner == null) throw new NotLoggedInException(); getData().setOwner(owner); } }
onBeforeCommit()
method beacuse an id is not assigned to the item
until after the insert (since we let the database generate the id). Currently
there are no items using this method and we recommend using onBeforeCommit()
if possible.
Transactional
items
(see section 4) after a successful commit()
.
It is not allowed to use the database in this method.
Transactional
items
(see section 4) after a failed commit()
.
It is not allowed to use the database in this method.
The get methods for basic property types are usually very simple. All that is needed is to return the value. Be aware of date values though, they are mutable and must be copied.
/** Get the value of the string property. */ public String getStringProperty() { return getData().getStringProperty(); } /** Get the value of the int property. */ public int getIntProperty() { return getData().getIntProperty(); } /** Get the value of the boolean property. */ public boolean isBooleanProperty() { return getData().isBooleanProperty(); } /** Get the value of the date property. @return A date object or null if unknown */ public Date getDateProperty() { return DateUtil.copy(getData().getDateProperty()); }
The set methods must always check for write permission (see section 2.3) and validate the parameters (see section 3.1.
/** The maximum length of the string property. Check the length agains this value before calling {@link #setStringProperty(String)} to avoid exceptions. */ public static final int MAX_STRINGPROPERTY_LENGTH = AnyData.MAX_STRINGPROPERTY_LENGTH; /** Set the value of the string property. Null values are not allowed and the length must be shorter than {@link #MAX_STRINGPROPERTY_LENGTH}. @param value The new value @throws PermissionDeniedException If the logged in user doesn't have write permission @throws InvalidDataException If the value is null or too long */ public void setStringProperty(String value) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); getData.setStringProperty( StringUtil.setNotNullString(value, "stringProperty", MAX_STRINGPROPERTY_LENGTH) ); } /** Set the value of the int property. The value mustn't be less than zero. @param value The new value @throws PermissionDeniedException If the logged in user doesn't have write permission @throws InvalidDataException If the value is less than zero */ public void setIntProperty(int value) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); getData.setIntProperty( IntegerUtil.checkMin(value, "intProperty", 0) ); } /** Set the value of the boolean property. @param value The new value @throws PermissionDeniedException If the logged in user doesn't have write permission */ public void setBooleanProperty(boolean value) throws PermissionDeniedException { checkPermission(Permission.WRITE); getData.setBooleanProperty(value); } /** Set the value of the date property. Null values are allowed. @param value The new value @throws PermissionDeniedException If the logged in user doesn't have write permission */ public void setDateProperty(Date value) throws PermissionDeniedException { checkPermission(Permission.WRITE); getData().setDateProperty(DateUtil.setNullableDate(value, "dateProperty")); }
Many-to-one associations require sligthly more work. First of all,
the item must be connected to a DbControl
since
it is used to load the information from the database and
crete the new item object. Secondly, we must make sure to check
for use permission on the associated object in the set method
(see section 2.4).
/**
Get the associated other item.
@return The OtherItem
item
@throws PermissionDeniedException If the logged in user
doesn't have read permission
@throws BaseException If there is another error
*/
public OtherItem getOtherItem()
throws PermissionDeniedException, BaseException
{
return getDbControl().getItem(OtherItem.class, getData().getOtherItem());
}
/**
Set the associated item. Null is not allowed.
@param other The other item
@throws PermissionDeniedException If the logged in user
doesn't have write permission
@throws InvalidDataException If the other item is null
@throws BaseException If there is another error
*/
public void setOtherItem(OtherItem other)
throws PermissionDeniedException, InvalidDataException, BaseException
{
checkPermission(Permission.WRITE);
if (otherItem == null) throw new InvalidUseOfNullException("otherItem");
getData().setOtherItem(otherItem.getData());
}
If the association is a one-to-many or many-to-many it becomes
a little more complicated again. There are many types of such
associations and how they are handled usually depends on if the
are set:s, map:s, list:s or any other type of collections.
In all cases we need methods for adding and removing items, and
a method that returns a Query
that can list all
associated items.
The first example if for parent/child relationship, which is a one-to-many association where the children are mapped as a set.
/**
Create a child item for this any item.
@return The new AChildItem
object
@throws PermissionDeniedException If the logged in user doesn't have
write permission
@throws BaseException If there is another error
*/
public AChildItem newChildItem()
throws PermissionDeniedException, BaseException
{
checkPermission(Permission.WRITE);
return AChildItem.getNew(getDbControl(), this);
}
/**
Get a query that will return all child items for this any item.
@return A {@link Query} object
*/
public ItemQuery<AChildItem> getChildItems()
{
return AChildItem.getQuery(this);
}
The second example is for the many-to-many associations between users and roles, which is also mapped as a set.
// Role.java /** Add a user to this role. @param user The user to add @throws PermissionDeniedException If the logged in user doesn't have write permission for the role and use permission for the user @throws InvalidDataException If the user is null */ public void addUser(User user) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); if (user == null) throw new InvalidUseOfNullException("user"); user.checkPermission(Permission.USE); getData().getUsers().add(user.getData()); } /** Remove a user from this role. @param user The user to remove @throws PermissionDeniedException If the logged in user doesn't have write permission for the role and use permission for the user @throws InvalidDataException If the user is null */ public void removeUser(User user) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); if (user == null) throw new InvalidUseOfNullException("user"); user.checkPermission(Permission.USE); getData().getUsers().remove(user.getData()); } /** Check if the given user is member of this role or not. @param user The user to check @return TRUE if the user is member, FALSE otherwise */ public boolean isMember(User user) { return getData().getUsers().contains(user.getData()); } /** Get a query that returns the users that are members of this role. This query excludes users that the logged in user doesn't have permission to read. @see User#getQuery() */ public ItemQuery<User> getUsers() { ItemQuery<User> query = User.getQuery(); query.joinPermanent( Hql.innerJoin("roles", Item.ROLE.getAlias()) ); query.restrictPermanent( Restrictions.eq( Hql.alias(Item.ROLE.getAlias()), Hql.entity(this) ) ); return query; } // User.java /** Get a query that returns the roles where this user is a member. The query excludes roles that the logged in user doesn't have permission to read. @see Role#getQuery() */ public ItemQuery<Role> getRoles() { ItemQuery<Role> query = Role.getQuery(); query.joinPermanent( Hql.innerJoin("users", Item.USER.getAlias()) ); query.restrictPermanent( Restrictions.eq( Hql.alias(Item.USER.getAlias()), Hql.entity(this) ) ); return query; }
Note that we have a query method in both classes, but the association
can only be changed from the Role
. We recommend that
change methods are put in one of the classes only.
The last example is the many-to-many relation between projects and users which is a map to the permission for the user in the project.
// Project.java /** Grant a user permissions to this project. Use an empty set or null to remove the user from this project. @param user The user @param permissions The permissions to grant, or null to revoke all permissions @throws PermissionDeniedException If the logged in user doesn't have write permission for the project @throws InvalidDataException If the user is null @see Permission */ public void setPermissions(User user, Set<Permission> permissions) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); if (user == null) throw new InvalidUseOfNullException("user"); if (permissions == null || permissions.isEmpty()) { getData().getUsers().remove(user.getData()); } else { getData().getUsers().put(user.getData(), Permission.grant(permissions)); } } /** Get the permissions for a user in this project. @param user The user for which we want to get the permission @return A set containing the granted permissions, or an empty set if no permissions have been granted @throws InvalidDataException If the user is null @see Permission */ public Set<Permission> getPermissions(User user) throws InvalidDataException { if (user == null) throw new InvalidUseOfNullException("user"); return Permission.fromInt(getData().getUsers().get(user.getData())); } /** Get a query that returns the users that are members of this project. This query excludes users that the logged in user doesn't have permission to read. @see User#getQuery() */ public ItemQuery<User> getUsers() { ItemQuery<User> query = User.getQuery(); query.joinPermanent( Hql.innerJoin("projects", Item.PROJECT.getAlias()) ); query.restrictPermanent( Restrictions.eq( Hql.alias(Item.PROJECT.getAlias()), Hql.entity(this) ) ); return query; } // User.java /** Get a query that returns the projects where this user is a member. The query excludes projects that the logged in user doesn't have permission to read. The query doesn't include projects where this user is the owner. @see Project#getQuery() */ public ItemQuery<Project> getProjects() { ItemQuery<Project> query = Project.getQuery(); query.joinPermanent( Hql.innerJoin("users", Item.USER.getAlias()) ); query.restrictPermanent( Restrictions.eq( Hql.index(Item.USER.getAlias(), null), Hql.entity(this) ) ); return query; }
As you can see from these examples, the code is very different depending on the type of association. We don't give any more examples here, but if you are unsure you should look in the source code to get more information.