All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.extensions.common.ChangeInfoDiffer Maven / Gradle / Ivy

The newest version!
// 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 getAdditionsForCollection( @Nullable Collection oldCollection, Collection newCollection) { if (oldCollection == null) { return ImmutableList.copyOf(newCollection); } Map> duplicatesMap = newCollection.stream().collect(groupingBy(v -> v)); oldCollection.forEach( v -> { if (duplicatesMap.containsKey(v)) { duplicatesMap.get(v).remove(v); } }); return duplicatesMap.values().stream().flatMap(Collection::stream).collect(toImmutableList()); } /** Returns {@code null} if nothing has been added to {@code oldMap} */ @Nullable private static ImmutableMap getAddedForMap( @Nullable Map oldMap, Map newMap) { ImmutableMap notInOldMap = getAdditionsForMap(oldMap, newMap); return notInOldMap.isEmpty() ? null : notInOldMap; } @Nullable private static ImmutableMap getAdditionsForMap( @Nullable Map oldMap, Map newMap) { if (oldMap == null) { return ImmutableMap.copyOf(newMap); } ImmutableMap.Builder additionsBuilder = ImmutableMap.builder(); for (Map.Entry entry : newMap.entrySet()) { Object added = getAdded(oldMap.get(entry.getKey()), entry.getValue()); if (added != null) { additionsBuilder.put(entry.getKey(), added); } } return additionsBuilder.build(); } private static Object get(Field field, Object obj) { try { return field.get(obj); } catch (IllegalAccessException e) { throw new IllegalStateException( String.format("Access denied getting field %s in %s", field.getName(), obj.getClass()), e); } } private static void set(Field field, Object obj, Object value) { try { field.set(obj, value); } catch (IllegalAccessException e) { throw new IllegalStateException( String.format( "Access denied setting field %s in %s", field.getName(), obj.getClass().getName()), e); } } private ChangeInfoDiffer() {} }