/* * Copyright 2016 MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mongodb.morphia; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.client.MongoCollection; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonString; import org.bson.Document; import org.bson.types.ObjectId; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mongodb.morphia.annotations.Collation; import org.mongodb.morphia.annotations.Embedded; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Field; import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Index; import org.mongodb.morphia.annotations.IndexOptions; import org.mongodb.morphia.annotations.Indexed; import org.mongodb.morphia.annotations.Indexes; import org.mongodb.morphia.annotations.Text; import org.mongodb.morphia.mapping.MappedClass; import org.mongodb.morphia.mapping.Mapper; import org.mongodb.morphia.mapping.MappingException; import org.mongodb.morphia.utils.IndexDirection; import org.mongodb.morphia.utils.IndexType; import java.util.List; import java.util.concurrent.TimeUnit; import static com.mongodb.BasicDBObject.parse; import static com.mongodb.client.model.CollationAlternate.SHIFTED; import static com.mongodb.client.model.CollationCaseFirst.UPPER; import static com.mongodb.client.model.CollationMaxVariable.SPACE; import static com.mongodb.client.model.CollationStrength.IDENTICAL; import static com.mongodb.client.model.CollationStrength.SECONDARY; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class IndexHelperTest extends TestBase { private final IndexHelper indexHelper = new IndexHelper(getMorphia().getMapper(), getDatabase()); @Before public void before() { getMorphia().map(AbstractParent.class, IndexedClass.class, NestedClass.class, NestedClassImpl.class); } @Test public void calculateBadKeys() { MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); IndexBuilder index = new IndexBuilder() .fields(new FieldBuilder() .value("texting") .type(IndexType.TEXT) .weight(1), new FieldBuilder() .value("nest") .type(IndexType.DESC)); try { indexHelper.calculateKeys(mappedClass, index); fail("Validation should have failed on the bad key"); } catch (MappingException e) { // all good } index.options(new IndexOptionsBuilder().disableValidation(true)); indexHelper.calculateKeys(mappedClass, index); } @Test public void calculateKeys() { MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); BsonDocument keys = indexHelper.calculateKeys(mappedClass, new IndexBuilder() .fields(new FieldBuilder() .value("text") .type(IndexType.TEXT) .weight(1), new FieldBuilder() .value("nest") .type(IndexType.DESC))); assertEquals(new BsonDocument() .append("text", new BsonString("text")) .append("nest", new BsonInt32(-1)), keys); } @Test public void createIndex() { checkMinServerVersion(3.4); String collectionName = getDs().getCollection(IndexedClass.class).getName(); MongoCollection<Document> collection = getDatabase().getCollection(collectionName); Mapper mapper = getMorphia().getMapper(); indexHelper.createIndex(collection, mapper.getMappedClass(IndexedClass.class), false); List<DBObject> indexInfo = getDs().getCollection(IndexedClass.class) .getIndexInfo(); assertEquals("Should have 6 indexes", 6, indexInfo.size()); for (DBObject dbObject : indexInfo) { String name = dbObject.get("name").toString(); if (name.equals("latitude_1")) { assertEquals(parse("{ 'latitude' : 1 }"), dbObject.get("key")); } else if (name.equals("behind_interface")) { assertEquals(parse("{ 'nest.name' : -1} "), dbObject.get("key")); assertEquals(parse("{ 'locale' : 'en' , 'caseLevel' : false , 'caseFirst' : 'off' , 'strength' : 2 , 'numericOrdering' :" + " false , 'alternate' : 'non-ignorable' , 'maxVariable' : 'punct' , 'normalization' : false , " + "'backwards' : false , 'version' : '57.1'}"), dbObject.get("collation")); } else if (name.equals("nest.name_1")) { assertEquals(parse("{ 'nest.name' : 1} "), dbObject.get("key")); } else if (name.equals("searchme")) { assertEquals(parse("{ 'text' : 10 }"), dbObject.get("weights")); } else if (name.equals("indexName_1")) { assertEquals(parse("{'indexName': 1 }"), dbObject.get("key")); } else { if (!"_id_".equals(dbObject.get("name"))) { throw new MappingException("Found an index I wasn't expecting: " + dbObject); } } } collection = getDatabase().getCollection(getDs().getCollection(AbstractParent.class).getName()); indexHelper.createIndex(collection, mapper.getMappedClass(AbstractParent.class), false); indexInfo = getDs().getCollection(AbstractParent.class).getIndexInfo(); assertTrue("Shouldn't find any indexes: " + indexInfo, indexInfo.isEmpty()); } @Test public void findField() { MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); assertEquals("indexName", indexHelper.findField(mappedClass, new IndexOptionsBuilder(), singletonList("indexName"))); assertEquals("nest.name", indexHelper.findField(mappedClass, new IndexOptionsBuilder(), asList("nested", "name"))); assertEquals("nest.name", indexHelper.findField(mappedClass, new IndexOptionsBuilder(), asList("nest", "name"))); try { assertEquals("nest.whatsit", indexHelper.findField(mappedClass, new IndexOptionsBuilder(), asList("nest", "whatsit"))); fail("Should have failed on the bad index path"); } catch (MappingException e) { // alles ist gut } assertEquals("nest.whatsit.nested.more.deeply.than.the.object.model", indexHelper.findField(mappedClass, new IndexOptionsBuilder().disableValidation(true), asList("nest", "whatsit", "nested", "more", "deeply", "than", "the", "object", "model"))); } @Test public void index() { checkMinServerVersion(3.4); MongoCollection<Document> indexes = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); indexes.drop(); Index index = new IndexBuilder() .fields(new FieldBuilder() .value("indexName"), new FieldBuilder() .value("text") .type(IndexType.DESC)) .options(indexOptions()); indexHelper.createIndex(indexes, mappedClass, index, false); List<DBObject> indexInfo = getDs().getCollection(IndexedClass.class) .getIndexInfo(); for (DBObject dbObject : indexInfo) { if (dbObject.get("name").equals("indexName")) { checkIndex(dbObject); assertEquals("en", dbObject.get("default_language")); assertEquals("de", dbObject.get("language_override")); assertEquals(new BasicDBObject() .append("locale", "en") .append("caseLevel", true) .append("caseFirst", "upper") .append("strength", 5) .append("numericOrdering", true) .append("alternate", "shifted") .append("maxVariable", "space") .append("backwards", true) .append("normalization", true) .append("version", "57.1"), dbObject.get("collation")); } } } @Test public void indexCollationConversion() { Collation collation = collation(); com.mongodb.client.model.Collation driver = indexHelper.convert(collation); assertEquals("en", driver.getLocale()); assertTrue(driver.getCaseLevel()); assertEquals(UPPER, driver.getCaseFirst()); assertEquals(IDENTICAL, driver.getStrength()); assertTrue(driver.getNumericOrdering()); assertEquals(SHIFTED, driver.getAlternate()); assertEquals(SPACE, driver.getMaxVariable()); assertTrue(driver.getNormalization()); assertTrue(driver.getBackwards()); } @Test public void indexOptionsConversion() { IndexOptionsBuilder indexOptions = indexOptions(); com.mongodb.client.model.IndexOptions options = indexHelper.convert(indexOptions, false); assertEquals("index_name", options.getName()); assertTrue(options.isBackground()); assertTrue(options.isUnique()); assertTrue(options.isSparse()); assertEquals(Long.valueOf(42), options.getExpireAfter(TimeUnit.SECONDS)); assertEquals("en", options.getDefaultLanguage()); assertEquals("de", options.getLanguageOverride()); assertEquals(indexHelper.convert(indexOptions.collation()), options.getCollation()); assertTrue(indexHelper.convert(indexOptions, true).isBackground()); assertTrue(indexHelper.convert(indexOptions.background(false), true).isBackground()); assertTrue(indexHelper.convert(indexOptions.background(true), true).isBackground()); assertTrue(indexHelper.convert(indexOptions.background(true), false).isBackground()); assertFalse(indexHelper.convert(indexOptions.background(false), false).isBackground()); } @Test public void oldIndexForm() { MongoCollection<Document> indexes = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); indexes.drop(); Index index = new IndexBuilder() .name("index_name") .background(true) .disableValidation(true) .dropDups(true) .expireAfterSeconds(42) .sparse(true) .unique(true) .value("indexName, -text"); indexHelper.createIndex(indexes, mappedClass, index, false); List<DBObject> indexInfo = getDs().getCollection(IndexedClass.class) .getIndexInfo(); for (DBObject dbObject : indexInfo) { if (dbObject.get("name").equals("index_indexName")) { checkIndex(dbObject); } } } @Test @SuppressWarnings("deprecation") public void oldIndexedForm() { Indexed indexed = new IndexedBuilder() .name("index_name") .background(true) .dropDups(true) .expireAfterSeconds(42) .sparse(true) .unique(true) .value(IndexDirection.DESC); assertEquals(indexed.options().name(), ""); Index converted = indexHelper.convert(indexed, "oldstyle"); assertEquals(converted.options().name(), "index_name"); assertTrue(converted.options().background()); assertTrue(converted.options().dropDups()); assertTrue(converted.options().sparse()); assertTrue(converted.options().unique()); assertEquals(new FieldBuilder().value("oldstyle").type(IndexType.DESC), converted.fields()[0]); } @Test @SuppressWarnings("deprecation") public void convertTextIndex() { TextBuilder text = new TextBuilder() .value(4) .options(new IndexOptionsBuilder() .name("index_name") .background(true) .dropDups(true) .expireAfterSeconds(42) .sparse(true) .unique(true)); Index index = indexHelper.convert(text, "search_field"); assertEquals(index.options().name(), "index_name"); assertTrue(index.options().background()); assertTrue(index.options().dropDups()); assertTrue(index.options().sparse()); assertTrue(index.options().unique()); assertEquals(new FieldBuilder() .value("search_field") .type(IndexType.TEXT) .weight(4), index.fields()[0]); } @Test @SuppressWarnings("deprecation") public void normalizeIndexed() { Indexed indexed = new IndexedBuilder() .value(IndexDirection.DESC) .options(new IndexOptionsBuilder().name("index_name") .background(true) .dropDups(true) .expireAfterSeconds(42) .sparse(true) .unique(true)); Index converted = indexHelper.convert(indexed, "oldstyle"); assertEquals(converted.options().name(), "index_name"); assertTrue(converted.options().background()); assertTrue(converted.options().dropDups()); assertTrue(converted.options().sparse()); assertTrue(converted.options().unique()); assertEquals(new FieldBuilder().value("oldstyle").type(IndexType.DESC), converted.fields()[0]); } @Test public void wildcardTextIndex() { MongoCollection<Document> indexes = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); IndexBuilder index = new IndexBuilder() .fields(new FieldBuilder() .value("$**") .type(IndexType.TEXT)); indexHelper.createIndex(indexes, mappedClass, index, false); List<DBObject> wildcard = getDb().getCollection("indexes").getIndexInfo(); boolean found = false; for (DBObject dbObject : wildcard) { found |= dbObject.get("name").equals("$**_text"); } assertTrue("Should have found the wildcard index", found); } @Test(expected = MappingException.class) public void weightsOnNonTextIndex() { MongoCollection<Document> indexes = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); IndexBuilder index = new IndexBuilder() .fields(new FieldBuilder() .value("name") .weight(10)); indexHelper.createIndex(indexes, mappedClass, index, false); } @Test public void indexPartialFilters() { MongoCollection<Document> collection = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); Index index = new IndexBuilder() .fields(new FieldBuilder().value("text")) .options(new IndexOptionsBuilder() .partialFilter("{ name : { $gt : 13 } }")); indexHelper.createIndex(collection, mappedClass, index, false); findPartialIndex(BasicDBObject.parse(index.options().partialFilter())); } @Test public void indexedPartialFilters() { MongoCollection<Document> collection = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); Indexed indexed = new IndexedBuilder() .options(new IndexOptionsBuilder() .partialFilter("{ name : { $gt : 13 } }")); indexHelper.createIndex(collection, mappedClass, indexHelper.convert(indexed, "text"), false); findPartialIndex(BasicDBObject.parse(indexed.options().partialFilter())); } @Test public void textPartialFilters() { MongoCollection<Document> collection = getDatabase().getCollection("indexes"); MappedClass mappedClass = getMorphia().getMapper().getMappedClass(IndexedClass.class); Text text = new TextBuilder() .value(4) .options(new IndexOptionsBuilder() .partialFilter("{ name : { $gt : 13 } }")); indexHelper.createIndex(collection, mappedClass, indexHelper.convert(text, "text"), false); findPartialIndex(BasicDBObject.parse(text.options().partialFilter())); } private void checkIndex(final DBObject dbObject) { assertTrue((Boolean) dbObject.get("background")); assertTrue((Boolean) dbObject.get("unique")); assertTrue((Boolean) dbObject.get("sparse")); assertEquals(42L, dbObject.get("expireAfterSeconds")); assertEquals(new BasicDBObject("name", 1).append("text", -1), dbObject.get("key")); } private void findPartialIndex(final BasicDBObject expected) { List<DBObject> indexInfo = getDs().getCollection(IndexedClass.class) .getIndexInfo(); for (DBObject dbObject : indexInfo) { if (!dbObject.get("name").equals("_id_")) { Assert.assertEquals(expected, dbObject.get("partialFilterExpression")); } } } private Collation collation() { return new CollationBuilder() .alternate(SHIFTED) .backwards(true) .caseFirst(UPPER) .caseLevel(true) .locale("en") .maxVariable(SPACE) .normalization(true) .numericOrdering(true) .strength(IDENTICAL); } private IndexOptionsBuilder indexOptions() { return new IndexOptionsBuilder() .name("index_name") .background(true) .collation(collation()) .disableValidation(true) .dropDups(true) .expireAfterSeconds(42) .language("en") .languageOverride("de") .sparse(true) .unique(true); } @Embedded private interface NestedClass { } @Entity("indexes") @Indexes(@Index(fields = @Field("latitude"))) private static class IndexedClass extends AbstractParent { @Text(value = 10, options = @IndexOptions(name = "searchme")) private String text; private double latitude; @Embedded("nest") private NestedClass nested; } @Indexes( @Index(fields = @Field(value = "name", type = IndexType.DESC), options = @IndexOptions(name = "behind_interface", collation = @Collation(locale = "en", strength = SECONDARY)))) private static class NestedClassImpl implements NestedClass { @Indexed private String name; } @Indexes(@Index(fields = @Field("indexName"))) private abstract static class AbstractParent { @Id private ObjectId id; private double indexName; } }