package org.mongodb.morphia;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.Document;
import org.bson.codecs.Encoder;
import org.bson.codecs.EncoderContext;
import org.mongodb.morphia.annotations.Collation;
import org.mongodb.morphia.annotations.Field;
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.NotSaved;
import org.mongodb.morphia.annotations.Reference;
import org.mongodb.morphia.annotations.Serialized;
import org.mongodb.morphia.annotations.Text;
import org.mongodb.morphia.logging.Logger;
import org.mongodb.morphia.logging.MorphiaLoggerFactory;
import org.mongodb.morphia.mapping.MappedClass;
import org.mongodb.morphia.mapping.MappedField;
import org.mongodb.morphia.mapping.Mapper;
import org.mongodb.morphia.mapping.MappingException;
import org.mongodb.morphia.utils.IndexType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.mongodb.morphia.AnnotationBuilder.toMap;
import static org.mongodb.morphia.utils.IndexType.fromValue;
final class IndexHelper {
private static final Logger LOG = MorphiaLoggerFactory.get(IndexHelper.class);
private static final EncoderContext ENCODER_CONTEXT = EncoderContext.builder().build();
private final Mapper mapper;
private final MongoDatabase database;
IndexHelper(final Mapper mapper, final MongoDatabase database) {
this.mapper = mapper;
this.database = database;
}
private static String join(final List<String> path, final char delimiter) {
StringBuilder builder = new StringBuilder();
for (String element : path) {
if (builder.length() != 0) {
builder.append(delimiter);
}
builder.append(element);
}
return builder.toString();
}
private void calculateWeights(final Index index, final com.mongodb.client.model.IndexOptions indexOptions) {
Document weights = new Document();
for (Field field : index.fields()) {
if (field.weight() != -1) {
if (field.type() != IndexType.TEXT) {
throw new MappingException("Weight values only apply to text indexes: " + Arrays.toString(index.fields()));
}
weights.put(field.value(), field.weight());
}
}
if (!weights.isEmpty()) {
indexOptions.weights(weights);
}
}
Index convert(final Text text, final String nameToStore) {
return new IndexBuilder()
.options(text.options())
.fields(Collections.<Field>singletonList(new FieldBuilder()
.value(nameToStore)
.type(IndexType.TEXT)
.weight(text.value())));
}
@SuppressWarnings("deprecation")
Index convert(final Indexed indexed, final String nameToStore) {
if (indexed.dropDups() || indexed.options().dropDups()) {
LOG.warning("Support for dropDups has been removed from the server. Please remove this setting.");
}
final Map<String, Object> newOptions = extractOptions(indexed.options());
if (!extractOptions(indexed).isEmpty() && !newOptions.isEmpty()) {
throw new MappingException("Mixed usage of deprecated @Indexed values with the new @IndexOption values is not "
+ "allowed. Please migrate all settings to @IndexOptions");
}
List<Field> fields = Collections.<Field>singletonList(new FieldBuilder()
.value(nameToStore)
.type(fromValue(indexed.value().toIndexValue())));
return newOptions.isEmpty()
? new IndexBuilder()
.options(new IndexOptionsBuilder()
.migrate(indexed))
.fields(fields)
: new IndexBuilder()
.options(indexed.options())
.fields(fields);
}
@SuppressWarnings("deprecation")
private List<Index> collectFieldIndexes(final MappedClass mc) {
List<Index> list = new ArrayList<Index>();
for (final MappedField mf : mc.getPersistenceFields()) {
if (mf.hasAnnotation(Indexed.class)) {
final Indexed indexed = mf.getAnnotation(Indexed.class);
list.add(convert(indexed, mf.getNameToStore()));
} else if (mf.hasAnnotation(Text.class)) {
final Text text = mf.getAnnotation(Text.class);
list.add(convert(text, mf.getNameToStore()));
}
}
return list;
}
private List<Index> collectIndexes(final MappedClass mc, final List<MappedClass> parentMCs) {
if (parentMCs.contains(mc) || mc.getEmbeddedAnnotation() != null && parentMCs.isEmpty()) {
return emptyList();
}
List<Index> indexes = collectTopLevelIndexes(mc);
indexes.addAll(collectFieldIndexes(mc));
indexes.addAll(collectNestedIndexes(mc, parentMCs));
return indexes;
}
private List<Index> collectNestedIndexes(final MappedClass mc, final List<MappedClass> parentMCs) {
List<Index> list = new ArrayList<Index>();
for (final MappedField mf : mc.getPersistenceFields()) {
if (!mf.isTypeMongoCompatible() && !mf.hasAnnotation(Reference.class) && !mf.hasAnnotation(Serialized.class)
&& !mf.hasAnnotation(NotSaved.class) && !mf.isTransient()) {
final List<MappedClass> parents = new ArrayList<MappedClass>(parentMCs);
parents.add(mc);
List<MappedClass> classes = new ArrayList<MappedClass>();
MappedClass mappedClass = mapper.getMappedClass(mf.isSingleValue() ? mf.getType() : mf.getSubClass());
classes.add(mappedClass);
classes.addAll(mapper.getSubTypes(mappedClass));
for (MappedClass aClass : classes) {
for (Index index : collectIndexes(aClass, parents)) {
List<Field> fields = new ArrayList<Field>();
for (Field field : index.fields()) {
fields.add(new FieldBuilder()
.value(field.value().equals("$**")
? field.value()
: mf.getNameToStore() + "." + field.value())
.type(field.type())
.weight(field.weight()));
}
list.add(new IndexBuilder(index)
.fields(fields));
}
}
}
}
return list;
}
private List<Index> collectTopLevelIndexes(final MappedClass mc) {
List<Index> list = new ArrayList<Index>();
if (mc != null) {
final List<Indexes> annotations = mc.getAnnotations(Indexes.class);
if (annotations != null) {
for (final Indexes indexes : annotations) {
for (final Index index : indexes.value()) {
Index updated = index;
if (index.fields().length == 0) {
LOG.warning(format("This index on '%s' is using deprecated configuration options. Please update to use the "
+ "fields value on @Index: %s", mc.getClazz().getName(), index.toString()));
updated = new IndexBuilder()
.migrate(index);
}
List<Field> fields = new ArrayList<Field>();
for (Field field : updated.fields()) {
fields.add(new FieldBuilder()
.value(findField(mc, index.options(), asList(field.value().split("\\."))))
.type(field.type())
.weight(field.weight()));
}
list.add(replaceFields(updated, fields));
}
}
}
list.addAll(collectTopLevelIndexes(mc.getSuperClass()));
}
return list;
}
private Map<String, Object> (final IndexOptions options) {
return toMap(options);
}
private Map<String, Object> (final Indexed indexed) {
Map<String, Object> map = toMap(indexed);
if (indexed.options().collation().locale().equals("")) {
map.remove("options");
}
map.remove("value");
return map;
}
private MappingException pathFail(final MappedClass mc, final List<String> path) {
return new MappingException(format("Could not resolve path '%s' against '%s'.", join(path, '.'), mc.getClazz().getName()));
}
private Index replaceFields(final Index original, final List<Field> list) {
return new IndexBuilder(original)
.fields(list);
}
@SuppressWarnings("unchecked")
private BsonDocument toBsonDocument(final String key, final Object value) {
BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
writer.writeStartDocument();
writer.writeName(key);
((Encoder) database.getCodecRegistry().get(value.getClass())).encode(writer, value, ENCODER_CONTEXT);
writer.writeEndDocument();
return writer.getDocument();
}
BsonDocument calculateKeys(final MappedClass mc, final Index index) {
BsonDocument keys = new BsonDocument();
for (Field field : index.fields()) {
String path;
try {
path = findField(mc, index.options(), new ArrayList<String>(asList(field.value().split("\\."))));
} catch (Exception e) {
path = field.value();
String message = format("The path '%s' can not be validated against '%s' and may represent an invalid index",
path, mc.getClazz().getName());
if (!index.options().disableValidation()) {
throw new MappingException(message);
}
LOG.warning(message);
}
keys.putAll(toBsonDocument(path, field.type().toIndexValue()));
}
return keys;
}
@SuppressWarnings("deprecation")
com.mongodb.client.model.IndexOptions convert(final IndexOptions options, final boolean background) {
if (options.dropDups()) {
LOG.warning("Support for dropDups has been removed from the server. Please remove this setting.");
}
com.mongodb.client.model.IndexOptions indexOptions = new com.mongodb.client.model.IndexOptions()
.background(options.background() || background)
.sparse(options.sparse())
.unique(options.unique());
if (!options.language().equals("")) {
indexOptions.defaultLanguage(options.language());
}
if (!options.languageOverride().equals("")) {
indexOptions.languageOverride(options.languageOverride());
}
if (!options.name().equals("")) {
indexOptions.name(options.name());
}
if (options.expireAfterSeconds() != -1) {
indexOptions.expireAfter((long) options.expireAfterSeconds(), TimeUnit.SECONDS);
}
if (!options.partialFilter().equals("")) {
indexOptions.partialFilterExpression(Document.parse(options.partialFilter()));
}
if (!options.collation().locale().equals("")) {
indexOptions.collation(convert(options.collation()));
}
return indexOptions;
}
com.mongodb.client.model.Collation convert(final Collation collation) {
return com.mongodb.client.model.Collation.builder()
.locale(collation.locale())
.backwards(collation.backwards())
.caseLevel(collation.caseLevel())
.collationAlternate(collation.alternate())
.collationCaseFirst(collation.caseFirst())
.collationMaxVariable(collation.maxVariable())
.collationStrength(collation.strength())
.normalization(collation.normalization())
.numericOrdering(collation.numericOrdering())
.build();
}
String findField(final MappedClass mc, final IndexOptions options, final List<String> path) {
String segment = path.get(0);
if (segment.equals("$**")) {
return segment;
}
MappedField mf = mc.getMappedField(segment);
if (mf == null) {
mf = mc.getMappedFieldByJavaField(segment);
}
if (mf == null && mc.isInterface()) {
for (final MappedClass mappedClass : mapper.getSubTypes(mc)) {
try {
return findField(mappedClass, options, new ArrayList<String>(path));
} catch (MappingException e) {
}
}
}
String namePath;
if (mf != null) {
namePath = mf.getNameToStore();
} else {
if (!options.disableValidation()) {
throw pathFail(mc, path);
} else {
return join(path, '.');
}
}
if (path.size() > 1) {
try {
Class concreteType = !mf.isSingleValue() ? mf.getSubClass() : mf.getConcreteType();
namePath += "." + findField(mapper.getMappedClass(concreteType), options, path.subList(1, path.size()));
} catch (MappingException e) {
if (!options.disableValidation()) {
throw pathFail(mc, path);
} else {
return join(path, '.');
}
}
}
return namePath;
}
void createIndex(final MongoCollection collection, final MappedClass mc, final boolean background) {
if (!mc.isInterface() && !mc.isAbstract()) {
for (Index index : collectIndexes(mc, Collections.<MappedClass>emptyList())) {
createIndex(collection, mc, index, background);
}
}
}
void createIndex(final MongoCollection collection, final MappedClass mc, final Index index, final boolean background) {
Index normalized = IndexBuilder.normalize(index);
BsonDocument keys = calculateKeys(mc, normalized);
com.mongodb.client.model.IndexOptions indexOptions = convert(normalized.options(), background);
calculateWeights(normalized, indexOptions);
collection.createIndex(keys, indexOptions);
}
}