diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index 6f5c2843ad8..f000472333e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -34,28 +34,31 @@ import org.springframework.util.StringUtils; */ final class BitsCronField extends CronField { - private static final BitsCronField ZERO_NANOS; + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; - static { - ZERO_NANOS = new BitsCronField(Type.NANO); - ZERO_NANOS.bits.set(0); - } + @Nullable + private static BitsCronField zeroNanos = null; - private final BitSet bits; + // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices + private long bits; private BitsCronField(Type type) { super(type); - this.bits = new BitSet((int) type.range().getMaximum()); } /** * Return a {@code BitsCronField} enabled for 0 nano seconds. */ public static BitsCronField zeroNanos() { - return BitsCronField.ZERO_NANOS; + if (zeroNanos == null) { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + zeroNanos = field; + } + return zeroNanos; } /** @@ -98,11 +101,10 @@ final class BitsCronField extends CronField { */ public static BitsCronField parseDaysOfWeek(String value) { BitsCronField result = parseDate(value, Type.DAY_OF_WEEK); - BitSet bits = result.bits; - if (bits.get(0)) { + if (result.getBit(0)) { // cron supports 0 for Sunday; we use 7 like java.time - bits.set(7); - bits.clear(0); + result.setBit(7); + result.clearBit(0); } return result; } @@ -173,10 +175,10 @@ final class BitsCronField extends CronField { @Override public > T nextOrSame(T temporal) { int current = type().get(temporal); - int next = this.bits.nextSetBit(current); + int next = nextSetBit(current); if (next == -1) { temporal = type().rollForward(temporal); - next = this.bits.nextSetBit(0); + next = nextSetBit(0); } if (next == current) { return temporal; @@ -195,23 +197,54 @@ final class BitsCronField extends CronField { } } - BitSet bits() { - return this.bits; + boolean getBit(int index) { + return (this.bits & (1L << index)) != 0; + } + + private int nextSetBit(int fromIndex) { + long result = this.bits & (MASK << fromIndex); + if (result != 0) { + return Long.numberOfTrailingZeros(result); + } + else { + return -1; + } + } private void setBits(ValueRange range) { - this.bits.set((int) range.getMinimum(), (int) range.getMaximum() + 1); + if (range.getMinimum() == range.getMaximum()) { + setBit((int) range.getMinimum()); + } + else { + long minMask = MASK << range.getMinimum(); + long maxMask = MASK >>> - (range.getMaximum() + 1); + this.bits |= (minMask & maxMask); + } } private void setBits(ValueRange range, int delta) { - for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) { - this.bits.set(i); + if (delta == 1) { + setBits(range); } + else { + for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) { + setBit(i); + } + } + } + + private void setBit(int index) { + this.bits |= (1L << index); + } + + private void clearBit(int index) { + this.bits &= ~(1L << index); } @Override public int hashCode() { - return this.bits.hashCode(); + return Long.hashCode(this.bits); } @Override @@ -223,13 +256,25 @@ final class BitsCronField extends CronField { return false; } BitsCronField other = (BitsCronField) o; - return type() == other.type() && - this.bits.equals(other.bits); + return type() == other.type() && this.bits == other.bits; } @Override public String toString() { - return type() + " " + this.bits; + StringBuilder builder = new StringBuilder(type().toString()); + builder.append(" {"); + int i = nextSetBit(0); + if (i != -1) { + builder.append(i); + i = nextSetBit(i+1); + while (i != -1) { + builder.append(", "); + builder.append(i); + i = nextSetBit(i+1); + } + } + builder.append('}'); + return builder.toString(); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index 6076cd8773d..ca57bb962f5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -252,6 +252,11 @@ abstract class CronField { } return temporal; } + + @Override + public String toString() { + return this.field.toString(); + } } } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java deleted file mode 100644 index a22ca83209e..00000000000 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2020 the original author or authors. - * - * Licensed 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.springframework.scheduling.support; - -import java.util.BitSet; - -import org.assertj.core.api.AbstractAssert; - -/** - * @author Arjen Poutsma - */ -public class BitSetAssert extends AbstractAssert { - - private BitSetAssert(BitSet bitSet) { - super(bitSet, BitSetAssert.class); - } - - public static BitSetAssert assertThat(BitSet actual) { - return new BitSetAssert(actual); - } - - public BitSetAssert hasSet(int... indices) { - isNotNull(); - - for (int index : indices) { - if (!this.actual.get(index)) { - failWithMessage("Invalid disabled bit at @%d", index); - } - } - return this; - } - - public BitSetAssert hasSetRange(int min, int max) { - isNotNull(); - - for (int i = min; i < max; i++) { - if (!this.actual.get(i)) { - failWithMessage("Invalid disabled bit at @%d", i); - } - } - return this; - } - - public BitSetAssert hasUnset(int... indices) { - isNotNull(); - - for (int index : indices) { - if (this.actual.get(index)) { - failWithMessage("Invalid enabled bit at @%d", index); - } - } - return this; - } - - public BitSetAssert hasUnsetRange(int min, int max) { - isNotNull(); - - for (int i = min; i < max; i++) { - if (this.actual.get(i)) { - failWithMessage("Invalid enabled bit at @%d", i); - } - } - return this; - } - -} - diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java index 8cd20cfe569..e05fa738b5c 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -16,10 +16,13 @@ package org.springframework.scheduling.support; +import java.util.Arrays; + +import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.springframework.scheduling.support.BitSetAssert.assertThat; /** * @author Arjen Poutsma @@ -28,12 +31,12 @@ public class BitsCronFieldTests { @Test void parse() { - assertThat(BitsCronField.parseSeconds("42").bits()).hasUnsetRange(0, 41).hasSet(42).hasUnsetRange(43, 59); - assertThat(BitsCronField.parseMinutes("1,2,5,9").bits()).hasUnset(0).hasSet(1, 2).hasUnset(3,4).hasSet(5).hasUnsetRange(6,8).hasSet(9).hasUnsetRange(10,59); - assertThat(BitsCronField.parseSeconds("0-4,8-12").bits()).hasSetRange(0, 4).hasUnsetRange(5,7).hasSetRange(8, 12).hasUnsetRange(13,59); - assertThat(BitsCronField.parseHours("0-23/2").bits()).hasSet(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22).hasUnset(1,3,5,7,9,11,13,15,17,19,21,23); - assertThat(BitsCronField.parseDaysOfWeek("0").bits()).hasUnsetRange(0, 6).hasSet(7, 7); - assertThat(BitsCronField.parseSeconds("57/2").bits()).hasUnsetRange(0, 56).hasSet(57).hasUnset(58).hasSet(59); + assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59)); + assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(clear(0)).has(set(1, 2)).has(clearRange(3,4)).has(set(5)).has(clearRange(6,8)).has(set(9)).has(clearRange(10,59)); + assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59)); + assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); + assertThat(BitsCronField.parseDaysOfWeek("0")).has(clearRange(0, 6)).has(set(7, 7)); + assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59)); } @Test @@ -55,22 +58,78 @@ public class BitsCronFieldTests { @Test void parseWildcards() { - assertThat(BitsCronField.parseSeconds("*").bits()).hasSetRange(0, 60); - assertThat(BitsCronField.parseMinutes("*").bits()).hasSetRange(0, 60); - assertThat(BitsCronField.parseHours("*").bits()).hasSetRange(0, 23); - assertThat(BitsCronField.parseDaysOfMonth("*").bits()).hasUnset(0).hasSetRange(1, 31); - assertThat(BitsCronField.parseDaysOfMonth("?").bits()).hasUnset(0).hasSetRange(1, 31); - assertThat(BitsCronField.parseMonth("*").bits()).hasUnset(0).hasSetRange(1, 12); - assertThat(BitsCronField.parseDaysOfWeek("*").bits()).hasUnset(0).hasSetRange(1, 7); - assertThat(BitsCronField.parseDaysOfWeek("?").bits()).hasUnset(0).hasSetRange(1, 7); + assertThat(BitsCronField.parseSeconds("*")).has(setRange(0, 60)); + assertThat(BitsCronField.parseMinutes("*")).has(setRange(0, 60)); + assertThat(BitsCronField.parseHours("*")).has(setRange(0, 23)); + assertThat(BitsCronField.parseDaysOfMonth("*")).has(clear(0)).has(setRange(1, 31)); + assertThat(BitsCronField.parseDaysOfMonth("?")).has(clear(0)).has(setRange(1, 31)); + assertThat(BitsCronField.parseMonth("*")).has(clear(0)).has(setRange(1, 12)); + assertThat(BitsCronField.parseDaysOfWeek("*")).has(clear(0)).has(setRange(1, 7)); + assertThat(BitsCronField.parseDaysOfWeek("?")).has(clear(0)).has(setRange(1, 7)); } @Test void names() { - assertThat(((BitsCronField)CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC")).bits()) - .hasUnset(0).hasSetRange(1, 12); - assertThat(((BitsCronField)CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT")).bits()) - .hasUnset(0).hasSetRange(1, 7); + assertThat(((BitsCronField)CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC"))) + .has(clear(0)).has(setRange(1, 12)); + assertThat(((BitsCronField)CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT"))) + .has(clear(0)).has(setRange(1, 7)); + } + + private static Condition set(int... indices) { + return new Condition(String.format("set bits %s", Arrays.toString(indices))) { + @Override + public boolean matches(BitsCronField value) { + for (int index : indices) { + if (!value.getBit(index)) { + return false; + } + } + return true; + } + }; + } + + private static Condition setRange(int min, int max) { + return new Condition(String.format("set range %d-%d", min, max)) { + @Override + public boolean matches(BitsCronField value) { + for (int i = min; i < max; i++) { + if (!value.getBit(i)) { + return false; + } + } + return true; + } + }; + } + + private static Condition clear(int... indices) { + return new Condition(String.format("clear bits %s", Arrays.toString(indices))) { + @Override + public boolean matches(BitsCronField value) { + for (int index : indices) { + if (value.getBit(index)) { + return false; + } + } + return true; + } + }; + } + + private static Condition clearRange(int min, int max) { + return new Condition(String.format("clear range %d-%d", min, max)) { + @Override + public boolean matches(BitsCronField value) { + for (int i = min; i < max; i++) { + if (value.getBit(i)) { + return false; + } + } + return true; + } + }; } }