View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.lang3.builder;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.time.Duration;
24  import java.time.Instant;
25  import java.time.LocalDate;
26  import java.time.LocalDateTime;
27  import java.time.LocalTime;
28  import java.time.OffsetDateTime;
29  import java.time.OffsetTime;
30  import java.time.Period;
31  import java.time.Year;
32  import java.time.YearMonth;
33  import java.time.ZoneId;
34  import java.time.ZoneOffset;
35  import java.time.ZonedDateTime;
36  import java.time.chrono.HijrahDate;
37  import java.time.chrono.JapaneseDate;
38  import java.time.chrono.MinguoDate;
39  import java.time.chrono.ThaiBuddhistDate;
40  import java.time.temporal.Temporal;
41  import java.time.temporal.TemporalAccessor;
42  import java.time.temporal.TemporalAmount;
43  import java.time.temporal.TemporalField;
44  import java.time.temporal.TemporalUnit;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.List;
48  import java.util.function.Supplier;
49  
50  import org.apache.commons.lang3.stream.IntStreams;
51  import org.junit.jupiter.api.Test;
52  
53  /**
54   * Tests that {@link EqualsBuilder} works using reflection when types that implement JRE interfaces like TemporalAccessor, TemporalAmout, and CharSequence work.
55   */
56  class EqualsBuilderReflectJreImplementationTest extends AbstractBuilderTest {
57  
58      static class MyCharSequence implements CharSequence {
59  
60          private final char[] chars;
61  
62          MyCharSequence(final char[] chars) {
63              this.chars = Arrays.copyOf(chars, chars.length);
64          }
65  
66          MyCharSequence(final char[] chars, final int start, final int end) {
67              this.chars = Arrays.copyOfRange(chars, start, end);
68          }
69  
70          MyCharSequence(final String string) {
71              this.chars = string.toCharArray();
72          }
73  
74          @Override
75          public char charAt(final int index) {
76              return chars[index];
77          }
78  
79          @Override
80          public int length() {
81              return chars.length;
82          }
83  
84          @Override
85          public CharSequence subSequence(final int start, final int end) {
86              return new MyCharSequence(chars, start, end);
87          }
88  
89          @Override
90          public String toString() {
91              return new String(chars);
92          }
93      }
94  
95      static class MyClass implements Cloneable {
96  
97          private final MyCharSequence charSequence;
98          private final MyTemporal temporal;
99          private final MyTemporalAccessor temporalAccessor;
100         private final MyTemporalAmount temporalAmount;
101         private final Object[] objects;
102         private final List<Supplier<?>> list = new ArrayList<>();
103 
104         MyClass(final MyCharSequence charSequence, final MyTemporal temporal, final MyTemporalAccessor temporalAccessor,
105                 final MyTemporalAmount temporalAmount) {
106             this.charSequence = charSequence;
107             this.temporal = temporal;
108             this.temporalAccessor = temporalAccessor;
109             this.temporalAmount = temporalAmount;
110             final int value = Integer.parseInt(charSequence.toString());
111             final LocalDate localDate = LocalDate.ofEpochDay(value);
112             final LocalTime localTime = LocalTime.of(value, value);
113             final LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
114             final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.UTC);
115             final ZoneOffset zoneOffset = ZoneOffset.ofHours(value);
116             this.objects = new Object[] {
117                     // a Long
118                     value,
119                     // all concrete dates and times
120                     localDate, HijrahDate.from(localDate), JapaneseDate.from(localDate), MinguoDate.from(localDate), ThaiBuddhistDate.from(localDate),
121                     localDate, localTime, localDateTime, offsetDateTime, OffsetTime.of(localTime, zoneOffset), Year.of(value), YearMonth.of(value, value),
122                     ZonedDateTime.of(localDateTime, zoneOffset), zoneOffset, ZoneId.of(zoneOffset.getId()) };
123             IntStreams.range(100).forEach(i -> list.add(() -> charSequence));
124         }
125 
126         @Override
127         public String toString() {
128             return String.format("%s[%s, %s, %s, %s, %s]", getClass().getSimpleName(), charSequence, temporal, temporalAccessor, temporalAmount,
129                     Arrays.toString(objects));
130         }
131     }
132 
133     static class MyTemporal implements Temporal {
134 
135         private final String string;
136         private final int value;
137         private final Duration duration;
138         private final Instant instant;
139         private final Period period;
140 
141         MyTemporal(final String string) {
142             this.string = string;
143             this.value = Integer.parseInt(string);
144             this.instant = Instant.ofEpochMilli(value);
145             this.duration = Duration.between(instant, instant.plusMillis(value));
146             this.period = Period.ofDays(value);
147         }
148 
149         @Override
150         public long getLong(final TemporalField field) {
151             return instant.get(field);
152         }
153 
154         @Override
155         public boolean isSupported(final TemporalField field) {
156             return instant.isSupported(field);
157         }
158 
159         @Override
160         public boolean isSupported(final TemporalUnit unit) {
161             return instant.isSupported(unit);
162         }
163 
164         @Override
165         public Temporal plus(final long amountToAdd, final TemporalUnit unit) {
166             return instant.plus(amountToAdd, unit);
167         }
168 
169         @Override
170         public String toString() {
171             return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period);
172         }
173 
174         @Override
175         public long until(final Temporal endExclusive, final TemporalUnit unit) {
176             return instant.until(endExclusive, unit);
177         }
178 
179         @Override
180         public Temporal with(final TemporalField field, final long newValue) {
181             return instant.with(field, newValue);
182         }
183 
184     }
185 
186     static class MyTemporalAccessor implements TemporalAccessor {
187 
188         private final String string;
189         private final int value;
190         private final Instant instant;
191         private final Duration duration;
192         private final Period period;
193 
194         MyTemporalAccessor(final String string) {
195             this.string = string;
196             this.value = Integer.parseInt(string);
197             this.instant = Instant.ofEpochMilli(value);
198             this.duration = Duration.between(instant, instant.plusMillis(value));
199             this.period = Period.ofDays(value);
200         }
201 
202         @Override
203         public long getLong(final TemporalField field) {
204             return instant.get(field);
205         }
206 
207         @Override
208         public boolean isSupported(final TemporalField field) {
209             return instant.isSupported(field);
210         }
211 
212         @Override
213         public String toString() {
214             return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period);
215         }
216 
217     }
218 
219     static class MyTemporalAmount implements TemporalAmount {
220 
221         private final String string;
222         private final int value;
223         private final Instant instant;
224         private final Duration duration;
225         private final Period period;
226 
227         MyTemporalAmount(final String string) {
228             this.string = string;
229             this.value = Integer.parseInt(string);
230             this.instant = Instant.ofEpochMilli(value);
231             this.duration = Duration.between(instant, instant.plusMillis(value));
232             this.period = Period.ofDays(value);
233         }
234 
235         @Override
236         public Temporal addTo(final Temporal temporal) {
237             return duration.addTo(temporal);
238         }
239 
240         @Override
241         public long get(final TemporalUnit unit) {
242             return duration.get(unit);
243         }
244 
245         @Override
246         public List<TemporalUnit> getUnits() {
247             return duration.getUnits();
248         }
249 
250         @Override
251         public Temporal subtractFrom(final Temporal temporal) {
252             return duration.subtractFrom(temporal);
253         }
254 
255         @Override
256         public String toString() {
257             return String.format("%s[%s - %s - %s - %s]", getClass().getSimpleName(), string, instant, duration, period);
258         }
259 
260     }
261 
262     private EqualsBuilder newRecursive() {
263         return new EqualsBuilder().setTestRecursive(true);
264     }
265 
266     @Test
267     void testRecursive() {
268         // assertions are tricky to the subclass
269         final MyClass o1 = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4"));
270         // This gives you different instances of MyTemporalAccessor for 1 (and 2) that should be equals by reflection.
271         final MyClass o1Bis = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4"));
272         final MyClass o2 = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8"));
273         final MyClass o2Bis = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8"));
274         // MyTemporal
275         final EqualsBuilder builder1 = newRecursive();
276         boolean defaultEq = builder1.isEquals();
277         assertEquals(defaultEq, builder1.append(new MyTemporal("1"), new MyTemporal("1")).isEquals());
278         // MyTemporalAccessor
279         final EqualsBuilder builder2 = newRecursive();
280         defaultEq = builder2.isEquals();
281         assertEquals(defaultEq, builder2.append(new MyTemporalAccessor("1"), new MyTemporalAccessor("1")).isEquals());
282         // MyCharSequence
283         final EqualsBuilder builder3 = newRecursive();
284         defaultEq = builder3.isEquals();
285         assertEquals(defaultEq, builder3.append(new MyCharSequence("1"), new MyCharSequence("1")).isEquals());
286         // MyClass
287         assertTrue(newRecursive().append(o1, o1).isEquals(), o1::toString);
288         final EqualsBuilder builder4 = newRecursive();
289         defaultEq = builder4.isEquals();
290         assertEquals(defaultEq, builder4.append(o1, o1Bis).isEquals(), o1::toString);
291         assertTrue(newRecursive().append(o2, o2).isEquals(), o2::toString);
292         final EqualsBuilder builder5 = newRecursive();
293         defaultEq = builder5.isEquals();
294         assertEquals(defaultEq, builder5.append(o2, o2Bis).isEquals(), o2::toString);
295         // not equal normally (see subclass)
296         final EqualsBuilder builder6 = newRecursive();
297         defaultEq = !isForceAccessible();
298         assertEquals(defaultEq, builder6.append(o1, o2).isEquals());
299         final EqualsBuilder builder7 = newRecursive();
300         assertEquals(defaultEq, builder7.append(o2, o1).isEquals());
301     }
302 
303     @Test
304     void testRetention() throws Exception {
305         // The following should not retain memory.
306         for (int i = 0; i < Integer.getInteger("testRetention", 10_000); i++) {
307             final Class<?> clazz = TestClassBuilder.defineSimpleClass(getClass().getPackage().getName(), i);
308             assertTrue(newRecursive().append(clazz.newInstance(), clazz.newInstance()).isEquals());
309         }
310         // some retention is checked in super's after().
311     }
312 
313 }