| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.document; |
| |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| |
| import com.google.common.collect.Maps; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| /** |
| * A DocumentStore "update" operation for one document. |
| */ |
| public final class UpdateOp { |
| |
| private final String id; |
| |
| private boolean isNew; |
| private boolean isDelete; |
| |
| private final Map<Key, Operation> changes; |
| private Map<Key, Condition> conditions; |
| |
| /** |
| * Create an update operation for the document with the given id. The commit |
| * root is assumed to be the path, unless this is changed later on. |
| * |
| * @param id the primary key |
| * @param isNew whether this is a new document |
| */ |
| public UpdateOp(@NotNull String id, boolean isNew) { |
| this(id, isNew, false, new HashMap<Key, Operation>(), null); |
| } |
| |
| UpdateOp(@NotNull String id, |
| boolean isNew, |
| boolean isDelete, |
| @NotNull Map<Key, Operation> changes, |
| @Nullable Map<Key, Condition> conditions) { |
| this.id = checkNotNull(id, "id must not be null"); |
| this.isNew = isNew; |
| this.isDelete = isDelete; |
| this.changes = checkNotNull(changes); |
| this.conditions = conditions; |
| } |
| |
| static UpdateOp combine(String id, Iterable<UpdateOp> ops) { |
| Map<Key, Operation> changes = Maps.newHashMap(); |
| Map<Key, Condition> conditions = Maps.newHashMap(); |
| for (UpdateOp op : ops) { |
| changes.putAll(op.getChanges()); |
| if (op.conditions != null) { |
| conditions.putAll(op.conditions); |
| } |
| } |
| if (conditions.isEmpty()) { |
| conditions = null; |
| } |
| return new UpdateOp(id, false, false, changes, conditions); |
| } |
| |
| /** |
| * Creates an update operation for the document with the given id. The |
| * changes are shared with the this update operation. |
| * |
| * @param id the primary key. |
| */ |
| public UpdateOp shallowCopy(String id) { |
| return new UpdateOp(id, isNew, isDelete, changes, conditions); |
| } |
| |
| /** |
| * Creates a deep copy of this update operation. Changes to the returned |
| * {@code UpdateOp} do not affect this object. |
| * |
| * @return a copy of this operation. |
| */ |
| public UpdateOp copy() { |
| Map<Key, Condition> conditionMap = null; |
| if (conditions != null) { |
| conditionMap = new HashMap<Key, Condition>(conditions); |
| } |
| return new UpdateOp(id, isNew, isDelete, |
| new HashMap<Key, Operation>(changes), conditionMap); |
| } |
| |
| @NotNull |
| public String getId() { |
| return id; |
| } |
| |
| public boolean isNew() { |
| return isNew; |
| } |
| |
| public void setNew(boolean isNew) { |
| this.isNew = isNew; |
| } |
| |
| void setDelete(boolean isDelete) { |
| this.isDelete = isDelete; |
| } |
| |
| boolean isDelete() { |
| return isDelete; |
| } |
| |
| public Map<Key, Operation> getChanges() { |
| return changes; |
| } |
| |
| public Map<Key, Condition> getConditions() { |
| if (conditions == null) { |
| return Collections.emptyMap(); |
| } else { |
| return conditions; |
| } |
| } |
| |
| /** |
| * Checks if the UpdateOp has any change operation is registered with |
| * current update operation |
| * |
| * @return true if any change operation is created |
| */ |
| public boolean hasChanges() { |
| return !changes.isEmpty(); |
| } |
| |
| /** |
| * Add a new or update an existing map entry. |
| * The property is a map of revisions / values. |
| * |
| * @param property the property |
| * @param revision the revision |
| * @param value the value |
| */ |
| void setMapEntry(@NotNull String property, @NotNull Revision revision, String value) { |
| Operation op = new Operation(Operation.Type.SET_MAP_ENTRY, value); |
| changes.put(new Key(property, checkNotNull(revision)), op); |
| } |
| |
| /** |
| * Remove a property. |
| * |
| * @param property the property name |
| */ |
| public void remove(@NotNull String property) { |
| if (Document.ID.equals(property)) { |
| throw new IllegalArgumentException(Document.ID + " must not be removed"); |
| } |
| Operation op = new Operation(Operation.Type.REMOVE, null); |
| changes.put(new Key(property, null), op); |
| } |
| |
| /** |
| * Remove a map entry. |
| * The property is a map of revisions / values. |
| * |
| * @param property the property |
| * @param revision the revision |
| */ |
| public void removeMapEntry(@NotNull String property, @NotNull Revision revision) { |
| Operation op = new Operation(Operation.Type.REMOVE_MAP_ENTRY, null); |
| changes.put(new Key(property, checkNotNull(revision)), op); |
| } |
| |
| /** |
| * Set the property to the given long value. |
| * |
| * @param property the property name |
| * @param value the value |
| */ |
| public void set(String property, long value) { |
| internalSet(property, value); |
| } |
| |
| /** |
| * Set the property to the given boolean value. |
| * |
| * @param property the property name |
| * @param value the value |
| */ |
| public void set(String property, boolean value) { |
| internalSet(property, value); |
| } |
| |
| /** |
| * Set the property to the given String value. |
| * <p> |
| * Note that {@link Document#ID} must not be set using this method; |
| * it is sufficiently specified by the id parameter set in the constructor. |
| * |
| * @param property the property name |
| * @param value the value |
| * @throws IllegalArgumentException |
| * if an attempt is made to set {@link Document#ID}. |
| */ |
| public void set(String property, String value) { |
| internalSet(property, value); |
| } |
| |
| /** |
| * Set the property to the given value if the new value is higher than the |
| * existing value. The property is also set to the given value if the |
| * property does not yet exist. |
| * <p> |
| * The result of a max operation with different types of values is |
| * undefined. |
| * |
| * @param property the name of the property to set. |
| * @param value the new value for the property. |
| */ |
| <T> void max(String property, Comparable<T> value) { |
| Operation op = new Operation(Operation.Type.MAX, value); |
| changes.put(new Key(property, null), op); |
| } |
| |
| /** |
| * Do not set the property entry (after it has been set). |
| * The property is a map of revisions / values. |
| * |
| * @param property the property name |
| * @param revision the revision |
| */ |
| void unsetMapEntry(@NotNull String property, @NotNull Revision revision) { |
| changes.remove(new Key(property, checkNotNull(revision))); |
| } |
| |
| /** |
| * Checks if the named key exists or is absent in the document. This |
| * method can be used to make a conditional update. |
| * |
| * @param property the property name |
| * @param revision the revision |
| */ |
| void containsMapEntry(@NotNull String property, |
| @NotNull Revision revision, |
| boolean exists) { |
| if (isNew) { |
| throw new IllegalStateException("Cannot use containsMapEntry() on new document"); |
| } |
| Condition c = exists ? Condition.EXISTS : Condition.MISSING; |
| getOrCreateConditions().put(new Key(property, checkNotNull(revision)), c); |
| } |
| |
| /** |
| * Checks if the named key exists or is absent in the document. This |
| * method can be used to make a conditional update. |
| * |
| * @param property the property name |
| * @param revision the revision |
| */ |
| void contains(@NotNull String property, boolean exists) { |
| if (isNew) { |
| throw new IllegalStateException("Cannot use contaons() on new document"); |
| } |
| Condition c = exists ? Condition.EXISTS : Condition.MISSING; |
| getOrCreateConditions().put(new Key(property, null), c); |
| } |
| |
| /** |
| * Checks if the property is equal to the given value. |
| * |
| * @param property the name of the property or map. |
| * @param value the value to compare to ({@code null} checks both for non-existence and the value being null) |
| */ |
| void equals(@NotNull String property, @Nullable Object value) { |
| equals(property, null, value); |
| } |
| |
| /** |
| * Checks if the property or map entry is equal to the given value. |
| * |
| * @param property the name of the property or map. |
| * @param revision the revision within the map or {@code null} if this check |
| * is for a property. |
| * @param value the value to compare to ({@code null} checks both for non-existence and the value being null) |
| */ |
| void equals(@NotNull String property, |
| @Nullable Revision revision, |
| @Nullable Object value) { |
| if (isNew) { |
| throw new IllegalStateException("Cannot perform equals check on new document"); |
| } |
| getOrCreateConditions().put(new Key(property, revision), |
| Condition.newEqualsCondition(value)); |
| } |
| |
| /** |
| * Checks if the property does not exist or is not equal to the given value. |
| * |
| * @param property the name of the property or map. |
| * @param value the value to compare to. |
| */ |
| void notEquals(@NotNull String property, @Nullable Object value) { |
| notEquals(property, null, value); |
| } |
| |
| /** |
| * Checks if the property or map entry does not exist or is not equal to the given value. |
| * |
| * @param property the name of the property or map. |
| * @param revision the revision within the map or {@code null} if this check |
| * is for a property. |
| * @param value the value to compare to. |
| */ |
| void notEquals(@NotNull String property, |
| @Nullable Revision revision, |
| @Nullable Object value) { |
| if (isNew) { |
| throw new IllegalStateException("Cannot perform notEquals check on new document"); |
| } |
| getOrCreateConditions().put(new Key(property, revision), |
| Condition.newNotEqualsCondition(value)); |
| } |
| |
| /** |
| * Increment the value. |
| * |
| * @param property the key |
| * @param value the increment |
| */ |
| public void increment(@NotNull String property, long value) { |
| Operation op = new Operation(Operation.Type.INCREMENT, value); |
| changes.put(new Key(property, null), op); |
| } |
| |
| public UpdateOp getReverseOperation() { |
| UpdateOp reverse = new UpdateOp(id, isNew); |
| for (Entry<Key, Operation> e : changes.entrySet()) { |
| Operation r = e.getValue().getReverse(); |
| if (r != null) { |
| reverse.changes.put(e.getKey(), r); |
| } |
| } |
| return reverse; |
| } |
| |
| @Override |
| public String toString() { |
| String s = "key: " + id + " " + (isNew ? "new" : "update") + " " + changes; |
| if (conditions != null) { |
| s += " conditions " + conditions; |
| } |
| return s; |
| } |
| |
| private Map<Key, Condition> getOrCreateConditions() { |
| if (conditions == null) { |
| conditions = Maps.newHashMap(); |
| } |
| return conditions; |
| } |
| |
| private void internalSet(String property, Object value) { |
| if (Document.ID.equals(property)) { |
| throw new IllegalArgumentException( |
| "updateOp.id (" + id + ") must not set " + Document.ID); |
| } |
| Operation op = new Operation(Operation.Type.SET, value); |
| changes.put(new Key(property, null), op); |
| } |
| |
| /** |
| * A DocumentStore operation for a given key within a document. |
| */ |
| public static final class Operation { |
| |
| /** |
| * The DocumentStore operation type. |
| */ |
| public enum Type { |
| |
| /** |
| * Set the value. |
| * The sub-key is not used. |
| */ |
| SET, |
| |
| /** |
| * Set the value if the new value is higher than the existing value. |
| * The new value is also considered higher, when there is no |
| * existing value. |
| * The sub-key is not used. |
| */ |
| MAX, |
| |
| /** |
| * Increment the Long value with the provided Long value. |
| * The sub-key is not used. |
| */ |
| INCREMENT, |
| |
| /** |
| * Add the sub-key / value pair. |
| * The value in the stored node is a map. |
| */ |
| SET_MAP_ENTRY, |
| |
| /** |
| * Remove the sub-key / value pair. |
| * The value in the stored node is a map. |
| */ |
| REMOVE_MAP_ENTRY, |
| |
| /** |
| * Remove the value. |
| * The sub-key is not used. |
| */ |
| REMOVE |
| } |
| |
| |
| /** |
| * The operation type. |
| */ |
| public final Type type; |
| |
| /** |
| * The value, if any. |
| */ |
| public final Object value; |
| |
| Operation(Type type, Object value) { |
| this.type = checkNotNull(type); |
| this.value = value; |
| } |
| |
| @Override |
| public String toString() { |
| return type + " " + value; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj instanceof Operation) { |
| Operation other = (Operation) obj; |
| return this.type.equals(other.type) |
| && Objects.equals(this.value, other.value); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(type, value); |
| } |
| |
| public Operation getReverse() { |
| Operation reverse = null; |
| switch (type) { |
| case INCREMENT: |
| reverse = new Operation(Type.INCREMENT, -(Long) value); |
| break; |
| case SET: |
| case MAX: |
| case REMOVE_MAP_ENTRY: |
| case REMOVE: |
| // nothing to do |
| break; |
| case SET_MAP_ENTRY: |
| reverse = new Operation(Type.REMOVE_MAP_ENTRY, null); |
| break; |
| } |
| return reverse; |
| } |
| |
| } |
| |
| /** |
| * A condition to check before an update is applied. |
| */ |
| public static final class Condition { |
| |
| /** |
| * Check if a map entry exists in a map. |
| */ |
| public static final Condition EXISTS = new Condition(Type.EXISTS, true); |
| |
| /** |
| * Check if a map entry is missing in a map. |
| */ |
| public static final Condition MISSING = new Condition(Type.EXISTS, false); |
| |
| public enum Type { |
| |
| /** |
| * Checks if the map entry is present in a map or not. |
| */ |
| EXISTS, |
| |
| /** |
| * Checks if a map entry equals a given value. |
| */ |
| EQUALS, |
| |
| /** |
| * Checks if a map entry does not equal a given value. |
| */ |
| NOTEQUALS |
| } |
| |
| /** |
| * The condition type. |
| */ |
| public final Type type; |
| |
| /** |
| * The value. |
| */ |
| public final Object value; |
| |
| private Condition(Type type, Object value) { |
| this.type = type; |
| this.value = value; |
| } |
| |
| /** |
| * Creates a new equals condition with the given value. |
| * |
| * @param value the value to compare to. |
| * @return the equals condition. |
| */ |
| public static Condition newEqualsCondition(@Nullable Object value) { |
| return new Condition(Type.EQUALS, value); |
| } |
| |
| /** |
| * Creates a new notEquals condition with the given value. |
| * |
| * @param value the value to compare to. |
| * @return the notEquals condition. |
| */ |
| public static Condition newNotEqualsCondition(@Nullable Object value) { |
| return new Condition(Type.NOTEQUALS, value); |
| } |
| |
| @Override |
| public String toString() { |
| return type + " " + value; |
| } |
| } |
| |
| /** |
| * A key for an operation consists of a property name and an optional |
| * revision. The revision is only set if the value for the operation is |
| * set for a certain revision. |
| */ |
| public static final class Key { |
| |
| private final String name; |
| private final Revision revision; |
| |
| public Key(@NotNull String name, @Nullable Revision revision) { |
| this.name = checkNotNull(name); |
| this.revision = revision; |
| } |
| |
| @NotNull |
| public String getName() { |
| return name; |
| } |
| |
| @Nullable |
| public Revision getRevision() { |
| return revision; |
| } |
| |
| @Override |
| public String toString() { |
| String s = name; |
| if (revision != null) { |
| s += "." + revision.toString(); |
| } |
| return s; |
| } |
| |
| @Override |
| public int hashCode() { |
| int hash = name.hashCode(); |
| if (revision != null) { |
| hash ^= revision.hashCode(); |
| } |
| return hash; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj instanceof Key) { |
| Key other = (Key) obj; |
| return name.equals(other.name) && |
| (revision != null ? revision.equals(other.revision) : other.revision == null); |
| } |
| return false; |
| } |
| } |
| } |