/* * Hibernate, Relational Persistence for Idiomatic Java * * JBoss, Home of Professional Open Source * Copyright 2010-2011 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public License, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package org.hibernate.ogm.util.impl; import org.hibernate.HibernateException; import org.hibernate.annotations.common.AssertionFailure; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.ogm.datastore.map.impl.MapAssociationSnapshot; import org.hibernate.ogm.datastore.spi.Association; import org.hibernate.ogm.datastore.spi.AssociationContext; import org.hibernate.ogm.datastore.spi.Tuple; import org.hibernate.ogm.dialect.GridDialect; import org.hibernate.ogm.grid.AssociationKey; import org.hibernate.ogm.grid.AssociationKind; import org.hibernate.ogm.grid.EntityKey; import org.hibernate.ogm.grid.RowKey; import org.hibernate.ogm.persister.CollectionPhysicalModel; import org.hibernate.ogm.persister.EntityKeyBuilder; import org.hibernate.ogm.persister.OgmCollectionPersister; import org.hibernate.ogm.persister.OgmEntityPersister; import org.hibernate.ogm.type.GridType; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.Loadable; import org.hibernate.type.CollectionType; import org.hibernate.type.EntityType; import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * @author Emmanuel Bernard */ public class PropertyMetadataProvider { private String tableName; private String[] keyColumnNames; private GridType keyGridType; private Object key; private SessionImplementor session; private AssociationKey collectionMetadataKey; private Association collectionMetadata; private Object[] columnValues; private GridDialect gridDialect; private OgmCollectionPersister collectionPersister; private boolean inverse; private Type propertyType; private String[] rowKeyColumnNames; private AssociationContext associationContext; /* * Return true if the other side association has been searched and not been found * The other side association is searched when we are looking forward to udpate it * and need to build the corresponding association key. * * It uses Boolean instead of boolean to make sure it's used only after being calculated */ private Boolean isBidirectional; //fluent methods for populating data public PropertyMetadataProvider gridDialect(GridDialect gridDialect) { this.gridDialect = gridDialect; return this; } //optional: data retrieved from gridManager if not set up public PropertyMetadataProvider tableName(String tableName) { this.tableName = tableName; return this; } public PropertyMetadataProvider keyColumnNames(String[] keyColumnNames) { this.keyColumnNames = keyColumnNames; return this; } public PropertyMetadataProvider keyGridType(GridType keyGridType) { this.keyGridType = keyGridType; return this; } public PropertyMetadataProvider session(SessionImplementor session) { this.session = session; return this; } public PropertyMetadataProvider key(Object key) { this.key = key; return this; } public PropertyMetadataProvider keyColumnValues(Object[] columnValues) { this.columnValues = columnValues; return this; } public PropertyMetadataProvider inverse() { this.inverse = true; return this; } //action methods private AssociationKey getCollectionMetadataKey() { if ( collectionMetadataKey == null ) { final Object[] columnValues = getKeyColumnValues(); collectionMetadataKey = new AssociationKey( tableName, keyColumnNames, columnValues ); // We have a collection on the main side if (collectionPersister != null) { EntityKey entityKey; // we are explicitly looking to update the non owning side if ( inverse ) { //look for the other side of the collection, build the key of the other side's entity OgmEntityPersister elementPersister = (OgmEntityPersister) collectionPersister.getElementPersister(); entityKey = EntityKeyBuilder.fromPersister( elementPersister, (Serializable) key, session ); collectionMetadataKey.setCollectionRole( buildCollectionRole(collectionPersister) ); } else { //we are on the right side, use the association property collectionMetadataKey.setCollectionRole( getUnqualifiedRole( collectionPersister ) ); entityKey = EntityKeyBuilder.fromPersister( (OgmEntityPersister) collectionPersister.getOwnerEntityPersister(), (Serializable) key, session ); } collectionMetadataKey.setOwnerEntityKey( entityKey ); //TODO add information on the collection type, set, map, bag, list etc AssociationKind type = collectionPersister.getElementType().isEntityType() ? AssociationKind.ASSOCIATION : AssociationKind.EMBEDDED; collectionMetadataKey.setAssociationKind( type ); collectionMetadataKey.setRowKeyColumnNames( collectionPersister.getRowKeyColumnNames() ); } // We have a to-one on the main side else if ( propertyType != null ) { collectionMetadataKey.setAssociationKind( propertyType.isEntityType() ? AssociationKind.ASSOCIATION : AssociationKind.EMBEDDED ); if ( propertyType instanceof EntityType ) { EntityType entityType = (EntityType) propertyType; OgmEntityPersister associatedPersister = (OgmEntityPersister) entityType.getAssociatedJoinable( session.getFactory() ); EntityKey entityKey = new EntityKey( associatedPersister.getTableName(), associatedPersister.getIdentifierColumnNames(), columnValues ); collectionMetadataKey.setOwnerEntityKey( entityKey ); collectionMetadataKey.setRowKeyColumnNames( rowKeyColumnNames ); collectionMetadataKey.setCollectionRole( getCollectionRoleFromToOne( associatedPersister ) ); } else { throw new AssertionFailure( "Cannot detect associated entity metadata. propertyType is of unexpected type: " + propertyType.getClass() ); } } else { throw new AssertionFailure( "Cannot detect associated entity metadata: collectionPersister and propertyType are both null" ); } } return collectionMetadataKey; } /* * Try and find the inverse association matching from the associated entity * If a match is found, use the other side's association name as role * Otherwise use the table name */ //TODO we could cache such knowledge in a service if that turns out to be costly private String getCollectionRoleFromToOne(OgmEntityPersister associatedPersister) { //code logic is slightly duplicated but the input and context is different, hence this choice Type[] propertyTypes = associatedPersister.getPropertyTypes(); String otherSidePropertyName = null; for ( int index = 0 ; index < propertyTypes.length ; index++ ) { Type type = propertyTypes[index]; boolean matching = false; //we try and restrict type search as much as possible //we look for associations that also are collections if ( type.isAssociationType() && type.isCollectionType() ) { matching = isCollectionMatching( (CollectionType) type, tableName ); } //we look for associations that are to-one else if ( type.isAssociationType() && ! type.isCollectionType() ) { //isCollectionType redundant but kept for readability matching = isToOneMatching( associatedPersister, index, type ); } if ( matching ) { otherSidePropertyName = associatedPersister.getPropertyNames()[index]; break; } } return processOtherSidePropertyName( otherSidePropertyName ); } private boolean isCollectionMatching(CollectionType type, String primarySideTableName) { // Find the reverse side collection and check if the table name and key columns are matching // what we have on the main side String collectionRole = type.getRole(); CollectionPhysicalModel reverseCollectionPersister = (CollectionPhysicalModel) session.getFactory().getCollectionPersister( collectionRole ); boolean isSameTable = primarySideTableName.equals( reverseCollectionPersister.getTableName() ); return isSameTable && Arrays.equals( keyColumnNames, reverseCollectionPersister.getKeyColumnNames() ); } /* * Try and find the inverse association matching from the associated entity * If a match is found, use the other side's association name as role * Otherwise use the table name */ //TODO we could cache such knowledge in a service if that turns out to be costly private String buildCollectionRole(OgmCollectionPersister collectionPersister) { String otherSidePropertyName = null; Loadable elementPersister = (Loadable) collectionPersister.getElementPersister(); Type[] propertyTypes = elementPersister.getPropertyTypes(); for ( int index = 0 ; index < propertyTypes.length ; index++ ) { Type type = propertyTypes[index]; //we try and restrict type search as much as possible if ( type.isAssociationType() ) { boolean matching = false; //if the main side collection is a one-to-many, the reverse side should be a to-one is not a collection if ( collectionPersister.isOneToMany() && ! type.isCollectionType() ) { matching = isToOneMatching( elementPersister, index, type ); } //if the main side collection is not a one-to-many, the reverse side should be a collection else if ( ! collectionPersister.isOneToMany() && type.isCollectionType() ) { matching = isCollectionMatching( (CollectionType) type, collectionPersister.getTableName() ); } if ( matching ) { otherSidePropertyName = elementPersister.getPropertyNames()[index]; break; } } } return processOtherSidePropertyName( otherSidePropertyName ); } private boolean isToOneMatching(Loadable elementPersister, int index, Type type) { if ( ( (EntityType) type ).isOneToOne() ) { // If that's a OneToOne check the associated property name and see if it matches where we come from // we need to do that as OneToOne don't define columns OneToOneType oneToOneType = (OneToOneType) type; String associatedProperty = oneToOneType.getRHSUniqueKeyPropertyName(); if ( associatedProperty != null ) { OgmEntityPersister mainSidePersister = (OgmEntityPersister) oneToOneType.getAssociatedJoinable( session.getFactory() ); try { int propertyIndex = mainSidePersister.getPropertyIndex( associatedProperty ); return mainSidePersister.getPropertyTypes()[propertyIndex] == propertyType; } catch ( HibernateException e ) { //not the right property //probably should not happen } } } //otherwise we do a key column comparison to see if it matches return Arrays.equals( keyColumnNames, elementPersister.getPropertyColumnNames( index ) ); } private String processOtherSidePropertyName(String otherSidePropertyName) { //if we found the matching property on the reverse side, we are //bidirectional, otherwise we are not if ( otherSidePropertyName != null ) { isBidirectional = Boolean.TRUE; } else { isBidirectional = Boolean.FALSE; otherSidePropertyName = tableName; } return otherSidePropertyName; } private String getUnqualifiedRole(CollectionPersister persister) { String entity = persister.getOwnerEntityPersister().getEntityName(); String role = persister.getRole(); return role.substring( entity.length() + 1 ); } private Object[] getKeyColumnValues() { if ( columnValues == null ) { columnValues = LogicalPhysicalConverterHelper.getColumnsValuesFromObjectValue( key, keyGridType, keyColumnNames, session ); } return columnValues; } public Tuple createAndPutAssociationTuple(RowKey rowKey) { Tuple associationTuple = gridDialect.createTupleAssociation( getCollectionMetadataKey(), rowKey ); getCollectionMetadata().put( rowKey, associationTuple); return associationTuple; } /* * Load a collection and create it if it is not found */ public Association getCollectionMetadata() { if ( collectionMetadata == null ) { // Compute bi-directionality first AssociationKey key = getCollectionMetadataKey(); if ( isBidirectional == Boolean.FALSE ){ //fake association to prevent unidirectional associations to keep record of the inverse side collectionMetadata = new Association( new MapAssociationSnapshot( Collections.EMPTY_MAP ) ); } else { collectionMetadata = gridDialect.getAssociation( key, this.getAssociationContext() ); if (collectionMetadata == null) { collectionMetadata = gridDialect.createAssociation( key ); } } } return collectionMetadata; } /* * Does not create a collection if it is not found */ public Association getCollectionMetadataOrNull() { if ( collectionMetadata == null ) { collectionMetadata = gridDialect.getAssociation( getCollectionMetadataKey(), this.getAssociationContext() ); } return collectionMetadata; } public void flushToCache() { //If we don't have a bidirectional association, do not update the info //to prevent unidirectional associations to keep record of the inverse side if ( isBidirectional != Boolean.FALSE ) { if ( getCollectionMetadata().isEmpty() ) { gridDialect.removeAssociation( getCollectionMetadataKey() ); collectionMetadata = null; } else { gridDialect.updateAssociation( getCollectionMetadata(), getCollectionMetadataKey() ); } } } public PropertyMetadataProvider collectionPersister(OgmCollectionPersister collectionPersister) { this.collectionPersister = collectionPersister; return this; } public PropertyMetadataProvider propertyType(Type type) { this.propertyType = type; return this; } public PropertyMetadataProvider rowKeyColumnNames(String[] rowKeyColumnNames) { this.rowKeyColumnNames = rowKeyColumnNames; return this; } private AssociationContext getAssociationContext() { if ( associationContext == null ) { if ( collectionPersister != null ) { associationContext = collectionPersister.getAssociationContext(); } else { List<String> selectableColumns = new ArrayList<String>( rowKeyColumnNames.length ); for ( String column : rowKeyColumnNames ) { selectableColumns.add( column ); } associationContext = new AssociationContext( selectableColumns ); } } return associationContext; } }