/* * Copyright 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.cassandra.core.query; import static org.springframework.data.cassandra.core.query.SerializationUtils.serializeToCqlSafely; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.data.cassandra.core.query.Update.AddToOp.Mode; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Update object representing representing a set of update operations. {@link Update} objects can be created in a fluent * style. Each construction operation creates a new immutable {@link Update} object. * * <pre class="code"> * Update update = Update.empty().set("foo", "bar").addTo("baz").prependAll(listOfValues); * </pre> * * @author Mark Paluch * @since 2.0 */ public class Update { private final Map<ColumnName, AssignmentOp> updateOperations; private Update(Map<ColumnName, AssignmentOp> updateOperations) { this.updateOperations = updateOperations; } /** * Create an empty {@link Update} object. * * @return a new {@link Update}. */ public static Update empty() { return new Update(Collections.emptyMap()); } /** * Create a {@link Update} object given a list of {@link AssignmentOp}s. * * @param assignmentOps must not be {@literal null}. */ public static Update of(Iterable<AssignmentOp> assignmentOps) { Assert.notNull(assignmentOps, "Update operations must not be null"); Map<ColumnName, AssignmentOp> updateOperations = assignmentOps instanceof Collection<?> ? new LinkedHashMap<>(((Collection<?>) assignmentOps).size()) : new LinkedHashMap<>(); assignmentOps.forEach(assignmentOp -> updateOperations.put(assignmentOp.getColumnName(), assignmentOp)); return new Update(updateOperations); } /** * Set the {@code columnName} to {@code value}. * * @return a new {@link Update}. */ public static Update update(String columnName, Object value) { return empty().set(columnName, value); } /** * Set the {@code columnName} to {@code value}. * * @param columnName must not be {@literal null}. * @param value value to set on column with name. * @return a new {@link Update} object containing the merge result of the existing assignments * and the current assignment. */ public Update set(String columnName, Object value) { return add(new SetOp(ColumnName.from(columnName), value)); } /** * Create a new {@link SetBuilder} to set a collection item for {@code columnName} in a fluent style. * * @param columnName must not be {@literal null}. * @return a new {@link AddToBuilder} to build an set assignment. */ public SetBuilder set(String columnName) { return new DefaultSetBuilder(ColumnName.from(columnName)); } /** * Create a new {@link AddToBuilder} to add items to a collection for {@code columnName} in a fluent style. * * @param columnName must not be {@literal null}. * @return a new {@link AddToBuilder} to build an add-to assignment. */ public AddToBuilder addTo(String columnName) { return new DefaultAddToBuilder(ColumnName.from(columnName)); } /** * Remove {@code value} from the collection at {@code columnName}. * * @param columnName must not be {@literal null}. * @param value must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update remove(String columnName, Object value) { return add(new RemoveOp(ColumnName.from(columnName), Collections.singletonList(value))); } /** * Cleat the collection at {@code columnName}. * * @param columnName must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update clear(String columnName) { return add(new SetOp(ColumnName.from(columnName), Collections.emptyList())); } /** * Increment the value at {@code columnName} by {@literal 1}. * * @param columnName must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update increment(String columnName) { return increment(columnName, 1); } /** * Increment the value at {@code columnName} by {@code delta}. * * @param columnName must not be {@literal null}. * @param delta increment value. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update increment(String columnName, Number delta) { return add(new IncrOp(ColumnName.from(columnName), delta)); } /** * Decrement the value at {@code columnName} by {@literal 1}. * * @param columnName must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update decrement(String columnName) { return decrement(columnName, 1); } /** * Decrement the value at {@code columnName} by {@code delta}. * * @param columnName must not be {@literal null}. * @param delta decrement value. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ public Update decrement(String columnName, Number delta) { double deltaValue = delta.doubleValue(); deltaValue = deltaValue > 0 ? -Math.abs(deltaValue) : deltaValue; return add(new IncrOp(ColumnName.from(columnName), deltaValue)); } /** * @return {@link Collection} of update operations. */ public Collection<AssignmentOp> getUpdateOperations() { return Collections.unmodifiableCollection(updateOperations.values()); } private Update add(AssignmentOp assignmentOp) { Map<ColumnName, AssignmentOp> map = new LinkedHashMap<>(this.updateOperations.size() + 1); map.putAll(this.updateOperations); map.put(assignmentOp.getColumnName(), assignmentOp); return new Update(map); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return StringUtils.collectionToDelimitedString(updateOperations.values(), ", "); } /** * Builder to add a single element/multiple elements to a collection associated with a {@link ColumnName}. * * @author Mark Paluch */ public interface AddToBuilder { /** * Prepend the {@code value} to the collection. * * @param value must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update prepend(Object value); /** * Prepend all {@code values} to the collection. * * @param values must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update prependAll(Object... values); /** * Prepend all {@code values} to the collection. * * @param values must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update prependAll(Iterable<? extends Object> values); /** * Append the {@code value} to the collection. * * @param value must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update append(Object value); /** * Append all {@code values} to the collection. * * @param values must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update appendAll(Object... values); /** * Append all {@code values} to the collection. * * @param values must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update appendAll(Iterable<? extends Object> values); /** * Associate the specified {@code value} with the specified {@code key} in the map. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update entry(Object key, Object value); /** * Associate all entries of the specified {@code map} with the map at {@link ColumnName}. * * @param map must not be {@literal null}. * @return a new {@link Update} object containing the merge result of the existing assignments and the current * assignment. */ Update addAll(Map<? extends Object, ? extends Object> map); } /** * Default {@link AddToBuilder} implementation. */ private class DefaultAddToBuilder implements AddToBuilder { private final ColumnName columnName; DefaultAddToBuilder(ColumnName columnName) { this.columnName = columnName; } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prepend(java.lang.Object) */ @Override public Update prepend(Object value) { return prependAll(Collections.singleton(value)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prependAll(java.lang.Object[]) */ @Override public Update prependAll(Object... values) { Assert.notNull(values, "Values must not be null"); return prependAll(Arrays.asList(values)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#prependAll(java.lang.Iterable) */ @Override public Update prependAll(Iterable<? extends Object> values) { Assert.notNull(values, "Values must not be null"); return add(new AddToOp(columnName, values, Mode.PREPEND)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#append(java.lang.Object) */ @Override public Update append(Object value) { return appendAll(Collections.singleton(value)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#appendAll(java.lang.Object[]) */ @Override public Update appendAll(Object... values) { Assert.notNull(values, "Values must not be null"); return appendAll(Arrays.asList(values)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#appendAll(java.lang.Iterable) */ @Override public Update appendAll(Iterable<? extends Object> values) { Assert.notNull(values, "Values must not be null"); return add(new AddToOp(columnName, values, Mode.APPEND)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#entry(java.lang.Object, java.lang.Object) */ @Override public Update entry(Object key, Object value) { Assert.notNull(key, "Key must not be null"); Assert.notNull(value, "Value must not be null"); return addAll(Collections.singletonMap(key, value)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.AddToBuilder#addAll(java.util.Map) */ @Override public Update addAll(Map<? extends Object, ? extends Object> map) { Assert.notNull(map, "Map must not be null"); return add(new AddToMapOp(columnName, map)); } } /** * Builder to associate a single value with a collection at a given index at {@link ColumnName}. * * @author Mark Paluch */ public interface SetBuilder { /** * Create a {@link SetValueBuilder} to set a value at a numeric {@code index}. Used for * {@link com.datastax.driver.core.DataType.Name#LIST} type columns. * * @param index positional index. * @return a {@link SetValueBuilder} to set a value at {@code index} */ SetValueBuilder atIndex(int index); /** * Create a {@link SetValueBuilder} to set a value at {@code index}. Used for * {@link com.datastax.driver.core.DataType.Name#MAP} type columns. * * @param key must not be {@literal null}. * @return a {@link SetValueBuilder} to set a value at {@code index} */ SetValueBuilder atKey(Object key); } /** * Builder to associate a single value with a collection at a given index at {@link ColumnName}. * * @author Mark Paluch */ public interface SetValueBuilder { /** * Associate the {@code value} with the collection at {@link ColumnName} with a previously specified index. * * @param value must not be {@literal null}. * @return the {@link Update} object. */ Update to(Object value); } /** * Default {@link SetBuilder} implementation. */ private class DefaultSetBuilder implements SetBuilder { private final ColumnName columnName; DefaultSetBuilder(ColumnName columnName) { this.columnName = columnName; } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.SetBuilder#atIndex(int) */ @Override public SetValueBuilder atIndex(int index) { return value -> add(new SetAtIndexOp(this.columnName, index, value)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.SetBuilder#atKey(java.lang.Object) */ @Override public SetValueBuilder atKey(Object key) { return value -> add(new SetAtKeyOp(this.columnName, key, value)); } } /** * Abstract class for an update assignment related to a specific {@link ColumnName}. */ public abstract static class AssignmentOp { private final ColumnName columnName; protected AssignmentOp(ColumnName columnName) { this.columnName = columnName; } /** * @return the {@link ColumnName}. */ public ColumnName getColumnName() { return columnName; } } /** * Add element(s) to collection operation. */ public static class AddToOp extends AssignmentOp { private final Iterable<Object> value; private final Mode mode; @SuppressWarnings("unchecked") public AddToOp(ColumnName columnName, Iterable<? extends Object> value, Mode mode) { super(columnName); this.value = (Iterable<Object>) value; this.mode = mode; } public Iterable<Object> getValue() { return value; } public Mode getMode() { return mode; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return Mode.PREPEND.equals(getMode()) ? String.format("%s = %s + %s", getColumnName(), serializeToCqlSafely(value), getColumnName()) : String.format("%s = %s + %s", getColumnName(), getColumnName(), serializeToCqlSafely(value)); } public enum Mode { PREPEND, APPEND, } } /** * Add element(s) to Map operation. */ public static class AddToMapOp extends AssignmentOp { private final Map<Object, Object> value; @SuppressWarnings({ "unchecked", "rawtypes" }) public AddToMapOp(ColumnName columnName, Map<? extends Object, ? extends Object> value) { super(columnName); this.value = (Map) value; } public Map<Object, Object> getValue() { return value; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s = %s + %s", getColumnName(), getColumnName(), serializeToCqlSafely(value)); } } /** * Set operation. */ public static class SetOp extends AssignmentOp { private final Object value; public SetOp(ColumnName columnName, Object value) { super(columnName); this.value = value; } public Object getValue() { return value; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s = %s", getColumnName(), serializeToCqlSafely(value)); } } /** * Set at index operation. */ public static class SetAtIndexOp extends SetOp { private final int index; public SetAtIndexOp(ColumnName columnName, int index, Object value) { super(columnName, value); Assert.notNull(value, "Value must not be null"); this.index = index; } public int getIndex() { return index; } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.SetOp#toString() */ @Override public String toString() { return String.format("%s[%d] = %s", getColumnName(), index, serializeToCqlSafely(getValue())); } } /** * Set at map key operation. */ public static class SetAtKeyOp extends SetOp { private final Object key; private final Object value; public SetAtKeyOp(ColumnName columnName, Object key, Object value) { super(columnName, value); Assert.notNull(key, "Key must not be null"); this.key = key; this.value = value; } public Object getKey() { return key; } @Override public Object getValue() { return value; } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.query.Update.SetOp#toString() */ @Override public String toString() { return String.format("%s[%s] = %s", getColumnName(), serializeToCqlSafely(key), serializeToCqlSafely(getValue())); } } /** * Increment operation. */ public static class IncrOp extends AssignmentOp { private final Number value; public IncrOp(ColumnName columnName, Number value) { super(columnName); this.value = value; } public Number getValue() { return value; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s = %s %s %d", getColumnName(), getColumnName(), value.doubleValue() > 0 ? "+" : "-", Math.abs(value.intValue())); } } /** * Remove operation. */ public static class RemoveOp extends AssignmentOp { private final Object value; public RemoveOp(ColumnName columnName, Object value) { super(columnName); Assert.notNull(value, "Value must not be null"); this.value = value; } public Object getValue() { return value; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s = %s - %s", getColumnName(), getColumnName(), serializeToCqlSafely(getValue())); } } }