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.assertFalse;
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.AbstractLangTest;
51  import org.apache.commons.lang3.stream.IntStreams;
52  import org.junit.jupiter.api.Test;
53  
54  /**
55   * Tests that {@link EqualsBuilder} works using reflection when types that implement JRE interfaces like TemporalAccessor, TemporalAmout, and CharSequence work.
56   */
57  class EqualsBuilderReflectJreImplementationTest extends AbstractLangTest {
58  
59      static class MyCharSequence implements CharSequence {
60  
61          private final char[] chars;
62  
63          MyCharSequence(final char[] chars) {
64              this.chars = Arrays.copyOf(chars, chars.length);
65          }
66  
67          MyCharSequence(final char[] chars, final int start, final int end) {
68              this.chars = Arrays.copyOfRange(chars, start, end);
69          }
70  
71          MyCharSequence(final String string) {
72              this.chars = string.toCharArray();
73          }
74  
75          @Override
76          public char charAt(final int index) {
77              return chars[index];
78          }
79  
80          @Override
81          public int length() {
82              return chars.length;
83          }
84  
85          @Override
86          public CharSequence subSequence(final int start, final int end) {
87              return new MyCharSequence(chars, start, end);
88          }
89  
90          @Override
91          public String toString() {
92              return new String(chars);
93          }
94      }
95  
96      static class MyClass implements Cloneable {
97  
98          private final MyCharSequence charSequence;
99          private final MyTemporal temporal;
100         private final MyTemporalAccessor temporalAccessor;
101         private final MyTemporalAmount temporalAmount;
102         private final Object[] objects;
103         private final List<Supplier<?>> list = new ArrayList<>();
104 
105         MyClass(final MyCharSequence charSequence, final MyTemporal temporal, final MyTemporalAccessor temporalAccessor,
106                 final MyTemporalAmount temporalAmount) {
107             this.charSequence = charSequence;
108             this.temporal = temporal;
109             this.temporalAccessor = temporalAccessor;
110             this.temporalAmount = temporalAmount;
111             final int value = Integer.parseInt(charSequence.toString());
112             final LocalDate localDate = LocalDate.ofEpochDay(value);
113             final LocalTime localTime = LocalTime.of(value, value);
114             final LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
115             final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.UTC);
116             final ZoneOffset zoneOffset = ZoneOffset.ofHours(value);
117             this.objects = new Object[] {
118                     // a Long
119                     value,
120                     // all concrete dates and times
121                     localDate, HijrahDate.from(localDate), JapaneseDate.from(localDate), MinguoDate.from(localDate), ThaiBuddhistDate.from(localDate),
122                     localDate, localTime, localDateTime, offsetDateTime, OffsetTime.of(localTime, zoneOffset), Year.of(value), YearMonth.of(value, value),
123                     ZonedDateTime.of(localDateTime, zoneOffset), zoneOffset, ZoneId.of(zoneOffset.getId()) };
124             IntStreams.range(100).forEach(i -> list.add(() -> charSequence));
125         }
126 
127         @Override
128         public String toString() {
129             return String.format("%s[%s, %s, %s, %s, %s]", getClass().getSimpleName(), charSequence, temporal, temporalAccessor, temporalAmount,
130                     Arrays.toString(objects));
131         }
132     }
133 
134     static class MyTemporal implements Temporal {
135 
136         private final String string;
137         private final int value;
138         private final Duration duration;
139         private final Instant instant;
140         private final Period period;
141 
142         MyTemporal(final String string) {
143             this.string = string;
144             this.value = Integer.parseInt(string);
145             this.instant = Instant.ofEpochMilli(value);
146             this.duration = Duration.between(instant, instant.plusMillis(value));
147             this.period = Period.ofDays(value);
148         }
149 
150         @Override
151         public long getLong(final TemporalField field) {
152             return instant.get(field);
153         }
154 
155         @Override
156         public boolean isSupported(final TemporalField field) {
157             return instant.isSupported(field);
158         }
159 
160         @Override
161         public boolean isSupported(final TemporalUnit unit) {
162             return instant.isSupported(unit);
163         }
164 
165         @Override
166         public Temporal plus(final long amountToAdd, final TemporalUnit unit) {
167             return instant.plus(amountToAdd, unit);
168         }
169 
170         @Override
171         public String toString() {
172             return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period);
173         }
174 
175         @Override
176         public long until(final Temporal endExclusive, final TemporalUnit unit) {
177             return instant.until(endExclusive, unit);
178         }
179 
180         @Override
181         public Temporal with(final TemporalField field, final long newValue) {
182             return instant.with(field, newValue);
183         }
184 
185     }
186 
187     static class MyTemporalAccessor implements TemporalAccessor {
188 
189         private final String string;
190         private final int value;
191         private final Instant instant;
192         private final Duration duration;
193         private final Period period;
194 
195         MyTemporalAccessor(final String string) {
196             this.string = string;
197             this.value = Integer.parseInt(string);
198             this.instant = Instant.ofEpochMilli(value);
199             this.duration = Duration.between(instant, instant.plusMillis(value));
200             this.period = Period.ofDays(value);
201         }
202 
203         @Override
204         public long getLong(final TemporalField field) {
205             return instant.get(field);
206         }
207 
208         @Override
209         public boolean isSupported(final TemporalField field) {
210             return instant.isSupported(field);
211         }
212 
213         @Override
214         public String toString() {
215             return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period);
216         }
217 
218     }
219 
220     static class MyTemporalAmount implements TemporalAmount {
221 
222         private final String string;
223         private final int value;
224         private final Instant instant;
225         private final Duration duration;
226         private final Period period;
227 
228         MyTemporalAmount(final String string) {
229             this.string = string;
230             this.value = Integer.parseInt(string);
231             this.instant = Instant.ofEpochMilli(value);
232             this.duration = Duration.between(instant, instant.plusMillis(value));
233             this.period = Period.ofDays(value);
234         }
235 
236         @Override
237         public Temporal addTo(final Temporal temporal) {
238             return duration.addTo(temporal);
239         }
240 
241         @Override
242         public long get(final TemporalUnit unit) {
243             return duration.get(unit);
244         }
245 
246         @Override
247         public List<TemporalUnit> getUnits() {
248             return duration.getUnits();
249         }
250 
251         @Override
252         public Temporal subtractFrom(final Temporal temporal) {
253             return duration.subtractFrom(temporal);
254         }
255 
256         @Override
257         public String toString() {
258             return String.format("%s[%s - %s - %s - %s]", getClass().getSimpleName(), string, instant, duration, period);
259         }
260 
261     }
262 
263     @Test
264     void testRecursive() {
265         final MyClass o1 = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4"));
266         // This gives you different instances of MyTemporalAccessor for 1 (and 2) that should be equals by reflection.
267         final MyClass o1Bis = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4"));
268         final MyClass o2 = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8"));
269         final MyClass o2Bis = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8"));
270         // MyTemporal
271         assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyTemporal("1"), new MyTemporal("1")).isEquals());
272         // MyTemporalAccessor
273         assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyTemporalAccessor("1"), new MyTemporalAccessor("1")).isEquals());
274         // MyCharSequence
275         assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyCharSequence("1"), new MyCharSequence("1")).isEquals());
276         // MyClass
277         assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1, o1).isEquals(), o1::toString);
278         assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1, o1Bis).isEquals(), o1::toString);
279         assertTrue(new EqualsBuilder().setTestRecursive(true).append(o2, o2).isEquals(), o2::toString);
280         assertTrue(new EqualsBuilder().setTestRecursive(true).append(o2, o2Bis).isEquals(), o2::toString);
281         assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1, o2).isEquals());
282         assertFalse(new EqualsBuilder().setTestRecursive(true).append(o2, o1).isEquals());
283     }
284 
285     @Test
286     void testRetention() throws Exception {
287         // The following should not retain memory.
288         for (int i = 0; i < Integer.getInteger("testRetention", 10_000); i++) {
289             final Class<?> clazz = TestClassBuilder.defineSimpleClass(getClass().getPackage().getName(), i);
290             assertTrue(new EqualsBuilder().setTestRecursive(true).append(clazz.newInstance(), clazz.newInstance()).isEquals());
291         }
292         // some retention is checked in super's after().
293     }
294 
295 }