This document contains important information about item classes for the 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.
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 net.sf.basedb.core.data
package
(See Section 29.2, “The Data Layer API”).
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:
Initialise permissions in the initPermissions()
method.
Check for write permission in settter methods.
Check for use permission when creating associations to other items.
Make sure the 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. The initPermissions()
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:
Combine the additional permissions with those that was passed as parameters.
Use the binary OR operator ( | ) with the result from the Permission.grant()
and Permission.deny()
methods to do this.
Call super.initPermissions()
. 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 of the news entry:
// News.java void initPermissions(int granted, int denied) throws BaseException { long today = new Date().getTime(); long startDate = getData().getStartDate().getTime(); long endDate = getData().getEndDate() == null ? 0 : getData().getEndDate().getTime()+24*3600*1000; if (startDate <= today && (endDate == 0 || today <= 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 ChildItem implements Nameable ... SharedData getSharedParent() { return getData().getClient(); }
BasicItem
This class will grant or deny permissions as the are defined by the roles the logged
in user is a member of. If a subclass extend directly from this class, it is common
that the initPermissions()
method needs to be overridden.
ChildItem
This class grant READ
permission if the logged in user has
READ
permission on the parent item, and CREATE
,
WRITE
and DELETE
permission if the logged in user has WRITE
(configurable)
permission on the parent item.
OwnedItem
The owner of an item gets 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
initPermissions()
method.
SharedItem
The logged in user get permissions as specified in the associated ItemKey
and/or ProjectmKey
. Subclasses to this class usually doesn't
have to overide the initPermissions()
method.
An item class is required to check for WRITE
permission in each
method that modifies the state from a public method. Example:
public void setName(String name) throws PermissionDeniedException { checkPermission(Permission.WRITE); // ... rest of code }
Warning | |
---|---|
If you forget this, an unauthorised user may be able to change the properties of an item.
|
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
Protocol
class:
public void setFile(File file) throws PermissionDeniedException { checkPermission(Permission.WRITE); if (file != null) file.checkPermission(Permission.USE); getData().setFile(file == null ? null : file.getData()); }
Warning | |
---|---|
If you forget this, an unauthorised user may be able to change the association of an item.
|
This method can be 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 initPermissions()
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); 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 Section 29.4, “The Query API” for more examples.
An item class must validate all data that is passed to it as parameters. There are three types of validation:
Validation of properties that are independent of other properties. For example, the length of a string or the value of number.
Validation of properties that depends on other properties on the same object. For example, we have properties for the row and column counts, and then an array of linked objects for each position.
Validation of properties that depends on the values of other objects. For example, the login of a user must be unique among all users.
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:
public void setName(String name) throws InvalidDataException { checkPermission(Permission.WRITE); // Null is not allowed if (name == null) throw new InvalidUseOfNullException("name"); // The name must not be too long 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:
Provide a default value that is set in the constructor. For example the name of a new user can be initilised to "New user".
Use constructor methods with parameters for required objects.
Which strategy to use is decided from case to case. Failure to validate a property will usually
result in a database exception, so no real harm is done, except that we don't want to show the
ugly error messages to our users. The News
class uses a mix of the two strategies:
// News.java public static News getNew(DbControl dc, Date startDate, Date newsDate) { News n = dc.newItem(News.class); n.setName("New news"); n.setStartDate(startDate); n.setNewsDate(newsDate); n.getData().setEntryDate(new Date()); return n; } ... public void setStartDate(Date startDate) throws PermissionDeniedException, InvalidDataException { checkPermission(Permission.WRITE); getData().setStartDate(DateUtil.setNotNullDate(startDate, "startDate")); } ...
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. Internally this functionality is
implemented by the DbControl
class, which keeps a
"commit queue" that holds all loaded items that implements the Validatable
interface. When DbControl.commit()
is called, the queue is iterated and the validate()
method is called for each item.
Here is another example from the News
class which must
validate that the three dates (startDate, newsData and endData) 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(); 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."); } }
Usually, we do not bother with checking for this case, but delegates to the database to do the check. 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:
This method is called before a commit is issued to Hibernate. It should be used by an
item when it needs to update dependent objects before anything is written to the database.
Note that nothing has been sent to the database yet and new items has not got an id when
this method 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 no owner has been explicitely 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); } }
This method is called before inserting new items into the database
to allow items to propagate default values from the active project.
The method is only called when a project is active. Subclasses should
always call super.setProjectDefaults()
and should only set default values that hasn't been explicitely set
by client code (including setFoo(null)
calls).
// DerivedBioAssay.java @Override @SubtypableRelatedItems({Item.PHYSICALBIOASSAY, Item.DERIVEDBIOASSAYSET, Item.SOFTWARE, Item.HARDWARE, Item.PROTOCOL}) public ItemSubtype getItemSubtype() { return getDbControl().getItem(ItemSubtype.class, getData().getItemSubtype()); } /** Set protocol, hardware and software from project default settings. */ @Override void setProjectDefaults(Project activeProject) throws BaseException { super.setProjectDefaults(activeProject); if (!hasPermission(Permission.WRITE)) return; DbControl dc = getDbControl(); if (!protocolHasBeenSet) { ProtocolData protocol = (ProtocolData)activeProject.findDefaultRelatedData(dc, this, Item.PROTOCOL, false); if (protocol != null) { getData().setProtocol(protocol); protocolHasBeenSet = true; } } if (!hardwareHasBeenSet) { HardwareData hardware = (HardwareData)activeProject.findDefaultRelatedData(dc, this, Item.HARDWARE, false); if (hardware != null) { getData().setHardware(hardware); hardwareHasBeenSet = true; } } if (!softwareHasBeenSet) { SoftwareData software = (SoftwareData)activeProject.findDefaultRelatedData(dc, this, Item.SOFTWARE, false); if (software != null) { getData().setSoftware(software); softwareHasBeenSet = true; } } }
This method is called on all items directly after Hibernate has inserted
it into the database. This method can be used in place of the
onBeforeCommit()
in case the id is needed.
This method is called after a successful commit has been issued to Hibernate. It should be
used by an item which needs to do additional processing. For example the
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 committed to the database. If the method fails, it should log
an exception with the Application.log()
method.
This method is called after an unsuccessful commit has been issued to Hibernate. The same rules as
for the onAfterCommit()
method applies to this method.
Internally this functionality is implemented by the DbControl
class, which keeps a "commit queue" that holds all new objects, all objects that are about to be deleted
and all objects that implements the Transactional
interface. When DbControl.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 being deleted from the database
The AnyItem.java and AChildItem.java 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 the specific details.
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. Do not forget to include the GNU licence and
copyright statement. Also note that the corresponding data layer class is specified as a
generics parameter of the superclass.
/* $Id $ Copyright (C) 2011 Your name This file is part of BASE - BioArray Software Environment. Available at https://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 BASE. If not, see <http://www.gnu.org/licenses/>. */ 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 @since 3.0 @see AnyData @base.modified $Date$ */ public class AnyItem extends CommonItem<AnyData> { ... }
This method is used to create a new item. The new item must be created by calling the
DbControl.newItem()
.
/** Create a new <code>AnyItem</code> item. @param dc The <code>DbControl</code> which will be used for permission checking and database access @return The new <code>AnyItem</code> 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; }
The method must initialise all not-null properties to a sensible default values or it may take values as parameters:
// 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 defaultQuota = HibernateUtil.loadData(session, QuotaData.class, defaultQuotaId); u.getData().setQuota(defaultQuota); 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.
This method is used to load an item from the database when the id of that item is known.
Use the DbControl.loadItem()
method to load the item. If the item
is not found an ItemNotFoundException
should
be thrown.
/** Get an <code>AnyItem</code> item when you know the id. @param dc The <code>DbControl</code> which will be used for permission checking and database access. @param id The id of the item to load @return The <code>AnyItem</code> item @throws ItemNotFoundException If an item with the specified id is not found @throws PermissionDeniedException If the logged in user doesn't have 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; }
See Section , “Making sure the getQuery() method only returns items with read permission”.
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); }
This method is defined by the BasicItem
class and
is called whenever we need to know if there are other items referencing the current item. The main
use case is to let client applications know if it is safe to delete an object or not.
The default implementation checks AnyToAny
links between items. A subclass must override this method if it can be referenced by other items.
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 Tag
is used if there is an
Extract
with that tag. The simplest way to check
if the item is used is to use a predefined query that counts the number of references.
/** Check if: <ul> <li>Some {@link Extract}:s are marked with this tag </ul> */ public boolean isUsed() throws BaseException { org.hibernate.Session session = getDbControl().getHibernateSession(); org.hibernate.Query query = HibernateUtil.getPredefinedQuery(session, "GET_EXTRACTS_FOR_TAG", "count(*)"); /* SELECT {1} FROM ExtractData ext WHERE ext.tag = :tag */ query.setEntity("tag", this.getData()); boolean used = HibernateUtil.loadData(Long.class, query) > 0; return used || super.isUsed(); }
Sometimes it may be harder to decide what counts as using an item or not. Some examples:
An event for a sample does not count as using the sample, since they hava a parent-child relationship. Ie. deleting the sample will also delete all events associated with it. On the other hand, the protocol registered for the event counts as using the protocol, because deleting the protocol should not delete all events.
As a general rule, if one item is used by a second item, then the second item cannot be used by the first. It could lead to situations where it would be impossible to delete either one of them.
Find all items that are referencing this one. This method is related to the
isUsed()
method and is defined in the BasicItem
class. The default implementation load all items linked via an AnyToAny
link that has the usingTo
flag set to true. A subclass must override this method if it can
be referenced to be used by other items. A subclass should always call super.getUsingItems()
first and then add extra items to the Set returned by that call. For example, a Tag
should load all Extract
:s with that tag.
/** Get all: <ul> <li>{@link Extract}:s marked with this tag <ul> */ @Override public Set<ItemProxy> getUsingItems() { Set<ItemProxy> using = super.getUsingItems(); org.hibernate.Session session = getDbControl().getHibernateSession(); // Extracts org.hibernate.Query query = HibernateUtil.getPredefinedQuery(session, "GET_EXTRACTS_FOR_TAG", "ext.id"); /* SELECT {1} FROM ExtractData ext WHERE ext.tag = :tag */ query.setEntity("tag", this.getData()); addUsingItems(using, Item.EXTRACT, query); return using; }
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 and validate
the parameters. There are plenty of utility method to help with this.
/** 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 referenced object in the setter method.
/** 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 modifier 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 inspiration.