From 8a1588ae295614858089c3382364825aa9f8b7e7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 10 Aug 2018 14:28:36 +0200 Subject: [PATCH] Add support for DataSize This commit provides a data type to represents a size in bytes and other standard unit. Issue: SPR-17154 --- .../springframework/util/unit/DataSize.java | 248 ++++++++++++++++++ .../springframework/util/unit/DataUnit.java | 82 ++++++ .../util/unit/package-info.java | 25 ++ .../util/unit/DataSizeTests.java | 158 +++++++++++ 4 files changed, 513 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/util/unit/DataSize.java create mode 100644 spring-core/src/main/java/org/springframework/util/unit/DataUnit.java create mode 100644 spring-core/src/main/java/org/springframework/util/unit/package-info.java create mode 100644 spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java new file mode 100644 index 0000000000..4921de8eba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java @@ -0,0 +1,248 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.util.unit; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A data size, such as '12MB'. + *

+ * This class models a size in terms of bytes and is immutable and thread-safe. + * + * @author Stephane Nicoll + * @since 5.1 + */ +public class DataSize implements Comparable { + + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^(\\d+)([a-zA-Z]{0,2})$"); + + /** + * Bytes per KiloByte. + */ + private static long BYTES_PER_KB = 1024; + + /** + * Bytes per MegaByte. + */ + private static long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per GigaByte. + */ + private static long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per TeraByte. + */ + private static long BYTES_PER_TB = BYTES_PER_GB * 1024; + + + private final long bytes; + + private DataSize(long bytes) { + this.bytes = bytes; + } + + + /** + * Obtain a {@link DataSize} representing the specified number of bytes. + * @param bytes the number of bytes + * @return a {@link DataSize} + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * Obtain a {@link DataSize} representing the specified number of kilobytes. + * @param kiloBytes the number of kilobytes + * @return a {@link DataSize} + */ + public static DataSize ofKiloBytes(long kiloBytes) { + return new DataSize(Math.multiplyExact(kiloBytes, BYTES_PER_KB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of megabytes. + * @param megaBytes the number of megabytes + * @return a {@link DataSize} + */ + public static DataSize ofMegaBytes(long megaBytes) { + return new DataSize(Math.multiplyExact(megaBytes, BYTES_PER_MB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of gigabytes. + * @param gigaBytes the number of gigabytes + * @return a {@link DataSize} + */ + public static DataSize ofGigaBytes(long gigaBytes) { + return new DataSize(Math.multiplyExact(gigaBytes, BYTES_PER_GB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of terabytes. + * @param teraBytes the number of terabytes + * @return a {@link DataSize} + */ + public static DataSize ofTeraBytes(long teraBytes) { + return new DataSize(Math.multiplyExact(teraBytes, BYTES_PER_TB)); + } + + /** + * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. + * @param amount the amount of the size, measured in terms of the unit + * @return a {@link DataSize} + */ + public static DataSize of(long amount, DataUnit unit) { + Assert.notNull(unit, "Unit must not be null"); + return new DataSize(Math.multiplyExact(amount, unit.getSize().toBytes())); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * {@link DataUnit#BYTES} if no unit is specified. + *

+ * Examples: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes" (where a minute is 60 seconds)
+	 * "20"   -- parses as "20 bytes"
+	 * 
+ * @param text the text to parse + * @return the parsed {@link DataSize} + * @see #parse(CharSequence, DataUnit) + */ + public static DataSize parse(CharSequence text) { + return parse(text, null); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

+ * The string starts with a number followed optionally by a unit matching one of the + * supported {@link DataUnit suffixes}. + *

+ * Examples: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes" (where a minute is 60 seconds)
+	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
+	 * 
+ * @param text the text to parse + * @return the parsed {@link DataSize} + */ + public static DataSize parse(CharSequence text, @Nullable DataUnit defaultUnit) { + Assert.notNull(text, "Text must not be null"); + try { + Matcher matcher = PATTERN.matcher(text); + Assert.state(matcher.matches(), "Does not match data size pattern"); + DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit); + Long amount = Long.parseLong(matcher.group(1)); + return DataSize.of(amount, unit); + } + catch (Exception ex) { + throw new IllegalArgumentException( + "'" + text + "' is not a valid data size", ex); + } + } + + private static DataUnit determineDataUnit(String suffix, + @Nullable DataUnit defaultUnit) { + defaultUnit = (defaultUnit != null ? defaultUnit : DataUnit.BYTES); + return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) + : defaultUnit); + } + + /** + * Return the number of bytes in this instance. + * @return the number of bytes + */ + public long toBytes() { + return this.bytes; + } + + /** + * Return the number of kilobytes in this instance. + * @return the number of kilobytes + */ + public long toKiloBytes() { + return this.bytes / BYTES_PER_KB; + } + + /** + * Return the number of megabytes in this instance. + * @return the number of megabytes + */ + public long toMegaBytes() { + return this.bytes / BYTES_PER_MB; + } + + /** + * Return the number of gigabytes in this instance. + * @return the number of gigabytes + */ + public long toGigaBytes() { + return this.bytes / BYTES_PER_GB; + } + + /** + * Return the number of terabytes in this instance. + * @return the number of terabytes + */ + public long toTeraBytes() { + return this.bytes / BYTES_PER_TB; + } + + @Override + public int compareTo(DataSize other) { + return Long.compare(this.bytes, other.bytes); + } + + @Override + public String toString() { + return String.format("%dB", this.bytes); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DataSize that = (DataSize) o; + return this.bytes == that.bytes; + } + + @Override + public int hashCode() { + return Long.hashCode(this.bytes); + } + +} + diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java new file mode 100644 index 0000000000..86158f6ed5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.util.unit; + +import java.util.Objects; + +/** + * A standard set of data size units. + * + * @author Stephane Nicoll + * @since 5.1 + */ +public enum DataUnit { + + /** + * Bytes. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * KiloByte. + */ + KILOBYTES("KB", DataSize.ofKiloBytes(1)), + + /** + * MegaByte. + */ + MEGABYTES("MB", DataSize.ofMegaBytes(1)), + + /** + * TeraByte. + */ + GIGABYTES("GB", DataSize.ofGigaBytes(1)), + + /** + * TeraByte. + */ + TERABYTES("TB", DataSize.ofTeraBytes(1)); + + private final String suffix; + + private final DataSize size; + + DataUnit(String suffix, DataSize size) { + this.suffix = suffix; + this.size = size; + } + + protected DataSize getSize() { + return this.size; + } + + /** + * Return the {@link DataUnit} matching the specified {@code suffix}. + * @param suffix one of the standard suffix + * @return the {@link DataUnit} matching the specified {@code suffix} + * @throws IllegalArgumentException if the suffix does not match any instance + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + if (Objects.equals(candidate.suffix, suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/unit/package-info.java b/spring-core/src/main/java/org/springframework/util/unit/package-info.java new file mode 100644 index 0000000000..fded112fe5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2018 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 + * + * http://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. + */ + +/** + * Useful unit data types. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.unit; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; \ No newline at end of file diff --git a/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java b/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java new file mode 100644 index 0000000000..091f97ca38 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.util.unit; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.*; + +/** + * Tests for {@link DataSize}. + * + * @author Stephane Nicoll + */ +public class DataSizeTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void ofBytesToBytes() { + assertEquals(1024, DataSize.ofBytes(1024).toBytes()); + } + + @Test + public void ofBytesToKiloBytes() { + assertEquals(1, DataSize.ofBytes(1024).toKiloBytes()); + } + + @Test + public void ofKiloBytesToKiloBytes() { + assertEquals(1024, DataSize.ofKiloBytes(1024).toKiloBytes()); + } + + @Test + public void ofKiloBytesToMegaBytes() { + assertEquals(1, DataSize.ofKiloBytes(1024).toMegaBytes()); + } + + @Test + public void ofMegaBytesToMegaBytes() { + assertEquals(1024, DataSize.ofMegaBytes(1024).toMegaBytes()); + } + + @Test + public void ofMegaBytesToGigaBytes() { + assertEquals(2, DataSize.ofMegaBytes(2048).toGigaBytes()); + } + + @Test + public void ofGigaBytesToGigaBytes() { + assertEquals(4096, DataSize.ofGigaBytes(4096).toGigaBytes()); + } + + @Test + public void ofGigaBytesToTeraBytes() { + assertEquals(4, DataSize.ofGigaBytes(4096).toTeraBytes()); + } + + @Test + public void ofTeraBytesToGigaBytes() { + assertEquals(1024, DataSize.ofTeraBytes(1).toGigaBytes()); + } + + @Test + public void ofWithBytesUnit() { + assertEquals(DataSize.ofBytes(10), DataSize.of(10, DataUnit.BYTES)); + } + + @Test + public void ofWithKiloBytesUnit() { + assertEquals(DataSize.ofKiloBytes(20), DataSize.of(20, DataUnit.KILOBYTES)); + } + + @Test + public void ofWithMegaBytesUnit() { + assertEquals(DataSize.ofMegaBytes(30), DataSize.of(30, DataUnit.MEGABYTES)); + } + + @Test + public void ofWithGigaBytesUnit() { + assertEquals(DataSize.ofGigaBytes(40), DataSize.of(40, DataUnit.GIGABYTES)); + } + + @Test + public void ofWithTeraBytesUnit() { + assertEquals(DataSize.ofTeraBytes(50), DataSize.of(50, DataUnit.TERABYTES)); + } + + @Test + public void parseWithDefaultUnitUsesBytes() { + assertEquals(DataSize.ofKiloBytes(1), DataSize.parse("1024")); + } + + @Test + public void parseWithNullDefaultUnitUsesBytes() { + assertEquals(DataSize.ofKiloBytes(1), DataSize.parse("1024", null)); + } + + @Test + public void parseWithCustomDefaultUnit() { + assertEquals(DataSize.ofKiloBytes(1), DataSize.parse("1", DataUnit.KILOBYTES)); + } + + @Test + public void parseWithBytes() { + assertEquals(DataSize.ofKiloBytes(1), DataSize.parse("1024B")); + } + + @Test + public void parseWithKiloBytes() { + assertEquals(DataSize.ofBytes(1024), DataSize.parse("1KB")); + } + + @Test + public void parseWithMegaBytes() { + assertEquals(DataSize.ofMegaBytes(4), DataSize.parse("4MB")); + } + + @Test + public void parseWithGigaBytes() { + assertEquals(DataSize.ofMegaBytes(1024), DataSize.parse("1GB")); + } + + @Test + public void parseWithTeraBytes() { + assertEquals(DataSize.ofTeraBytes(1), DataSize.parse("1TB")); + } + + @Test + public void toStringUsesBytes() { + assertEquals("1024B", DataSize.ofKiloBytes(1).toString()); + } + + @Test + public void parseWithUnsupportedUnit() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("3WB"); + this.thrown.expectMessage("is not a valid data size"); + DataSize.parse("3WB"); + } + +}