org.hibernate.ogm.datastore.mongodb.impl.MongoDBSchemaDefiner.java Source code

Java tutorial

Introduction

Here is the source code for org.hibernate.ogm.datastore.mongodb.impl.MongoDBSchemaDefiner.java

Source

/*
 * 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.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.NamingHelper;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Index;
import org.hibernate.mapping.Table;
import org.hibernate.mapping.UniqueKey;
import org.hibernate.ogm.datastore.mongodb.MongoDBDialect;
import org.hibernate.ogm.datastore.mongodb.index.impl.MongoDBIndexSpec;
import org.hibernate.ogm.datastore.mongodb.logging.impl.Log;
import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory;
import org.hibernate.ogm.datastore.spi.BaseSchemaDefiner;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata;
import org.hibernate.ogm.model.key.spi.EntityKeyMetadata;
import org.hibernate.ogm.model.key.spi.IdSourceKeyMetadata;
import org.hibernate.ogm.options.shared.impl.IndexOptionsOption;
import org.hibernate.ogm.options.shared.spi.IndexOption;
import org.hibernate.ogm.options.shared.spi.IndexOptions;
import org.hibernate.ogm.options.spi.OptionsService;
import org.hibernate.ogm.persister.impl.OgmEntityPersister;
import org.hibernate.ogm.util.impl.Contracts;
import org.hibernate.ogm.util.impl.StringHelper;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy;

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoException;

/**
 * Performs sanity checks of the mapped objects.
 *
 * @author Gunnar Morling
 * @author Sanne Grinovero
 * @author Francois Le Droff
 * @author Guillaume Smet
 */
public class MongoDBSchemaDefiner extends BaseSchemaDefiner {

    private static final Log log = LoggerFactory.getLogger();

    private static final int INDEX_CREATION_ERROR_CODE = 85;

    private List<MongoDBIndexSpec> indexSpecs = new ArrayList<>();

    @Override
    public void validateMapping(SchemaDefinitionContext context) {
        validateGenerators(context.getAllIdSourceKeyMetadata());
        validateEntityCollectionNames(context.getAllEntityKeyMetadata());
        validateAssociationNames(context.getAllAssociationKeyMetadata());
        validateAllPersisters(context.getSessionFactory().getEntityPersisters().values());
        validateIndexSpecs(context);
    }

    @Override
    public void initializeSchema(SchemaDefinitionContext context) {
        SessionFactoryImplementor sessionFactoryImplementor = context.getSessionFactory();
        ServiceRegistryImplementor registry = sessionFactoryImplementor.getServiceRegistry();
        MongoDBDatastoreProvider provider = (MongoDBDatastoreProvider) registry.getService(DatastoreProvider.class);

        for (MongoDBIndexSpec indexSpec : indexSpecs) {
            createIndex(provider.getDatabase(), indexSpec);
        }
    }

    private void validateAllPersisters(Iterable<EntityPersister> persisters) {
        for (EntityPersister persister : persisters) {
            if (persister instanceof OgmEntityPersister) {
                OgmEntityPersister ogmPersister = (OgmEntityPersister) persister;
                int propertySpan = ogmPersister.getEntityMetamodel().getPropertySpan();
                for (int i = 0; i < propertySpan; i++) {
                    String[] columnNames = ogmPersister.getPropertyColumnNames(i);
                    for (String columnName : columnNames) {
                        validateAsMongoDBFieldName(columnName);
                    }
                }
            }
        }
    }

    private void validateAssociationNames(Iterable<AssociationKeyMetadata> allAssociationKeyMetadata) {
        for (AssociationKeyMetadata associationKeyMetadata : allAssociationKeyMetadata) {
            validateAsMongoDBCollectionName(associationKeyMetadata.getTable());
            for (String column : associationKeyMetadata.getRowKeyColumnNames()) {
                validateAsMongoDBFieldName(column);
            }
        }
    }

    private void validateEntityCollectionNames(Iterable<EntityKeyMetadata> allEntityKeyMetadata) {
        for (EntityKeyMetadata entityKeyMetadata : allEntityKeyMetadata) {
            validateAsMongoDBCollectionName(entityKeyMetadata.getTable());
            for (String column : entityKeyMetadata.getColumnNames()) {
                validateAsMongoDBFieldName(column);
            }
        }
    }

    private void validateGenerators(Iterable<IdSourceKeyMetadata> allIdSourceKeyMetadata) {
        for (IdSourceKeyMetadata idSourceKeyMetadata : allIdSourceKeyMetadata) {
            String keyColumn = idSourceKeyMetadata.getKeyColumnName();

            if (!keyColumn.equals(MongoDBDialect.ID_FIELDNAME)) {
                log.cannotUseGivenPrimaryKeyColumnName(keyColumn, MongoDBDialect.ID_FIELDNAME);
            }
        }
    }

