/* * 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.beam.sdk.util; import java.util.Arrays; import java.util.Objects; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.CoderException; /** * Static methods for creating and working with {@link MutationDetector}. */ public class MutationDetectors { private MutationDetectors() {} /** * Creates a new {@code MutationDetector} for the provided {@code value} that uses the provided * {@link Coder} to perform deep copies and comparisons by serializing and deserializing values. * * <p>It is permissible for {@code value} to be {@code null}. Since {@code null} is immutable, * the mutation check will always succeed. */ public static <T> MutationDetector forValueWithCoder(T value, Coder<T> coder) throws CoderException { if (value == null) { return noopMutationDetector(); } else { return new CodedValueMutationDetector<>(value, coder); } } /** * Creates a new {@code MutationDetector} that always succeeds. * * <p>This is useful, for example, for providing a very efficient mutation detector for a value * which is already immutable by design. */ public static MutationDetector noopMutationDetector() { return new NoopMutationDetector(); } /** * A {@link MutationDetector} for {@code null}, which is immutable. */ private static class NoopMutationDetector implements MutationDetector { @Override public void verifyUnmodified() { } @Override public void close() { } } /** * Given a value of type {@code T} and a {@link Coder} for that type, provides facilities to save * check that the value has not changed. * * @param <T> the type of values checked for mutation */ private static class CodedValueMutationDetector<T> implements MutationDetector { private final Coder<T> coder; /** * A saved pointer to an in-memory value provided upon construction, which we will check for * forbidden mutations. */ private final T possiblyModifiedObject; /** * A saved encoded copy of the same value as {@link #possiblyModifiedObject}. Naturally, it * will not change if {@link #possiblyModifiedObject} is mutated. */ private final byte[] encodedOriginalObject; /** * The object decoded from {@link #encodedOriginalObject}. It will be used during every call to * {@link #verifyUnmodified}, which could be called many times throughout the lifetime of this * {@link CodedValueMutationDetector}. */ private final T clonedOriginalObject; /** * Create a mutation detector for the provided {@code value}, using the provided {@link Coder} * for cloning and checking serialized forms for equality. */ public CodedValueMutationDetector(T value, Coder<T> coder) throws CoderException { this.coder = coder; this.possiblyModifiedObject = value; this.encodedOriginalObject = CoderUtils.encodeToByteArray(coder, value); this.clonedOriginalObject = CoderUtils.decodeFromByteArray(coder, encodedOriginalObject); } @Override public void verifyUnmodified() { try { verifyUnmodifiedThrowingCheckedExceptions(); } catch (CoderException exn) { throw new RuntimeException(exn); } } private void verifyUnmodifiedThrowingCheckedExceptions() throws CoderException { // If either object believes they are equal, we trust that and short-circuit deeper checks. if (Objects.equals(possiblyModifiedObject, clonedOriginalObject) || Objects.equals(clonedOriginalObject, possiblyModifiedObject)) { return; } // Since retainedObject is in general an instance of a subclass of T, when it is cloned to // clonedObject using a Coder<T>, the two will generally be equivalent viewed as a T, but in // general neither retainedObject.equals(clonedObject) nor clonedObject.equals(retainedObject) // will hold. // // For example, CoderUtils.clone(IterableCoder<Integer>, IterableSubclass<Integer>) will // produce an ArrayList<Integer> with the same contents as the IterableSubclass, but the // latter will quite reasonably not consider itself equivalent to an ArrayList (and vice // versa). // // To enable a reasonable comparison, we clone retainedObject again here, converting it to // the same sort of T that the Coder<T> output when it created clonedObject. T clonedPossiblyModifiedObject = CoderUtils.clone(coder, possiblyModifiedObject); // If deepEquals() then we trust the equals implementation. // This deliberately allows fields to escape this check. if (Objects.deepEquals(clonedPossiblyModifiedObject, clonedOriginalObject)) { return; } // If not deepEquals(), the class may just have a poor equals() implementation. // So we next try checking their serialized forms. We re-serialize instead of checking // encodedObject, because the Coder may treat it differently. // // For example, an unbounded Iterable will be encoded in an unbounded way, but decoded into an // ArrayList, which will then be re-encoded in a bounded format. So we really do need to // encode-decode-encode retainedObject. if (Arrays.equals( CoderUtils.encodeToByteArray(coder, clonedOriginalObject), CoderUtils.encodeToByteArray(coder, clonedPossiblyModifiedObject))) { return; } // If we got here, then they are not deepEquals() and do not have deepEquals() encodings. // Even if there is some conceptual sense in which the objects are equivalent, it has not // been adequately expressed in code. illegalMutation(clonedOriginalObject, clonedPossiblyModifiedObject); } private void illegalMutation(T previousValue, T newValue) throws CoderException { throw new IllegalMutationException( String.format("Value %s mutated illegally, new value was %s." + " Encoding was %s, now %s.", previousValue, newValue, CoderUtils.encodeToBase64(coder, previousValue), CoderUtils.encodeToBase64(coder, newValue)), previousValue, newValue); } @Override public void close() { verifyUnmodified(); } } }