/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.mongodb.commands.pojos.index; import com.eightkdata.mongowp.bson.BsonDocument; import com.eightkdata.mongowp.bson.BsonDocument.Entry; import com.eightkdata.mongowp.bson.BsonValue; import com.eightkdata.mongowp.bson.utils.DefaultBsonValues; import com.eightkdata.mongowp.exceptions.BadValueException; import com.eightkdata.mongowp.exceptions.NoSuchKeyException; import com.eightkdata.mongowp.exceptions.TypesMismatchException; import com.eightkdata.mongowp.fields.BooleanField; import com.eightkdata.mongowp.fields.DocField; import com.eightkdata.mongowp.fields.IntField; import com.eightkdata.mongowp.fields.NumberField; import com.eightkdata.mongowp.fields.StringField; import com.eightkdata.mongowp.utils.BsonDocumentBuilder; import com.eightkdata.mongowp.utils.BsonReaderTool; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.torodb.mongodb.commands.pojos.index.type.AscIndexType; import com.torodb.mongodb.commands.pojos.index.type.DescIndexType; import com.torodb.mongodb.commands.pojos.index.type.GeoHaystackIndexType; import com.torodb.mongodb.commands.pojos.index.type.HashedIndexType; import com.torodb.mongodb.commands.pojos.index.type.IndexType; import com.torodb.mongodb.commands.pojos.index.type.TextIndexType; import com.torodb.mongodb.commands.pojos.index.type.TwoDIndexType; import com.torodb.mongodb.commands.pojos.index.type.UnknownIndexType; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class IndexOptions { private static final String VERSION_FIELD_NAME = "v"; private static final String NAME_FIELD_NAME = "name"; private static final String NAMESPACE_FIELD_NAME = "ns"; private static final String BACKGROUND_FIELD_NAME = "background"; private static final String UNIQUE_FIELD_NAME = "unique"; private static final String SPARSE_FIELD_NAME = "sparse"; private static final String EXPIRE_AFTER_SECONDS_FIELD_NAME = "expireAfterSeconds"; private static final String KEYS_FIELD_NAME = "key"; private static final String STORAGE_ENGINE_FIELD_NAME = "storageEngine"; private static final NumberField<?> VERSION_FIELD = new NumberField<>(VERSION_FIELD_NAME); private static final StringField NAME_FIELD = new StringField(NAME_FIELD_NAME); private static final StringField NAMESPACE_FIELD = new StringField(NAMESPACE_FIELD_NAME); private static final BooleanField BACKGROUND_FIELD = new BooleanField(BACKGROUND_FIELD_NAME); private static final BooleanField UNIQUE_FIELD = new BooleanField(UNIQUE_FIELD_NAME); private static final BooleanField SPARSE_FIELD = new BooleanField(SPARSE_FIELD_NAME); private static final IntField EXPIRE_AFTER_SECONDS_FIELD = new IntField( EXPIRE_AFTER_SECONDS_FIELD_NAME); private static final DocField KEYS_FIELD = new DocField(KEYS_FIELD_NAME); private static final DocField STORAGE_ENGINE_FIELD = new DocField(STORAGE_ENGINE_FIELD_NAME); private static final Joiner PATH_JOINER = Joiner.on('.'); private static final Splitter PATH_SPLITER = Splitter.on('.'); private final IndexVersion version; private final String name; @Nullable private final String database; @Nullable private final String collection; private final boolean background; private final boolean unique; private final boolean sparse; private final int expireAfterSeconds; @Nonnull private final BsonDocument otherProps; private final List<Key> keys; @Nonnull private final BsonDocument storageEngine; public static final Function<IndexOptions, BsonDocument> MARSHALLER_FUN = new MyMarshaller(); public static final Function<BsonValue<?>, IndexOptions> UNMARSHALLER_FUN = new MyUnMarshaller(); public IndexOptions( IndexVersion version, String name, @Nullable String database, @Nullable String collection, boolean background, boolean unique, boolean sparse, int expireAfterSeconds, @Nonnull List<Key> keys, @Nullable BsonDocument storageEngine, @Nullable BsonDocument otherProps) { this.version = version; this.name = name; this.database = database; this.collection = collection; this.background = background; this.unique = unique; this.sparse = sparse; this.expireAfterSeconds = expireAfterSeconds; this.keys = keys; this.storageEngine = storageEngine != null ? storageEngine : DefaultBsonValues.EMPTY_DOC; this.otherProps = otherProps != null ? otherProps : DefaultBsonValues.EMPTY_DOC; } public IndexVersion getVersion() { return version; } public String getName() { return name; } @Nullable public String getDatabase() { return database; } @Nullable public String getCollection() { return collection; } /** * Returns a map with the indexed paths and if they are ascending or descending. * * The keys are lists of strings that represent the path of the index and values are booleans that * indicates if the index is ascending or descending. * * @return */ public List<Key> getKeys() { return Collections.unmodifiableList(keys); } public boolean isBackground() { return background; } public boolean isUnique() { return unique; } public boolean isSparse() { return sparse; } public int getExpireAfterSeconds() { return expireAfterSeconds; } @Nonnull public BsonDocument getStorageEngine() { return storageEngine; } @Nonnull public BsonDocument getOtherProps() { return otherProps; } public BsonDocument marshall() { BsonDocumentBuilder keysDoc = new BsonDocumentBuilder(); for (Key key : keys) { String path = PATH_JOINER.join(key.getKeys()); BsonValue<?> value = key.getType().toBsonValue(); keysDoc.appendUnsafe(path, value); } BsonDocumentBuilder builder = new BsonDocumentBuilder() .appendNumber(VERSION_FIELD, version.toInt()) .append(NAME_FIELD, name) .append(KEYS_FIELD, keysDoc) .append(BACKGROUND_FIELD, background) .append(UNIQUE_FIELD, unique) .append(SPARSE_FIELD, sparse) .append(EXPIRE_AFTER_SECONDS_FIELD, expireAfterSeconds); if (!storageEngine.isEmpty()) { builder.append(STORAGE_ENGINE_FIELD, storageEngine); } if (database != null && collection != null) { builder.append(NAMESPACE_FIELD, database + '.' + collection); } for (Entry<?> otherProp : otherProps) { builder.appendUnsafe(otherProp.getKey(), otherProp.getValue()); } return builder.build(); } public static IndexOptions unmarshall(BsonDocument requestDoc) throws BadValueException, TypesMismatchException, NoSuchKeyException { IndexVersion version = IndexVersion.V1; String name = null; String namespace = null; boolean background = false; boolean unique = false; boolean sparse = false; int expireAfterSeconds = 0; BsonDocument keyDoc = null; BsonDocument storageEngine = null; BsonDocumentBuilder otherBuilder = new BsonDocumentBuilder(); for (Entry<?> entry : requestDoc) { String key = entry.getKey(); switch (key) { case VERSION_FIELD_NAME: { int vInt = BsonReaderTool.getNumeric(requestDoc, VERSION_FIELD).intValue(); try { version = IndexVersion.fromInt(vInt); } catch (IndexOutOfBoundsException ex) { throw new BadValueException("Value " + vInt + " is not a valid version"); } break; } case NAME_FIELD_NAME: { name = BsonReaderTool.getString(entry); break; } case NAMESPACE_FIELD_NAME: { namespace = BsonReaderTool.getString(entry); break; } case BACKGROUND_FIELD_NAME: { background = BsonReaderTool.getBooleanOrNumeric(entry, BACKGROUND_FIELD); break; } case UNIQUE_FIELD_NAME: { unique = BsonReaderTool.getBooleanOrNumeric(entry, UNIQUE_FIELD); break; } case SPARSE_FIELD_NAME: { sparse = BsonReaderTool.getBooleanOrNumeric(entry, SPARSE_FIELD); break; } case EXPIRE_AFTER_SECONDS_FIELD_NAME: { expireAfterSeconds = BsonReaderTool.getNumeric(entry, EXPIRE_AFTER_SECONDS_FIELD .getFieldName()).intValue(); break; } case KEYS_FIELD_NAME: { keyDoc = BsonReaderTool.getDocument(entry); break; } case STORAGE_ENGINE_FIELD_NAME: { storageEngine = BsonReaderTool.getDocument(entry); break; } default: { otherBuilder.appendUnsafe(key, entry.getValue()); break; } } } String db = null; String collection = null; if (namespace != null) { int dotIndex = namespace.indexOf('.'); if (dotIndex < 1 || dotIndex > namespace.length() - 2) { throw new BadValueException("The not valid namespace " + namespace + " found"); } db = namespace.substring(0, dotIndex); collection = namespace.substring(dotIndex + 1); } if (name == null) { throw new NoSuchKeyException(NAME_FIELD_NAME, "Indexes need names"); } if (keyDoc == null) { throw new NoSuchKeyException(KEYS_FIELD_NAME, "Indexes need at least one key to index"); } List<Key> keys = unmarshllKeys(keyDoc); return new IndexOptions( version, name, db, collection, background, unique, sparse, expireAfterSeconds, keys, storageEngine, otherBuilder.build() ); } public static List<Key> unmarshllKeys(BsonDocument keyDoc) { List<Key> keys = new ArrayList<>(keyDoc.size()); for (Entry<?> entry : keyDoc) { List<String> key = PATH_SPLITER.splitToList(entry.getKey()); IndexType value = null; for (KnownType knownType : KnownType.values()) { if (knownType.getIndexType().equalsToBsonValue(entry.getValue())) { value = knownType.getIndexType(); break; } } if (value == null) { value = new UnknownIndexType(entry.getValue()); } keys.add(new Key(key, value)); } return keys; } public static enum IndexVersion { V1, V2; private static IndexVersion fromInt(int i) { return IndexVersion.values()[i]; } public int toInt() { return ordinal(); } } private static class MyMarshaller implements Function<IndexOptions, BsonDocument> { @Override public BsonDocument apply(@Nonnull IndexOptions input) { return input.marshall(); } } private static class MyUnMarshaller implements Function<BsonValue<?>, IndexOptions> { @Override public IndexOptions apply(@Nonnull BsonValue<?> input) { try { if (!input.isDocument()) { throw new IllegalArgumentException("Expected a document, " + "but a " + input.getType() + " was found"); } return IndexOptions.unmarshall(input.asDocument()); } catch (BadValueException ex) { throw new IllegalArgumentException(ex); } catch (TypesMismatchException ex) { throw new IllegalArgumentException(ex); } catch (NoSuchKeyException ex) { throw new IllegalArgumentException(ex); } } } public enum KnownType { asc(AscIndexType.INSTANCE), desc(DescIndexType.INSTANCE), text(TextIndexType.INSTANCE), twodsphere(TwoDIndexType.INSTANCE), geoHaystack(GeoHaystackIndexType.INSTANCE), twod(TwoDIndexType.INSTANCE), hashed(HashedIndexType.INSTANCE); private final IndexType indexType; private KnownType(IndexType indexType) { this.indexType = indexType; } public IndexType getIndexType() { return indexType; } public static boolean contains(IndexType indexType) { for (KnownType knownType : values()) { if (knownType.indexType == indexType) { return true; } } return false; } } public static class Key { private final List<String> keys; private final IndexType type; public Key(List<String> keys, IndexType type) { super(); this.keys = keys; this.type = type; } public List<String> getKeys() { return keys; } public IndexType getType() { return type; } } }