31.3.5. Item-class rules

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.

Basic class and interface hierarchy

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”).

Figure 31.1. Basic class and interface hierarchy

Basic class and interface hierarchy

Access permissions

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.

Initialise permissions

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();
}
Permissions granted by the base classes
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.

Checking for write permission in setter methods

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] Warning

If you forget this, an unauthorised user may be able to change the properties of an item. 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.

Checking for use permission when creating associations

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] Warning

If you forget this, an unauthorised user may be able to change the association of an item. 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.

Making sure the getQuery() method only returns items with read permission

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.

Data validation

An item class must validate all data that is passed to it as parameters. There are three types of validation:

  1. Validation of properties that are independent of other properties. For example, the length of a string or the value of number.

  2. 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.

  3. 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.

Case 1 validation

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"));
}
...
Case 2 validation

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.");
   }
}

Case 3 validation

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.

Participating in transactions

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:

[Note] Note

The methods are always called for new items and items that are about to be deleted. It is only neccessary for an item to implement the Transactional interface if it needs to act on UPDATE events.

onBeforeCommit(Action)

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);
   }
}

setProjectDefaults(Project)

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).

[Note] Note

With few exceptions a project can only hold ItemSubtype items as default values. This means that the item that is going to use the default value should implement the Subtypeable interface and list the other related item types in the @SubtypableRelatedItems annotation.


// 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;
		}
	}
}

onAfterInsert()

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.

onAfterCommit(Action)

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.

onRollback(Action)

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

Template code for item classes

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.

Class declaration

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>
{
  ...
}

Static methods and fields
getNew(DbControl)

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.

getById(DbControl, int)

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;
}

getQuery()

See Section , “Making sure the getQuery() method only returns items with read permission”.

Constructors

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);
}

Core methods
isUsed()

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.

getUsingItems()

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;
}

initPermissions(int, int)

See Section , “Initialise permissions”.

validate()

See the section called “Data validation”.

onBeforeCommit(Action), setProjectDefaults(Project), onAfterInsert(), onAfterCommit(Action) , onRollback(Action)

See the section called “Participating in transactions”.

Getter and setter methods

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

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());
}

One-to-many and many-to-many associations

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.