/*
* 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.datastore.document.impl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hibernate.ogm.dialect.spi.TupleContext;
import org.hibernate.ogm.model.spi.Tuple;
/**
* Captures useful data around the state of embeddable objects in tuples
* Note that the current implementation is *not* specific to MongoDB on purpose.
*
* @author Emmanuel Bernard <emmanuel@hibernate.org>
* @author Gunnar Morling
*/
public class EmbeddableStateFinder {
private final Tuple tuple;
private final List<String> columns;
private Set<String> nullEmbeddables = new HashSet<String>();
private Map<String, String> columnToOuterMostNullEmbeddableCache = new HashMap<String, String>();
public EmbeddableStateFinder(Tuple tuple, TupleContext tupleContext) {
this.tuple = tuple;
this.columns = tupleContext.getTupleTypeContext().getSelectableColumns();
}
/**
* Should only called on a column that is being set to null.
*
* Returns the most outer embeddable containing {@code column} that is entirely null.
* Return null otherwise i.e. not embeddable.
*
* The implementation lazily compute the embeddable state and caches it.
* The idea behind the lazy computation is that only some columns will be set to null
* and only in some situations.
* The idea behind caching is that an embeddable contains several columns, no need to recompute its state.
*/
public String getOuterMostNullEmbeddableIfAny(String column) {
String[] path = column.split( "\\." );
if ( !isEmbeddableColumn( path ) ) {
return null;
}
// the cached value may be null hence the explicit key lookup
if ( columnToOuterMostNullEmbeddableCache.containsKey( column ) ) {
return columnToOuterMostNullEmbeddableCache.get( column );
}
return determineAndCacheOuterMostNullEmbeddable( column, path );
}
/**
* Walks from the most outer embeddable to the most inner one
* look for all columns contained in these embeddables
* and exclude the embeddables that have a non null column
* because of caching, the algorithm is only run once per column parameter
*/
private String determineAndCacheOuterMostNullEmbeddable(String column, String[] path) {
String embeddable = path[0];
// process each embeddable from less specific to most specific
// exclude path leaves as it's a column and not an embeddable
for ( int index = 0; index < path.length - 1; index++ ) {
Set<String> columnsOfEmbeddable = getColumnsOfEmbeddableAndComputeEmbeddableNullness( embeddable );
if ( nullEmbeddables.contains( embeddable ) ) {
// the current embeddable only has null columns; cache that info for all the columns
for ( String columnOfEmbeddable : columnsOfEmbeddable ) {
columnToOuterMostNullEmbeddableCache.put( columnOfEmbeddable, embeddable );
}
break;
}
else {
maybeCacheOnNonNullEmbeddable( path, index, columnsOfEmbeddable );
}
// a more specific null embeddable might be present, carry on
embeddable += "." + path[index + 1];
}
return columnToOuterMostNullEmbeddableCache.get( column );
}
/**
* The embeddable is not null.
* Only cache the values if we are in the most specific embeddable containing {@code column}
* otherwise a more specific embeddable might be null and we would miss it
*
* Only set the values for the columns sharing this specific embeddable
* columns from deeper embeddables might be null
*/
private void maybeCacheOnNonNullEmbeddable(String[] path, int index, Set<String> columnsOfEmbeddable) {
if ( index == path.length - 2 ) {
//right level (i.e. the most specific embeddable for the column at bay
for ( String columnInvolved : columnsOfEmbeddable ) {
if ( columnInvolved.split( "\\." ).length == path.length ) {
// Only cache for columns from the same embeddable
columnToOuterMostNullEmbeddableCache.put( columnInvolved, null );
}
}
}
}
/**
* Gets all the columns (direct and nested) of the given embeddable. Also manages the {@link #fullyNullEmbeddables}
* cache to avoid one additional loop over the columns.
*/
private Set<String> getColumnsOfEmbeddableAndComputeEmbeddableNullness(String embeddable) {
Set<String> columnsOfEmbeddable = new HashSet<String>();
boolean hasOnlyNullColumns = true;
for ( String selectableColumn : columns ) {
if ( !isColumnPartOfEmbeddable( embeddable, selectableColumn ) ) {
continue;
}
columnsOfEmbeddable.add( selectableColumn );
if ( hasOnlyNullColumns && tuple.get( selectableColumn ) != null ) {
hasOnlyNullColumns = false;
}
}
if ( hasOnlyNullColumns ) {
nullEmbeddables.add( embeddable );
}
return columnsOfEmbeddable;
}
private boolean isColumnPartOfEmbeddable(String embeddable, String selectableColumn) {
return selectableColumn.startsWith( embeddable );
}
private boolean isEmbeddableColumn(String[] path) {
return path.length >= 2;
}
}