    private void validateIndexSpecs(SchemaDefinitionContext context) {
        OptionsService optionsService = context.getSessionFactory().getServiceRegistry()
                .getService(OptionsService.class);
        Map<String, Class<?>> tableEntityTypeMapping = context.getTableEntityTypeMapping();

        Database database = context.getDatabase();
        UniqueConstraintSchemaUpdateStrategy constraintMethod = UniqueConstraintSchemaUpdateStrategy
                .interpret(context.getSessionFactory().getProperties()
                        .get(Environment.UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY));
        if (constraintMethod == UniqueConstraintSchemaUpdateStrategy.SKIP) {
            log.tracef("Skipping generation of unique constraints");
        }

        for (Namespace namespace : database.getNamespaces()) {
            for (Table table : namespace.getTables()) {
                if (table.isPhysicalTable()) {
                    Class<?> entityType = tableEntityTypeMapping.get(table.getName());
                    if (entityType == null) {
                        continue;
                    }

                    IndexOptions indexOptions = getIndexOptions(optionsService, entityType);
                    Set<String> forIndexNotReferenced = new HashSet<>(indexOptions.getReferencedIndexes());

                    validateIndexSpecsForUniqueColumns(table, indexOptions, forIndexNotReferenced,
                            constraintMethod);

                    validateIndexSpecsForUniqueKeys(table, indexOptions, forIndexNotReferenced, constraintMethod);

                    validateIndexSpecsForIndexes(table, indexOptions, forIndexNotReferenced);

                    for (String forIndex : forIndexNotReferenced) {
                        log.indexOptionReferencingNonExistingIndex(table.getName(), forIndex);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void validateIndexSpecsForUniqueColumns(Table table, IndexOptions indexOptions,
            Set<String> forIndexNotReferenced, UniqueConstraintSchemaUpdateStrategy constraintMethod) {
        Iterator<Column> columnIterator = table.getColumnIterator();
        while (columnIterator.hasNext()) {
            Column column = columnIterator.next();
            if (column.isUnique()) {
                String indexName = NamingHelper.INSTANCE.generateHashedConstraintName("UK_",
                        table.getNameIdentifier(), Identifier.toIdentifier(column.getName()));
                forIndexNotReferenced.remove(indexName);

                if (constraintMethod != UniqueConstraintSchemaUpdateStrategy.SKIP) {
                    MongoDBIndexSpec indexSpec = new MongoDBIndexSpec(table.getName(), column.getName(), indexName,
                            getIndexOptionDBObject(table, indexOptions.getOptionForIndex(indexName)));
                    if (validateIndexSpec(indexSpec)) {
                        indexSpecs.add(indexSpec);
                    }
                }
            }
        }
    }

    private void validateIndexSpecsForUniqueKeys(Table table, IndexOptions indexOptions,
            Set<String> forIndexNotReferenced, UniqueConstraintSchemaUpdateStrategy constraintMethod) {
        Iterator<UniqueKey> keys = table.getUniqueKeyIterator();
        while (keys.hasNext()) {
            UniqueKey uniqueKey = keys.next();
            forIndexNotReferenced.remove(uniqueKey.getName());

            if (constraintMethod != UniqueConstraintSchemaUpdateStrategy.SKIP) {
                MongoDBIndexSpec indexSpec = new MongoDBIndexSpec(uniqueKey,
                        getIndexOptionDBObject(table, indexOptions.getOptionForIndex(uniqueKey.getName())));
                if (validateIndexSpec(indexSpec)) {
                    indexSpecs.add(indexSpec);
                }
            }
        }
    }

    private void validateIndexSpecsForIndexes(Table table, IndexOptions indexOptions,
            Set<String> forIndexNotReferenced) {
        Iterator<Index> indexes = table.getIndexIterator();
        while (indexes.hasNext()) {
            Index index = indexes.next();
            forIndexNotReferenced.remove(index.getName());
            MongoDBIndexSpec indexSpec = new MongoDBIndexSpec(index,
                    getIndexOptionDBObject(table, indexOptions.getOptionForIndex(index.getName())));
            if (validateIndexSpec(indexSpec)) {
                indexSpecs.add(indexSpec);
            }
        }
    }

    private DBObject getIndexOptionDBObject(Table table, IndexOption indexOption) {
        try {
            BasicDBObject options;
            if (StringHelper.isNullOrEmptyString(indexOption.getOptions())) {
                options = new BasicDBObject();
            } else {
                options = BasicDBObject.parse(indexOption.getOptions());
            }
            options.put("name", indexOption.getTargetIndexName());
            return options;
        } catch (Exception e) {
            throw log.invalidOptionsFormatForIndex(table.getName(), indexOption.getTargetIndexName(), e);
        }
    }

    private IndexOptions getIndexOptions(OptionsService optionsService, Class<?> entityType) {
        IndexOptions options = optionsService.context().getEntityOptions(entityType)
                .getUnique(IndexOptionsOption.class);
        if (options == null) {
            options = new IndexOptions();
        }
        return options;
    }

    private boolean validateIndexSpec(MongoDBIndexSpec indexSpec) {
        boolean valid = true;
        if (StringHelper.isNullOrEmptyString(indexSpec.getIndexName())) {
            log.indexNameIsEmpty(indexSpec.getCollection());
            valid = false;
        }
        if (indexSpec.getIndexKeysDBObject().keySet().isEmpty()) {
            log.noValidKeysForIndex(indexSpec.getCollection(), indexSpec.getIndexName());
            valid = false;
        }
        return valid;
    }

    public void createIndex(DB database, MongoDBIndexSpec indexSpec) {
        DBCollection collection = database.getCollection(indexSpec.getCollection());
        Map<String, DBObject> preexistingIndexes = getIndexes(collection);
        String preexistingTextIndex = getPreexistingTextIndex(preexistingIndexes);

        // if a text index already exists in the collection, MongoDB silently ignores the creation of the new text index
        // so we might as well log a warning about it
        if (indexSpec.isTextIndex() && preexistingTextIndex != null
                && !preexistingTextIndex.equalsIgnoreCase(indexSpec.getIndexName())) {
            throw log.unableToCreateTextIndex(collection.getName(), indexSpec.getIndexName(), preexistingTextIndex);
        }

        try {
            // if the index is already present and with the same definition, MongoDB simply ignores the call
            // if the definition is not the same, MongoDB throws an error, except in the case of a text index
            // where it silently ignores the creation
            collection.createIndex(indexSpec.getIndexKeysDBObject(), indexSpec.getOptions());
        } catch (MongoException e) {
            String indexName = indexSpec.getIndexName();
            if (e.getCode() == INDEX_CREATION_ERROR_CODE && !StringHelper.isNullOrEmptyString(indexName)
                    && preexistingIndexes.containsKey(indexName)) {
                // The index already exists with a different definition and has a name: we drop it and we recreate it
                collection.dropIndex(indexName);
                collection.createIndex(indexSpec.getIndexKeysDBObject(), indexSpec.getOptions());
            } else {
                throw log.unableToCreateIndex(collection.getName(), indexName, e);
            }
        }
    }

    private Map<String, DBObject> getIndexes(DBCollection collection) {
        List<DBObject> indexes = collection.getIndexInfo();
        Map<String, DBObject> indexMap = new HashMap<>();
        for (DBObject index : indexes) {
            indexMap.put(index.get("name").toString(), index);
        }
        return indexMap;
    }

    private String getPreexistingTextIndex(Map<String, DBObject> preexistingIndexes) {
        for (Entry<String, DBObject> indexEntry : preexistingIndexes.entrySet()) {
            DBObject keys = (DBObject) indexEntry.getValue().get("key");
            if (keys != null && keys.containsField("_fts")) {
                return indexEntry.getKey();
            }
        }
        return null;
    }

    /**
     * Validates a String to be a valid name to be used in MongoDB for a collection name.
     *
     * @param collectionName
     */
    private static void validateAsMongoDBCollectionName(String collectionName) {
        Contracts.assertStringParameterNotEmpty(collectionName, "requestedName");
        //Yes it has some strange requirements.
        if (collectionName.startsWith("system.")) {
            throw log.collectionNameHasInvalidSystemPrefix(collectionName);
        } else if (collectionName.contains("\u0000")) {
            throw log.collectionNameContainsNULCharacter(collectionName);
        } else if (collectionName.contains("$")) {
            throw log.collectionNameContainsDollarCharacter(collectionName);
        }
    }

    /**
     * Validates a String to be a valid name to be used in MongoDB for a field name.
     *
     * @param fieldName
     */
    private void validateAsMongoDBFieldName(String fieldName) {
        if (fieldName.startsWith("$")) {
            throw log.fieldNameHasInvalidDollarPrefix(fieldName);
        } else if (fieldName.contains("\u0000")) {
            throw log.fieldNameContainsNULCharacter(fieldName);
        }
    }

}