/* * Hibernate OGM, Domain model persistence for NoSQL datastores * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.ogm.model.spi; import static org.hibernate.ogm.model.spi.AssociationOperationType.PUT; import static org.hibernate.ogm.model.spi.AssociationOperationType.REMOVE; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.ogm.datastore.impl.EmptyAssociationSnapshot; import org.hibernate.ogm.dialect.spi.GridDialect; import org.hibernate.ogm.model.key.spi.RowKey; import org.hibernate.ogm.util.impl.CollectionHelper; import org.hibernate.ogm.util.impl.Contracts; import org.hibernate.ogm.util.impl.StringHelper; /** * Represents an association (think of it as a set of rows, each representing a specific link). * <p> * An association accepts a {@link AssociationSnapshot} which is a read-only state of the association when read from the * database or freshly created. * <p> * An association collects changes applied to it. These changes are represented by a list of * {@link AssociationOperation}. It is intended that {@link GridDialect}s retrieve these actions and apply them to the * datastore. The list of changes is computed against the snapshot. * <p> * Note that the {@link Tuple}s representing association rows always also contain the columns of their {@link RowKey}. * * @author Emmanuel Bernard <emmanuel@hibernate.org> * @author Gunnar Morling * @author Guillaume Smet */ public class Association { private final AssociationSnapshot snapshot; private final Map<RowKey, AssociationOperation> currentState = new LinkedHashMap<RowKey, AssociationOperation>(); private boolean cleared; /** * Creates a new association, based on an empty association snapshot. */ public Association() { this.snapshot = EmptyAssociationSnapshot.INSTANCE; } public Association(AssociationSnapshot snapshot) { this.snapshot = snapshot; } /** * Returns the association row with the given key. * * @param key the key of the row to return. * @return the association row with the given key or {@code null} if no row with that key is contained in this * association */ public Tuple get(RowKey key) { AssociationOperation result = currentState.get( key ); if ( result == null ) { return cleared ? null : snapshot.get( key ); } else if ( result.getType() == REMOVE ) { return null; } return result.getValue(); } /** * Adds the given row to this association, using the given row key. * The row must not be null, use the {@link org.hibernate.ogm.model.spi.Association#remove(org.hibernate.ogm.model.key.spi.RowKey)} * operation instead. * * @param key the key to store the row under * @param value the association row to store */ public void put(RowKey key, Tuple value) { // instead of setting it to null, core must use remove Contracts.assertNotNull( value, "association.put value" ); currentState.put( key, new AssociationOperation( key, value, PUT ) ); } /** * Removes the row with the specified key from this association. * * @param key the key of the association row to remove */ public void remove(RowKey key) { currentState.put( key, new AssociationOperation( key, null, REMOVE ) ); } /** * Return the list of actions on the tuple. Operations are inherently deduplicated, i.e. there will be at most one * operation for a specific row key. * <p> * Note that the global CLEAR operation is put at the top of the list. * * @return the operations to execute on the association, the global CLEAR operation is put at the top of the list */ public List<AssociationOperation> getOperations() { List<AssociationOperation> result = new ArrayList<AssociationOperation>( currentState.size() + 1 ); if ( cleared ) { result.add( new AssociationOperation( null, null, AssociationOperationType.CLEAR ) ); } result.addAll( currentState.values() ); return result; } /** * Returns the snapshot upon which this association is based, i.e. its original state when loaded from the datastore * or newly created. * * @return the snapshot upon which this association is based */ public AssociationSnapshot getSnapshot() { return snapshot; } /** * Whether this association contains no rows. * * @return {@code true} if this association contains no rows, {@code false} otherwise */ public boolean isEmpty() { int snapshotSize = cleared ? 0 : snapshot.size(); //nothing in both if ( snapshotSize == 0 && currentState.isEmpty() ) { return true; } //snapshot bigger than changeset if ( snapshotSize > currentState.size() ) { return false; } return size() == 0; } /** * Returns the number of rows within this association. * * @return the number of rows within this association */ public int size() { int size = cleared ? 0 : snapshot.size(); for ( Map.Entry<RowKey,AssociationOperation> op : currentState.entrySet() ) { switch ( op.getValue().getType() ) { case PUT: if ( cleared || !snapshot.containsKey( op.getKey() ) ) { size++; } break; case REMOVE: if ( !cleared && snapshot.containsKey( op.getKey() ) ) { size--; } break; } } return size; } /** * Returns all keys of all rows contained within this association. * * @return all keys of all rows contained within this association */ public Iterable<RowKey> getKeys() { if ( currentState.isEmpty() ) { if ( cleared ) { // if the association has been cleared and the currentState is empty, we consider that there are no rows. return Collections.emptyList(); } else { // otherwise, the snapshot rows are the current ones return snapshot.getRowKeys(); } } else { // It may be a bit too large in case of removals, but that's fine for now Set<RowKey> keys = CollectionHelper.newLinkedHashSet( cleared ? currentState.size() : snapshot.size() + currentState.size() ); if ( !cleared ) { // we add the snapshot RowKeys only if the association has not been cleared for ( RowKey rowKey : snapshot.getRowKeys() ) { keys.add( rowKey ); } } for ( Map.Entry<RowKey,AssociationOperation> op : currentState.entrySet() ) { switch ( op.getValue().getType() ) { case PUT: keys.add( op.getKey() ); break; case REMOVE: keys.remove( op.getKey() ); break; } } return keys; } } /** * Removes all rows from this association. */ public void clear() { cleared = true; currentState.clear(); } /** * Reset the association to the datastore state. */ public void reset() { cleared = false; currentState.clear(); } @Override public String toString() { StringBuilder sb = new StringBuilder( "Association[" ).append( StringHelper.lineSeparator() ); Iterator<RowKey> rowKeys = getKeys().iterator(); while ( rowKeys.hasNext() ) { RowKey rowKey = rowKeys.next(); sb.append( " " ).append( rowKey ).append( "=" ).append( get( rowKey ) ); if ( rowKeys.hasNext() ) { sb.append( "," ).append( StringHelper.lineSeparator() ); } } sb.append( StringHelper.lineSeparator() ).append( "]" ); return sb.toString(); } }