/* * 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.mongodb.index.impl; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.concurrent.TimeUnit; import org.hibernate.mapping.Column; import org.hibernate.mapping.Index; import org.hibernate.mapping.UniqueKey; import org.hibernate.ogm.datastore.mongodb.logging.impl.Log; import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory; import com.mongodb.client.model.IndexOptions; import org.bson.Document; import org.bson.conversions.Bson; /** * Definition of an index to be applied to a MongoDB collection * * @author Guillaume Smet * @see <a href="https://docs.mongodb.com/manual/indexes/">info about indexes in MongoDB Java Driver 3.4</a> */ public class MongoDBIndexSpec { private Log log = LoggerFactory.getLogger(); /** * The MongoDB collection/table for which the index will be set */ private String collection; /** * Optional. The name of the index. If unspecified, MongoDB generates an index name * by concatenating the names of the indexed fields and the sort order. * * Whether user specified or MongoDB generated, index names including their full namespace (i.e. database.collection) * cannot be longer than the Index Name Limit (that is 128 characters) */ private String indexName; /** * The index keys of the index prepared for MongoDB. */ private Document indexKeys = new Document(); /** * The options specific to MongoDB. */ private IndexOptions options; /** * Indicates if the index is a text index. */ private boolean isTextIndex = false; /** * Constructor used for columns marked as unique. */ public MongoDBIndexSpec(String collection, String columnName, String indexName, Document options) { this.options = prepareOptions( options, indexName, true ); this.collection = collection; this.indexName = indexName; indexKeys.put( columnName, 1 ); } /** * Constructor used for {@link UniqueKey}s. */ public MongoDBIndexSpec(UniqueKey uniqueKey, Document options) { this.options = prepareOptions( options, uniqueKey.getName(), true ); this.collection = uniqueKey.getTable().getName(); this.indexName = uniqueKey.getName(); this.addIndexKeys( uniqueKey.getColumnIterator(), uniqueKey.getColumnOrderMap() ); } /** * Constructor used for {@link Index}es. */ public MongoDBIndexSpec(Index index, Document options) { this.options = prepareOptions( options, index.getName(), false ); this.collection = index.getTable().getName(); this.indexName = index.getName(); // TODO OGM-1080: the columnOrderMap is not accessible for an Index this.addIndexKeys( index.getColumnIterator(), Collections.<Column, String>emptyMap() ); } /** * Prepare the options by adding additional information to them. * @see <a href="https://docs.mongodb.com/manual/core/index-ttl/"> TTL Indexes</a> */ private IndexOptions prepareOptions(Document options, String indexName, boolean unique) { IndexOptions indexOptions = new IndexOptions(); indexOptions.name( indexName ).unique( unique ).background( options.getBoolean( "background" , false ) ); if ( unique ) { // MongoDB only allows one null value per unique index which is not in line with what we usually consider // as the definition of a unique constraint. Thus, we mark the index as sparse to only index values // defined and avoid this issue. We do this only if a partialFilterExpression has not been defined // as partialFilterExpression and sparse are exclusive. indexOptions.sparse( !options.containsKey( "partialFilterExpression" ) ); } else if ( options.containsKey( "partialFilterExpression" ) ) { indexOptions.partialFilterExpression( (Bson) options.get( "partialFilterExpression" ) ); } if ( options.containsKey( "expireAfterSeconds" ) ) { //@todo is it correct? indexOptions.expireAfter( options.getInteger( "expireAfterSeconds" ).longValue() , TimeUnit.SECONDS ); } if ( Boolean.TRUE.equals( options.get( "text" ) ) ) { // text is an option we take into account to mark an index as a full text index as we cannot put "text" as // the order like MongoDB as ORM explicitely checks that the order is either asc or desc. // we remove the option from the Document so that we don't pass it to MongoDB if ( options.containsKey( "default_language" ) ) { indexOptions.defaultLanguage( options.getString( "default_language" ) ); } if ( options.containsKey( "weights" ) ) { indexOptions.weights( (Bson) options.get( "weights" ) ); } isTextIndex = true; options.remove( "text" ); } return indexOptions; } public String getCollection() { return collection; } public String getIndexName() { return indexName; } public boolean isTextIndex() { return isTextIndex; } private void addIndexKeys(Iterator<Column> columnIterator, Map<Column, String> columnOrderMap) { while ( columnIterator.hasNext() ) { Column column = columnIterator.next(); Object mongoDBOrder; if ( isTextIndex ) { mongoDBOrder = "text"; } else { String order = columnOrderMap.get( column ) != null ? columnOrderMap.get( column ) : "asc"; mongoDBOrder = "asc".equals( order ) ? 1 : -1; } indexKeys.put( column.getName(), mongoDBOrder ); } } public IndexOptions getOptions() { return options; } public Document getIndexKeysDocument() { return indexKeys; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append( getClass().getSimpleName() ); sb.append( "[" ); sb.append( "collection: " ).append( collection ).append( ", " ); sb.append( "indexName: " ).append( indexName ); sb.append( "]" ); return sb.toString(); } }