com.google.gerrit.extensions.common.ChangeInfoDiffer Maven / Gradle / Ivy
// Copyright (C) 2021 The Android Open Source Project
//
// 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 com.google.gerrit.extensions.common;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.groupingBy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * Gets the differences between two {@link ChangeInfo}s.
 *
 * This must be in package {@code com.google.gerrit.extensions.common} for access to protected
 * constructors.
 *
 * 
This assumes that every class reachable from {@link ChangeInfo} has a non-private constructor
 * with zero parameters and overrides the equals method.
 */
public final class ChangeInfoDiffer {
  /**
   * Returns the difference between two instances of {@link ChangeInfo}.
   *
   * 
The {@link ChangeInfoDifference} returned has the following properties:
   *
   * 
Unrepeated fields are present in the difference returned when they differ between {@code
   * oldChangeInfo} and {@code newChangeInfo}. When there's an unrepeated field that's not a {@link
   * String}, primitive, or enum, its fields are only returned when they differ.
   *
   * 
Entries in {@link Map} fields are returned when a key is present in {@code newChangeInfo}
   * and not {@code oldChangeInfo}. If a key is present in both, the diff of the value is returned.
   *
   * 
{@link Collection} fields in {@link ChangeInfoDifference#added()} contain only items found
   * in {@code newChangeInfo} and not {@code oldChangeInfo}.
   *
   * 
{@link Collection} fields in {@link ChangeInfoDifference#removed()} contain only items found
   * in {@code oldChangeInfo} and not {@code newChangeInfo}.
   *
   * @param oldChangeInfo the previous {@link ChangeInfo} to diff against {@code newChangeInfo}
   * @param newChangeInfo the {@link ChangeInfo} to diff against {@code oldChangeInfo}
   * @return the difference between the given {@link ChangeInfo}s
   */
  public static ChangeInfoDifference getDifference(
      ChangeInfo oldChangeInfo, ChangeInfo newChangeInfo) {
    return ChangeInfoDifference.builder()
        .setOldChangeInfo(oldChangeInfo)
        .setNewChangeInfo(newChangeInfo)
        .setAdded(getAdded(oldChangeInfo, newChangeInfo))
        .setRemoved(getAdded(newChangeInfo, oldChangeInfo))
        .build();
  }
  @SuppressWarnings("unchecked") // reflection is used to construct instances of T
  private static  T getAdded(T oldValue, T newValue) {
    if (newValue instanceof Collection) {
      ImmutableList> result =
          getAddedForCollection((Collection>) oldValue, (Collection>) newValue);
      return (T) result;
    }
    if (newValue instanceof Map) {
      ImmutableMap, ?> result = getAddedForMap((Map, ?>) oldValue, (Map, ?>) newValue);
      return (T) result;
    }
    T toPopulate = (T) construct(newValue.getClass());
    if (toPopulate == null) {
      return null;
    }
    for (Field field : newValue.getClass().getDeclaredFields()) {
      if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
        continue;
      }
      Object newFieldObj = get(field, newValue);
      if (oldValue == null || newFieldObj == null) {
        set(field, toPopulate, newFieldObj);
        continue;
      }
      Object oldFieldObj = get(field, oldValue);
      if (newFieldObj.equals(oldFieldObj)) {
        continue;
      }
      if (isSimple(field.getType()) || oldFieldObj == null) {
        set(field, toPopulate, newFieldObj);
      } else if (newFieldObj instanceof Collection || newFieldObj instanceof Map) {
        set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
      } else {
        // Recurse to set all fields in the non-primitive object.
        set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
      }
    }
    return toPopulate;
  }
  @VisibleForTesting
  static boolean isSimple(Class> c) {
    return c.isPrimitive()
        || c.isEnum()
        || String.class.isAssignableFrom(c)
        || Number.class.isAssignableFrom(c)
        || Boolean.class.isAssignableFrom(c)
        || Timestamp.class.isAssignableFrom(c);
  }
  @VisibleForTesting
  static Object construct(Class> c) {
    // Only use constructors without parameters because we can't determine what values to pass.
    return stream(c.getDeclaredConstructors())
        .filter(constructor -> constructor.getParameterCount() == 0)
        .findAny()
        .map(ChangeInfoDiffer::construct)
        .orElseThrow(
            () ->
                new IllegalStateException("Class " + c + " must have a zero argument constructor"));
  }
  private static Object construct(Constructor> constructor) {
    try {
      return constructor.newInstance();
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException("Failed to construct class " + constructor.getName(), e);
    }
  }
  /** Returns {@code null} if nothing has been added to {@code oldCollection} */
  @Nullable
  private static ImmutableList> getAddedForCollection(
      @Nullable Collection> oldCollection, Collection> newCollection) {
    ImmutableList> notInOldCollection = getAdditionsForCollection(oldCollection, newCollection);
    return notInOldCollection.isEmpty() ? null : notInOldCollection;
  }
  @Nullable
  private static ImmutableList