/* * Copyright 2010-2017 the original author or authors. * * 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.springframework.data.mongodb.core.query; import static org.springframework.util.ObjectUtils.*; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.bson.Document; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Class to easily construct MongoDB update clauses. * * @author Thomas Risberg * @author Mark Pollack * @author Oliver Gierke * @author Becca Gaspard * @author Christoph Strobl * @author Thomas Darimont * @author Alexey Plotnik * @author Mark Paluch * @author Pavel Vodrazka */ public class Update { public enum Position { LAST, FIRST } private boolean isolated = false; private Set<String> keysToUpdate = new HashSet<String>(); private Map<String, Object> modifierOps = new LinkedHashMap<String, Object>(); private Map<String, PushOperatorBuilder> pushCommandBuilders = new LinkedHashMap<String, PushOperatorBuilder>(1); /** * Static factory method to create an Update using the provided key * * @param key * @return */ public static Update update(String key, Object value) { return new Update().set(key, value); } /** * Creates an {@link Update} instance from the given {@link Document}. Allows to explicitly exclude fields from making * it into the created {@link Update} object. Note, that this will set attributes directly and <em>not</em> use * {@literal $set}. This means fields not given in the {@link Document} will be nulled when executing the update. To * create an only-updating {@link Update} instance of a {@link Document}, call {@link #set(String, Object)} for each * value in it. * * @param object the source {@link Document} to create the update from. * @param exclude the fields to exclude. * @return */ public static Update fromDocument(Document object, String... exclude) { Update update = new Update(); List<String> excludeList = Arrays.asList(exclude); for (String key : object.keySet()) { if (excludeList.contains(key)) { continue; } Object value = object.get(key); update.modifierOps.put(key, value); if (isKeyword(key) && value instanceof Document) { update.keysToUpdate.addAll(((Document) value).keySet()); } else { update.keysToUpdate.add(key); } } return update; } /** * Update using the {@literal $set} update modifier * * @param key * @param value * @return * @see <a href="https://docs.mongodb.com/manual/reference/operator/update/set/">MongoDB Update operator: $set</a> */ public Update set(String key, Object value) { addMultiFieldOperation("$set", key, value); return this; } /** * Update using the {@literal $setOnInsert} update modifier * * @param key * @param value * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/setOnInsert/">MongoDB Update operator: $setOnInsert</a> */ public Update setOnInsert(String key, Object value) { addMultiFieldOperation("$setOnInsert", key, value); return this; } /** * Update using the {@literal $unset} update modifier * * @param key * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/unset/">MongoDB Update operator: $unset</a> */ public Update unset(String key) { addMultiFieldOperation("$unset", key, 1); return this; } /** * Update using the {@literal $inc} update modifier * * @param key * @param inc * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/inc/">MongoDB Update operator: $inc</a> */ public Update inc(String key, Number inc) { addMultiFieldOperation("$inc", key, inc); return this; } /** * Update using the {@literal $push} update modifier * * @param key * @param value * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/push/">MongoDB Update operator: $push</a> */ public Update push(String key, Object value) { addMultiFieldOperation("$push", key, value); return this; } /** * Update using {@code $push} modifier. <br/> * Allows creation of {@code $push} command for single or multiple (using {@code $each}) values as well as using * {@code $position}. * * @param key * @return {@link PushOperatorBuilder} for given key * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/push/">MongoDB Update operator: $push</a> * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/each/">MongoDB Update operator: $each</a> */ public PushOperatorBuilder push(String key) { if (!pushCommandBuilders.containsKey(key)) { pushCommandBuilders.put(key, new PushOperatorBuilder(key)); } return pushCommandBuilders.get(key); } /** * Update using the {@code $pushAll} update modifier. <br> * <b>Note</b>: In mongodb 2.4 the usage of {@code $pushAll} has been deprecated in favor of {@code $push $each}. * {@link #push(String)}) returns a builder that can be used to populate the {@code $each} object. * * @param key * @param values * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pushAll/">MongoDB Update operator: $pushAll</a> */ public Update pushAll(String key, Object[] values) { addMultiFieldOperation("$pushAll", key, Arrays.asList(values)); return this; } /** * Update using {@code $addToSet} modifier. <br/> * Allows creation of {@code $push} command for single or multiple (using {@code $each}) values * * @param key * @return * @since 1.5 */ public AddToSetBuilder addToSet(String key) { return new AddToSetBuilder(key); } /** * Update using the {@literal $addToSet} update modifier * * @param key * @param value * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/addToSet/">MongoDB Update operator: $addToSet</a> */ public Update addToSet(String key, Object value) { addMultiFieldOperation("$addToSet", key, value); return this; } /** * Update using the {@literal $pop} update modifier * * @param key * @param pos * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pop/">MongoDB Update operator: $pop</a> */ public Update pop(String key, Position pos) { addMultiFieldOperation("$pop", key, pos == Position.FIRST ? -1 : 1); return this; } /** * Update using the {@literal $pull} update modifier * * @param key * @param value * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pull/">MongoDB Update operator: $pull</a> */ public Update pull(String key, Object value) { addMultiFieldOperation("$pull", key, value); return this; } /** * Update using the {@literal $pullAll} update modifier * * @param key * @param values * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/pullAll/">MongoDB Update operator: $pullAll</a> */ public Update pullAll(String key, Object[] values) { addMultiFieldOperation("$pullAll", key, Arrays.asList(values)); return this; } /** * Update using the {@literal $rename} update modifier * * @param oldName * @param newName * @return * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/rename/">MongoDB Update operator: $rename</a> */ public Update rename(String oldName, String newName) { addMultiFieldOperation("$rename", oldName, newName); return this; } /** * Update given key to current date using {@literal $currentDate} modifier. * * @param key * @return * @since 1.6 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/currentDate/">MongoDB Update operator: $currentDate</a> */ public Update currentDate(String key) { addMultiFieldOperation("$currentDate", key, true); return this; } /** * Update given key to current date using {@literal $currentDate : { $type : "timestamp" }} modifier. * * @param key * @return * @since 1.6 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/currentDate/">MongoDB Update operator: $currentDate</a> */ public Update currentTimestamp(String key) { addMultiFieldOperation("$currentDate", key, new Document("$type", "timestamp")); return this; } /** * Multiply the value of given key by the given number. * * @param key must not be {@literal null}. * @param multiplier must not be {@literal null}. * @return * @since 1.7 * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/mul/">MongoDB Update operator: $mul</a> */ public Update multiply(String key, Number multiplier) { Assert.notNull(multiplier, "Multiplier must not be null."); addMultiFieldOperation("$mul", key, multiplier.doubleValue()); return this; } /** * Update given key to the {@code value} if the {@code value} is greater than the current value of the field. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @return * @since 1.10 * @see <a href="https://docs.mongodb.com/manual/reference/bson-type-comparison-order/">Comparison/Sort Order</a> * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/max/">MongoDB Update operator: $max</a> */ public Update max(String key, Object value) { Assert.notNull(value, "Value for max operation must not be null."); addMultiFieldOperation("$max", key, value); return this; } /** * Update given key to the {@code value} if the {@code value} is less than the current value of the field. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @return * @since 1.10 * @see <a href="https://docs.mongodb.com/manual/reference/bson-type-comparison-order/">Comparison/Sort Order</a> * @see <a href="https://docs.mongodb.org/manual/reference/operator/update/min/">MongoDB Update operator: $min</a> */ public Update min(String key, Object value) { Assert.notNull(value, "Value for min operation must not be null."); addMultiFieldOperation("$min", key, value); return this; } /** * The operator supports bitwise {@code and}, bitwise {@code or}, and bitwise {@code xor} operations. * * @param key * @return * @since 1.7 */ public BitwiseOperatorBuilder bitwise(String key) { return new BitwiseOperatorBuilder(this, key); } /** * Prevents a write operation that affects <strong>multiple</strong> documents from yielding to other reads or writes * once the first document is written. <br /> * Use with {@link org.springframework.data.mongodb.core.MongoOperations#updateMulti(Query, Update, Class)}. * * @return never {@literal null}. * @since 2.0 */ public Update isolated() { isolated = true; return this; } /** * @return {@literal true} if update isolated is set. * @since 2.0 */ public Boolean isIsolated() { return isolated; } public Document getUpdateObject() { return new Document(modifierOps); } /** * This method is not called anymore rather override {@link #addMultiFieldOperation(String, String, Object)}. * * @param operator * @param key * @param value * @deprectaed Use {@link #addMultiFieldOperation(String, String, Object)} instead. */ @Deprecated protected void addFieldOperation(String operator, String key, Object value) { Assert.hasText(key, "Key/Path for update must not be null or blank."); modifierOps.put(operator, new Document(key, value)); this.keysToUpdate.add(key); } protected void addMultiFieldOperation(String operator, String key, Object value) { Assert.hasText(key, "Key/Path for update must not be null or blank."); Object existingValue = this.modifierOps.get(operator); Document keyValueMap; if (existingValue == null) { keyValueMap = new Document(); this.modifierOps.put(operator, keyValueMap); } else { if (existingValue instanceof Document) { keyValueMap = (Document) existingValue; } else { throw new InvalidDataAccessApiUsageException( "Modifier Operations should be a LinkedHashMap but was " + existingValue.getClass()); } } keyValueMap.put(key, value); this.keysToUpdate.add(key); } /** * Determine if a given {@code key} will be touched on execution. * * @param key * @return */ public boolean modifies(String key) { return this.keysToUpdate.contains(key); } /** * Inspects given {@code key} for '$'. * * @param key * @return */ private static boolean isKeyword(String key) { return StringUtils.startsWithIgnoreCase(key, "$"); } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return getUpdateObject().hashCode(); } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Update that = (Update) obj; return this.getUpdateObject().equals(that.getUpdateObject()); } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return SerializationUtils.serializeToJsonSafely(getUpdateObject()); } /** * Modifiers holds a distinct collection of {@link Modifier} * * @author Christoph Strobl * @author Thomas Darimont */ public static class Modifiers { private Map<String, Modifier> modifiers; public Modifiers() { this.modifiers = new LinkedHashMap<String, Modifier>(1); } public Collection<Modifier> getModifiers() { return Collections.unmodifiableCollection(this.modifiers.values()); } public void addModifier(Modifier modifier) { this.modifiers.put(modifier.getKey(), modifier); } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return nullSafeHashCode(modifiers); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Modifiers that = (Modifiers) obj; return this.modifiers.equals(that.modifiers); } } /** * Marker interface of nested commands. * * @author Christoph Strobl */ public static interface Modifier { /** * @return the command to send eg. {@code $push} */ String getKey(); /** * @return value to be sent with command */ Object getValue(); } /** * Implementation of {@link Modifier} representing {@code $each}. * * @author Christoph Strobl * @author Thomas Darimont */ private static class Each implements Modifier { private Object[] values; public Each(Object... values) { this.values = extractValues(values); } private Object[] extractValues(Object[] values) { if (values == null || values.length == 0) { return values; } if (values.length == 1 && values[0] instanceof Collection) { return ((Collection<?>) values[0]).toArray(); } return Arrays.copyOf(values, values.length); } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getKey() */ @Override public String getKey() { return "$each"; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getValue() */ @Override public Object getValue() { return this.values; } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return nullSafeHashCode(values); } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null || getClass() != that.getClass()) { return false; } return nullSafeEquals(values, ((Each) that).values); } } /** * {@link Modifier} implementation used to propagate {@code $position}. * * @author Christoph Strobl * @since 1.7 */ private static class PositionModifier implements Modifier { private final int position; public PositionModifier(int position) { this.position = position; } @Override public String getKey() { return "$position"; } @Override public Object getValue() { return position; } } /** * Implementation of {@link Modifier} representing {@code $slice}. * * @author Mark Paluch * @since 1.10 */ private static class Slice implements Modifier { private int count; public Slice(int count) { this.count = count; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getKey() */ @Override public String getKey() { return "$slice"; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getValue() */ @Override public Object getValue() { return this.count; } } /** * Implementation of {@link Modifier} representing {@code $sort}. * * @author Pavel Vodrazka * @author Mark Paluch * @since 1.10 */ private static class SortModifier implements Modifier { private final Object sort; /** * Creates a new {@link SortModifier} instance given {@link Direction}. * * @param direction must not be {@literal null}. */ public SortModifier(Direction direction) { Assert.notNull(direction, "Direction must not be null!"); this.sort = direction.isAscending() ? 1 : -1; } /** * Creates a new {@link SortModifier} instance given {@link Sort}. * * @param sort must not be {@literal null}. */ public SortModifier(Sort sort) { Assert.notNull(sort, "Sort must not be null!"); for (Order order : sort) { if (order.isIgnoreCase()) { throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! " + "MongoDB does not support sorting ignoring case currently!", order.getProperty())); } } this.sort = sort; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getKey() */ @Override public String getKey() { return "$sort"; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.query.Update.Modifier#getValue() */ @Override public Object getValue() { return this.sort; } } /** * Builder for creating {@code $push} modifiers * * @author Christoph Strobl * @author Thomas Darimont */ public class PushOperatorBuilder { private final String key; private final Modifiers modifiers; PushOperatorBuilder(String key) { this.key = key; this.modifiers = new Modifiers(); } /** * Propagates {@code $each} to {@code $push} * * @param values * @return never {@literal null}. */ public Update each(Object... values) { this.modifiers.addModifier(new Each(values)); return Update.this.push(key, this.modifiers); } /** * Propagates {@code $slice} to {@code $push}. {@code $slice} requires the {@code $each operator}. <br /> * If {@literal count} is zero, {@code $slice} updates the array to an empty array. <br /> * If {@literal count} is negative, {@code $slice} updates the array to contain only the last {@code count} * elements. <br /> * If {@literal count} is positive, {@code $slice} updates the array to contain only the first {@code count} * elements. <br /> * * @param count * @return never {@literal null}. * @since 1.10 */ public PushOperatorBuilder slice(int count) { this.modifiers.addModifier(new Slice(count)); return this; } /** * Propagates {@code $sort} to {@code $push}. {@code $sort} requires the {@code $each} operator. Forces elements to * be sorted by values in given {@literal direction}. * * @param direction must not be {@literal null}. * @return never {@literal null}. * @since 1.10 */ public PushOperatorBuilder sort(Direction direction) { Assert.notNull(direction, "Direction must not be null."); this.modifiers.addModifier(new SortModifier(direction)); return this; } /** * Propagates {@code $sort} to {@code $push}. {@code $sort} requires the {@code $each} operator. Forces document * elements to be sorted in given {@literal order}. * * @param sort must not be {@literal null}. * @return never {@literal null}. * @since 1.10 */ public PushOperatorBuilder sort(Sort sort) { Assert.notNull(sort, "Sort must not be null."); this.modifiers.addModifier(new SortModifier(sort)); return this; } /** * Forces values to be added at the given {@literal position}. * * @param position needs to be greater than or equal to zero. * @return never {@literal null}. * @since 1.7 */ public PushOperatorBuilder atPosition(int position) { if (position < 0) { throw new IllegalArgumentException("Position must be greater than or equal to zero."); } this.modifiers.addModifier(new PositionModifier(position)); return this; } /** * Forces values to be added at given {@literal position}. * * @param position can be {@literal null} which will be appended at the last position. * @return never {@literal null}. * @since 1.7 */ public PushOperatorBuilder atPosition(Position position) { if (position == null || Position.LAST.equals(position)) { return this; } this.modifiers.addModifier(new PositionModifier(0)); return this; } /** * Propagates {@link #value(Object)} to {@code $push} * * @param values * @return never {@literal null}. */ public Update value(Object value) { return Update.this.push(key, value); } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { int result = 17; result += 31 * result + getOuterType().hashCode(); result += 31 * result + nullSafeHashCode(key); result += 31 * result + nullSafeHashCode(modifiers); return result; } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } PushOperatorBuilder that = (PushOperatorBuilder) obj; if (!getOuterType().equals(that.getOuterType())) { return false; } return nullSafeEquals(this.key, that.key) && nullSafeEquals(this.modifiers, that.modifiers); } private Update getOuterType() { return Update.this; } } /** * Builder for creating {@code $addToSet} modifier. * * @author Christoph Strobl * @since 1.5 */ public class AddToSetBuilder { private final String key; public AddToSetBuilder(String key) { this.key = key; } /** * Propagates {@code $each} to {@code $addToSet} * * @param values * @return */ public Update each(Object... values) { return Update.this.addToSet(this.key, new Each(values)); } /** * Propagates {@link #value(Object)} to {@code $addToSet} * * @param values * @return */ public Update value(Object value) { return Update.this.addToSet(this.key, value); } } /** * @author Christoph Strobl * @since 1.7 */ public static class BitwiseOperatorBuilder { private final String key; private final Update reference; private static final String BIT_OPERATOR = "$bit"; private enum BitwiseOperator { AND, OR, XOR; @Override public String toString() { return super.toString().toLowerCase(); }; } /** * Creates a new {@link BitwiseOperatorBuilder}. * * @param reference must not be {@literal null} * @param key must not be {@literal null} */ protected BitwiseOperatorBuilder(Update reference, String key) { Assert.notNull(reference, "Reference must not be null!"); Assert.notNull(key, "Key must not be null!"); this.reference = reference; this.key = key; } /** * Updates to the result of a bitwise and operation between the current value and the given one. * * @param value * @return */ public Update and(long value) { addFieldOperation(BitwiseOperator.AND, value); return reference; } /** * Updates to the result of a bitwise or operation between the current value and the given one. * * @param value * @return */ public Update or(long value) { addFieldOperation(BitwiseOperator.OR, value); return reference; } /** * Updates to the result of a bitwise xor operation between the current value and the given one. * * @param value * @return */ public Update xor(long value) { addFieldOperation(BitwiseOperator.XOR, value); return reference; } private void addFieldOperation(BitwiseOperator operator, Number value) { reference.addMultiFieldOperation(BIT_OPERATOR, key, new Document(operator.toString(), value)); } } }