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 : }
|