LCOV - code coverage report
Current view: top level - extensions/common - ChangeInfoDiffer.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 64 75 85.3 %
Date: 2022-11-19 15:00:39 Functions: 13 14 92.9 %

          Line data    Source code
       1             : // Copyright (C) 2021 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.extensions.common;
      16             : 
      17             : import static com.google.common.collect.ImmutableList.toImmutableList;
      18             : import static java.util.Arrays.stream;
      19             : import static java.util.stream.Collectors.groupingBy;
      20             : 
      21             : import com.google.common.annotations.VisibleForTesting;
      22             : import com.google.common.collect.ImmutableList;
      23             : import com.google.common.collect.ImmutableMap;
      24             : import com.google.gerrit.common.Nullable;
      25             : import java.lang.reflect.Constructor;
      26             : import java.lang.reflect.Field;
      27             : import java.sql.Timestamp;
      28             : import java.util.Collection;
      29             : import java.util.List;
      30             : import java.util.Map;
      31             : 
      32             : /**
      33             :  * Gets the differences between two {@link ChangeInfo}s.
      34             :  *
      35             :  * <p>This must be in package {@code com.google.gerrit.extensions.common} for access to protected
      36             :  * constructors.
      37             :  *
      38             :  * <p>This assumes that every class reachable from {@link ChangeInfo} has a non-private constructor
      39             :  * with zero parameters and overrides the equals method.
      40             :  */
      41             : public final class ChangeInfoDiffer {
      42             : 
      43             :   /**
      44             :    * Returns the difference between two instances of {@link ChangeInfo}.
      45             :    *
      46             :    * <p>The {@link ChangeInfoDifference} returned has the following properties:
      47             :    *
      48             :    * <p>Unrepeated fields are present in the difference returned when they differ between {@code
      49             :    * oldChangeInfo} and {@code newChangeInfo}. When there's an unrepeated field that's not a {@link
      50             :    * String}, primitive, or enum, its fields are only returned when they differ.
      51             :    *
      52             :    * <p>Entries in {@link Map} fields are returned when a key is present in {@code newChangeInfo}
      53             :    * and not {@code oldChangeInfo}. If a key is present in both, the diff of the value is returned.
      54             :    *
      55             :    * <p>{@link Collection} fields in {@link ChangeInfoDifference#added()} contain only items found
      56             :    * in {@code newChangeInfo} and not {@code oldChangeInfo}.
      57             :    *
      58             :    * <p>{@link Collection} fields in {@link ChangeInfoDifference#removed()} contain only items found
      59             :    * in {@code oldChangeInfo} and not {@code newChangeInfo}.
      60             :    *
      61             :    * @param oldChangeInfo the previous {@link ChangeInfo} to diff against {@code newChangeInfo}
      62             :    * @param newChangeInfo the {@link ChangeInfo} to diff against {@code oldChangeInfo}
      63             :    * @return the difference between the given {@link ChangeInfo}s
      64             :    */
      65             :   public static ChangeInfoDifference getDifference(
      66             :       ChangeInfo oldChangeInfo, ChangeInfo newChangeInfo) {
      67           2 :     return ChangeInfoDifference.builder()
      68           2 :         .setOldChangeInfo(oldChangeInfo)
      69           2 :         .setNewChangeInfo(newChangeInfo)
      70           2 :         .setAdded(getAdded(oldChangeInfo, newChangeInfo))
      71           2 :         .setRemoved(getAdded(newChangeInfo, oldChangeInfo))
      72           2 :         .build();
      73             :   }
      74             : 
      75             :   @SuppressWarnings("unchecked") // reflection is used to construct instances of T
      76             :   private static <T> T getAdded(T oldValue, T newValue) {
      77           2 :     if (newValue instanceof Collection) {
      78           2 :       List<?> result = getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
      79           2 :       return (T) result;
      80             :     }
      81             : 
      82           2 :     if (newValue instanceof Map) {
      83           2 :       Map<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
      84           2 :       return (T) result;
      85             :     }
      86             : 
      87           2 :     T toPopulate = (T) construct(newValue.getClass());
      88           2 :     if (toPopulate == null) {
      89           0 :       return null;
      90             :     }
      91             : 
      92           2 :     for (Field field : newValue.getClass().getDeclaredFields()) {
      93           2 :       if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
      94           2 :         continue;
      95             :       }
      96             : 
      97           2 :       Object newFieldObj = get(field, newValue);
      98           2 :       if (oldValue == null || newFieldObj == null) {
      99           2 :         set(field, toPopulate, newFieldObj);
     100           2 :         continue;
     101             :       }
     102             : 
     103           2 :       Object oldFieldObj = get(field, oldValue);
     104           2 :       if (newFieldObj.equals(oldFieldObj)) {
     105           2 :         continue;
     106             :       }
     107             : 
     108           2 :       if (isSimple(field.getType()) || oldFieldObj == null) {
     109           2 :         set(field, toPopulate, newFieldObj);
     110           2 :       } else if (newFieldObj instanceof Collection || newFieldObj instanceof Map) {
     111           2 :         set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
     112             :       } else {
     113             :         // Recurse to set all fields in the non-primitive object.
     114           1 :         set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
     115             :       }
     116             :     }
     117           2 :     return toPopulate;
     118             :   }
     119             : 
     120             :   @VisibleForTesting
     121             :   static boolean isSimple(Class<?> c) {
     122           2 :     return c.isPrimitive()
     123           2 :         || c.isEnum()
     124           2 :         || String.class.isAssignableFrom(c)
     125           2 :         || Number.class.isAssignableFrom(c)
     126           2 :         || Boolean.class.isAssignableFrom(c)
     127           2 :         || Timestamp.class.isAssignableFrom(c);
     128             :   }
     129             : 
     130             :   @VisibleForTesting
     131             :   static Object construct(Class<?> c) {
     132             :     // Only use constructors without parameters because we can't determine what values to pass.
     133           2 :     return stream(c.getDeclaredConstructors())
     134           2 :         .filter(constructor -> constructor.getParameterCount() == 0)
     135           2 :         .findAny()
     136           2 :         .map(ChangeInfoDiffer::construct)
     137           2 :         .orElseThrow(
     138             :             () ->
     139           0 :                 new IllegalStateException("Class " + c + " must have a zero argument constructor"));
     140             :   }
     141             : 
     142             :   private static Object construct(Constructor<?> constructor) {
     143             :     try {
     144           2 :       return constructor.newInstance();
     145           0 :     } catch (ReflectiveOperationException e) {
     146           0 :       throw new IllegalStateException("Failed to construct class " + constructor.getName(), e);
     147             :     }
     148             :   }
     149             : 
     150             :   /** Returns {@code null} if nothing has been added to {@code oldCollection} */
     151             :   @Nullable
     152             :   private static ImmutableList<?> getAddedForCollection(
     153             :       Collection<?> oldCollection, Collection<?> newCollection) {
     154           2 :     ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
     155           2 :     return notInOldCollection.isEmpty() ? null : notInOldCollection;
     156             :   }
     157             : 
     158             :   @Nullable
     159             :   private static ImmutableList<Object> getAdditions(
     160             :       Collection<?> oldCollection, Collection<?> newCollection) {
     161           2 :     if (oldCollection == null)
     162           1 :       return newCollection != null ? ImmutableList.copyOf(newCollection) : null;
     163             : 
     164           2 :     Map<Object, List<Object>> duplicatesMap = newCollection.stream().collect(groupingBy(v -> v));
     165           2 :     oldCollection.forEach(
     166             :         v -> {
     167           2 :           if (duplicatesMap.containsKey(v)) {
     168           1 :             duplicatesMap.get(v).remove(v);
     169             :           }
     170           2 :         });
     171           2 :     return duplicatesMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
     172             :   }
     173             : 
     174             :   /** Returns {@code null} if nothing has been added to {@code oldMap} */
     175             :   @Nullable
     176             :   private static ImmutableMap<Object, Object> getAddedForMap(Map<?, ?> oldMap, Map<?, ?> newMap) {
     177           2 :     ImmutableMap.Builder<Object, Object> additionsBuilder = ImmutableMap.builder();
     178           2 :     for (Map.Entry<?, ?> entry : newMap.entrySet()) {
     179           2 :       Object added = getAdded(oldMap.get(entry.getKey()), entry.getValue());
     180           2 :       if (added != null) {
     181           2 :         additionsBuilder.put(entry.getKey(), added);
     182             :       }
     183           2 :     }
     184           2 :     ImmutableMap<Object, Object> additions = additionsBuilder.build();
     185           2 :     return additions.isEmpty() ? null : additions;
     186             :   }
     187             : 
     188             :   private static Object get(Field field, Object obj) {
     189             :     try {
     190           2 :       return field.get(obj);
     191           0 :     } catch (IllegalAccessException e) {
     192           0 :       throw new IllegalStateException(
     193           0 :           String.format("Access denied getting field %s in %s", field.getName(), obj.getClass()),
     194             :           e);
     195             :     }
     196             :   }
     197             : 
     198             :   private static void set(Field field, Object obj, Object value) {
     199             :     try {
     200           2 :       field.set(obj, value);
     201           0 :     } catch (IllegalAccessException e) {
     202           0 :       throw new IllegalStateException(
     203           0 :           String.format(
     204           0 :               "Access denied setting field %s in %s", field.getName(), obj.getClass().getName()),
     205             :           e);
     206           2 :     }
     207           2 :   }
     208             : 
     209             :   private ChangeInfoDiffer() {}
     210             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750