| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 |
| * |
| * https://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 org.apache.commons.lang3.builder; |
| |
| import static org.junit.jupiter.api.Assertions.assertFalse; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.LocalDate; |
| import java.time.LocalDateTime; |
| import java.time.LocalTime; |
| import java.time.OffsetDateTime; |
| import java.time.OffsetTime; |
| import java.time.Period; |
| import java.time.Year; |
| import java.time.YearMonth; |
| import java.time.ZoneId; |
| import java.time.ZoneOffset; |
| import java.time.ZonedDateTime; |
| import java.time.chrono.HijrahDate; |
| import java.time.chrono.JapaneseDate; |
| import java.time.chrono.MinguoDate; |
| import java.time.chrono.ThaiBuddhistDate; |
| import java.time.temporal.Temporal; |
| import java.time.temporal.TemporalAccessor; |
| import java.time.temporal.TemporalAmount; |
| import java.time.temporal.TemporalField; |
| import java.time.temporal.TemporalUnit; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.function.Supplier; |
| |
| import org.apache.commons.lang3.AbstractLangTest; |
| import org.apache.commons.lang3.stream.IntStreams; |
| import org.junit.jupiter.api.Test; |
| |
| /** |
| * Tests that {@link EqualsBuilder} works using reflection when types that implement JRE interfaces like TemporalAccessor, TemporalAmout, and CharSequence work. |
| */ |
| class EqualsBuilderReflectJreImplementationTest extends AbstractLangTest { |
| |
| static class MyCharSequence implements CharSequence { |
| |
| private final char[] chars; |
| |
| MyCharSequence(final char[] chars) { |
| this.chars = Arrays.copyOf(chars, chars.length); |
| } |
| |
| MyCharSequence(final char[] chars, final int start, final int end) { |
| this.chars = Arrays.copyOfRange(chars, start, end); |
| } |
| |
| MyCharSequence(final String string) { |
| this.chars = string.toCharArray(); |
| } |
| |
| @Override |
| public char charAt(final int index) { |
| return chars[index]; |
| } |
| |
| @Override |
| public int length() { |
| return chars.length; |
| } |
| |
| @Override |
| public CharSequence subSequence(final int start, final int end) { |
| return new MyCharSequence(chars, start, end); |
| } |
| |
| @Override |
| public String toString() { |
| return new String(chars); |
| } |
| } |
| |
| static class MyClass implements Cloneable { |
| |
| private final MyCharSequence charSequence; |
| private final MyTemporal temporal; |
| private final MyTemporalAccessor temporalAccessor; |
| private final MyTemporalAmount temporalAmount; |
| private final Object[] objects; |
| private final List<Supplier<?>> list = new ArrayList<>(); |
| |
| MyClass(final MyCharSequence charSequence, final MyTemporal temporal, final MyTemporalAccessor temporalAccessor, |
| final MyTemporalAmount temporalAmount) { |
| this.charSequence = charSequence; |
| this.temporal = temporal; |
| this.temporalAccessor = temporalAccessor; |
| this.temporalAmount = temporalAmount; |
| final int value = Integer.parseInt(charSequence.toString()); |
| final LocalDate localDate = LocalDate.ofEpochDay(value); |
| final LocalTime localTime = LocalTime.of(value, value); |
| final LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime); |
| final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.UTC); |
| final ZoneOffset zoneOffset = ZoneOffset.ofHours(value); |
| this.objects = new Object[] { |
| // a Long |
| value, |
| // all concrete dates and times |
| localDate, HijrahDate.from(localDate), JapaneseDate.from(localDate), MinguoDate.from(localDate), ThaiBuddhistDate.from(localDate), |
| localDate, localTime, localDateTime, offsetDateTime, OffsetTime.of(localTime, zoneOffset), Year.of(value), YearMonth.of(value, value), |
| ZonedDateTime.of(localDateTime, zoneOffset), zoneOffset, ZoneId.of(zoneOffset.getId()) }; |
| IntStreams.range(100).forEach(i -> list.add(() -> charSequence)); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s, %s, %s, %s, %s]", getClass().getSimpleName(), charSequence, temporal, temporalAccessor, temporalAmount, |
| Arrays.toString(objects)); |
| } |
| } |
| |
| static class MyTemporal implements Temporal { |
| |
| private final String string; |
| private final int value; |
| private final Duration duration; |
| private final Instant instant; |
| private final Period period; |
| |
| MyTemporal(final String string) { |
| this.string = string; |
| this.value = Integer.parseInt(string); |
| this.instant = Instant.ofEpochMilli(value); |
| this.duration = Duration.between(instant, instant.plusMillis(value)); |
| this.period = Period.ofDays(value); |
| } |
| |
| @Override |
| public long getLong(final TemporalField field) { |
| return instant.get(field); |
| } |
| |
| @Override |
| public boolean isSupported(final TemporalField field) { |
| return instant.isSupported(field); |
| } |
| |
| @Override |
| public boolean isSupported(final TemporalUnit unit) { |
| return instant.isSupported(unit); |
| } |
| |
| @Override |
| public Temporal plus(final long amountToAdd, final TemporalUnit unit) { |
| return instant.plus(amountToAdd, unit); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period); |
| } |
| |
| @Override |
| public long until(final Temporal endExclusive, final TemporalUnit unit) { |
| return instant.until(endExclusive, unit); |
| } |
| |
| @Override |
| public Temporal with(final TemporalField field, final long newValue) { |
| return instant.with(field, newValue); |
| } |
| |
| } |
| |
| static class MyTemporalAccessor implements TemporalAccessor { |
| |
| private final String string; |
| private final int value; |
| private final Instant instant; |
| private final Duration duration; |
| private final Period period; |
| |
| MyTemporalAccessor(final String string) { |
| this.string = string; |
| this.value = Integer.parseInt(string); |
| this.instant = Instant.ofEpochMilli(value); |
| this.duration = Duration.between(instant, instant.plusMillis(value)); |
| this.period = Period.ofDays(value); |
| } |
| |
| @Override |
| public long getLong(final TemporalField field) { |
| return instant.get(field); |
| } |
| |
| @Override |
| public boolean isSupported(final TemporalField field) { |
| return instant.isSupported(field); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s, %s, %s, %s]", getClass().getSimpleName(), string, instant, duration, period); |
| } |
| |
| } |
| |
| static class MyTemporalAmount implements TemporalAmount { |
| |
| private final String string; |
| private final int value; |
| private final Instant instant; |
| private final Duration duration; |
| private final Period period; |
| |
| MyTemporalAmount(final String string) { |
| this.string = string; |
| this.value = Integer.parseInt(string); |
| this.instant = Instant.ofEpochMilli(value); |
| this.duration = Duration.between(instant, instant.plusMillis(value)); |
| this.period = Period.ofDays(value); |
| } |
| |
| @Override |
| public Temporal addTo(final Temporal temporal) { |
| return duration.addTo(temporal); |
| } |
| |
| @Override |
| public long get(final TemporalUnit unit) { |
| return duration.get(unit); |
| } |
| |
| @Override |
| public List<TemporalUnit> getUnits() { |
| return duration.getUnits(); |
| } |
| |
| @Override |
| public Temporal subtractFrom(final Temporal temporal) { |
| return duration.subtractFrom(temporal); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s - %s - %s - %s]", getClass().getSimpleName(), string, instant, duration, period); |
| } |
| |
| } |
| |
| @Test |
| void testRecursive() { |
| final MyClass o1 = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4")); |
| // This gives you different instances of MyTemporalAccessor for 1 (and 2) that should be equals by reflection. |
| final MyClass o1Bis = new MyClass(new MyCharSequence("1"), new MyTemporal("2"), new MyTemporalAccessor("3"), new MyTemporalAmount("4")); |
| final MyClass o2 = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8")); |
| final MyClass o2Bis = new MyClass(new MyCharSequence("5"), new MyTemporal("6"), new MyTemporalAccessor("7"), new MyTemporalAmount("8")); |
| // MyTemporal |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyTemporal("1"), new MyTemporal("1")).isEquals()); |
| // MyTemporalAccessor |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyTemporalAccessor("1"), new MyTemporalAccessor("1")).isEquals()); |
| // MyCharSequence |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(new MyCharSequence("1"), new MyCharSequence("1")).isEquals()); |
| // MyClass |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1, o1).isEquals(), o1::toString); |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1, o1Bis).isEquals(), o1::toString); |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(o2, o2).isEquals(), o2::toString); |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(o2, o2Bis).isEquals(), o2::toString); |
| assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1, o2).isEquals()); |
| assertFalse(new EqualsBuilder().setTestRecursive(true).append(o2, o1).isEquals()); |
| } |
| |
| @Test |
| void testRetention() throws Exception { |
| // The following should not retain memory. |
| for (int i = 0; i < Integer.getInteger("testRetention", 10_000); i++) { |
| final Class<?> clazz = TestClassBuilder.defineSimpleClass(getClass().getPackage().getName(), i); |
| assertTrue(new EqualsBuilder().setTestRecursive(true).append(clazz.newInstance(), clazz.newInstance()).isEquals()); |
| } |
| // some retention is checked in super's after(). |
| } |
| |
| } |