/*
* 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.language;
import com.eightkdata.mongowp.bson.BsonDocument;
import com.eightkdata.mongowp.bson.BsonDocument.Entry;
import com.eightkdata.mongowp.bson.BsonValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.torodb.core.exceptions.user.UpdateException;
import com.torodb.core.language.AttributeReference;
import com.torodb.kvdocument.conversion.mongowp.MongoWpConverter;
import com.torodb.kvdocument.values.KvDocument;
import com.torodb.kvdocument.values.KvNumeric;
import com.torodb.kvdocument.values.KvValue;
import com.torodb.mongodb.language.update.CompositeUpdateAction;
import com.torodb.mongodb.language.update.IncrementUpdateAction;
import com.torodb.mongodb.language.update.MoveUpdateAction;
import com.torodb.mongodb.language.update.MultiplyUpdateAction;
import com.torodb.mongodb.language.update.SetDocumentUpdateAction;
import com.torodb.mongodb.language.update.SetFieldUpdateAction;
import com.torodb.mongodb.language.update.UnsetFieldUpdateAction;
import com.torodb.mongodb.language.update.UpdateAction;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.annotation.Nonnull;
/**
*
*/
public class UpdateActionTranslator {
UpdateActionTranslator() {
}
public static UpdateAction translate(BsonDocument updateObject) throws UpdateException {
if (isReplaceUpdate(updateObject)) {
return translateReplaceObject(updateObject);
} else {
return translateComposedUpdate(updateObject);
}
}
private static boolean isUpdateOperator(String key) {
return UpdateOperator.isSubQuery(key);
}
private static boolean isReplaceUpdate(BsonDocument updateObject) {
for (Entry<?> entry : updateObject) {
if (isUpdateOperator(entry.getKey())) {
return false;
}
}
return true;
}
private static UpdateAction translateReplaceObject(BsonDocument updateObject) {
return new SetDocumentUpdateAction(
(KvDocument) MongoWpConverter.translate(updateObject));
}
private static UpdateAction translateComposedUpdate(BsonDocument updateObject) throws
UpdateException {
CompositeUpdateAction.Builder builder =
new CompositeUpdateAction.Builder();
for (Entry<?> entry : updateObject) {
String key = entry.getKey();
if (!UpdateOperator.isSubQuery(key)) {
throw new UpdateException("Unknown modifier: " + key);
}
translateSingleOperation(
builder,
UpdateOperator.fromKey(key),
entry.getValue()
);
}
return builder.build();
}
private static void translateSingleOperation(
CompositeUpdateAction.Builder builder,
UpdateOperator key,
BsonValue<?> value
) throws UpdateException {
if (!value.isDocument()) {
throw new UpdateException("Modifiers operate on fields but found "
+ "a " + value + " instead.");
}
BsonDocument castedValue = value.asDocument();
switch (key) {
case INCREMENT: {
translateIncrement(builder, castedValue);
break;
}
case MOVE: {
translateMove(builder, castedValue);
break;
}
case MULTIPLY: {
translateMultiply(builder, castedValue);
break;
}
case SET_CURRENT_DATE: {
translateSetCurrentDate(builder, castedValue);
break;
}
case SET_FIELD: {
translateSetField(builder, castedValue);
break;
}
case UNSET_FIELD: {
translateUnsetField(builder, castedValue);
break;
}
default:
}
}
private static AttributeReference parseAttributReferenceAsObjectReference(
String key) {
ImmutableList.Builder<AttributeReference.Key<?>> newKeysBuilder =
ImmutableList.<AttributeReference.Key<?>>builder();
StringTokenizer st = new StringTokenizer(key, ".");
while (st.hasMoreTokens()) {
newKeysBuilder.add(new AttributeReference.ObjectKey(st.nextToken()));
}
return new AttributeReference(newKeysBuilder.build());
}
private static Collection<AttributeReference> parseAttributeReference(
String key) {
String[] tokens = key.split("\\.");
List<AttributeReference.Key<?>> keys = Lists.newArrayList();
List<AttributeReference> attRefs = Lists.newArrayList();
parseAttributeReference(tokens, 0, keys, attRefs);
return attRefs;
}
private static void parseAttributeReference(
String[] tokens,
int depth,
List<AttributeReference.Key<?>> keys,
Collection<AttributeReference> attRefs) {
if (tokens.length == depth) {
attRefs.add(new AttributeReference(keys));
} else {
String nextKey = tokens[depth];
keys.add(new AttributeReference.ObjectKey(nextKey));
parseAttributeReference(tokens, depth + 1, keys, attRefs);
keys.remove(keys.size() - 1);
try {
int keyAsInt = Integer.parseInt(nextKey);
keys.add(new AttributeReference.ArrayKey(keyAsInt));
parseAttributeReference(tokens, depth + 1, keys, attRefs);
keys.remove(keys.size() - 1);
} catch (NumberFormatException ex) { //the key is not an array key candidate
}
}
}
private static void translateIncrement(
CompositeUpdateAction.Builder builder, BsonDocument argument) throws UpdateException {
for (Entry<?> entry : argument) {
Collection<AttributeReference> attRefs =
parseAttributeReference(entry.getKey());
KvValue<?> translatedValue = MongoWpConverter.translate(entry.getValue());
if (!(translatedValue instanceof KvNumeric)) {
throw new UpdateException("Cannot increment with a "
+ "non-numeric argument");
}
builder.add(new IncrementUpdateAction(
attRefs,
(KvNumeric<?>) translatedValue
), false
);
}
}
private static void translateMove(
CompositeUpdateAction.Builder builder, BsonDocument argument) throws UpdateException {
for (Entry<?> entry : argument) {
Collection<AttributeReference> attRefs =
parseAttributeReference(entry.getKey());
if (!entry.getValue().isString()) {
throw new UpdateException("The 'to' field for $rename must "
+ "be a string, but " + entry.getValue() + " were found "
+ "with key " + entry.getKey());
}
AttributeReference newRef =
parseAttributReferenceAsObjectReference(
entry.getValue().asString().getValue()
);
builder.add(
new MoveUpdateAction(
attRefs,
newRef
), false
);
}
}
private static void translateMultiply(
CompositeUpdateAction.Builder builder, BsonDocument argument) throws UpdateException {
for (Entry<?> entry : argument) {
Collection<AttributeReference> attRefs =
parseAttributeReference(entry.getKey());
KvValue<?> translatedValue =
MongoWpConverter.translate(entry.getValue());
if (!(translatedValue instanceof KvNumeric)) {
throw new UpdateException("Cannot multiply with a "
+ "non-numeric argument");
}
builder.add(new MultiplyUpdateAction(
attRefs,
(KvNumeric<?>) translatedValue
), false
);
}
}
private static void translateSetCurrentDate(
CompositeUpdateAction.Builder builder, BsonDocument argument) {
throw new UnsupportedOperationException("Not supported yet.");
}
private static void translateSetField(
CompositeUpdateAction.Builder builder, BsonDocument argument) {
for (Entry<?> entry : argument) {
Collection<AttributeReference> attRefs =
parseAttributeReference(entry.getKey());
KvValue<?> translatedValue =
MongoWpConverter.translate(entry.getValue());
builder.add(
new SetFieldUpdateAction(
attRefs,
translatedValue
), false
);
}
}
private static void translateUnsetField(
CompositeUpdateAction.Builder builder, BsonDocument argument) {
for (Entry<?> entry : argument) {
Collection<AttributeReference> attRefs =
parseAttributeReference(entry.getKey());
builder.add(new UnsetFieldUpdateAction(attRefs), false);
}
}
private static enum UpdateOperator {
INCREMENT("$inc"),
MOVE("$rename"),
MULTIPLY("$mul"),
SET_CURRENT_DATE("$currentDate"),
SET_FIELD("$set"),
UNSET_FIELD("$unset");
private static final Map<String, UpdateOperator> operandsByKey;
private final String key;
static {
operandsByKey = Maps.newHashMapWithExpectedSize(UpdateOperator.values().length);
for (UpdateOperator operand : UpdateOperator.values()) {
operandsByKey.put(operand.key, operand);
}
}
private UpdateOperator(String key) {
this.key = key;
}
public static boolean isSubQuery(String key) {
return operandsByKey.containsKey(key);
}
@Nonnull
public static UpdateOperator fromKey(String key) {
UpdateOperator result = operandsByKey.get(key);
if (result == null) {
throw new IllegalArgumentException("There is no operand whose "
+ "key is '" + key + "'");
}
return result;
}
}
}