From e18020821b7c248c8293d35b017b7423f3230792 Mon Sep 17 00:00:00 2001 From: Paul Millar Date: Mon, 30 Jul 2018 23:44:38 +0200 Subject: [PATCH] common: add standard byte-size parser Motivation: Parsing a string input that describes some capacity may allow byte-unit modifiers. There is currently no standard class to parse a String into the corresponding long value. Modification: Add parse method to the four representations of ByteUnit. Add ByteSizeParser class that follows a fluent design when parsing a String. Update dCache to make use of these two new features. Result: The "reserve space" and "update space" admin commands in SpaceManager no longer accept ISO units with incorrect case. For example, "kiB" and "kib" may no longer be used, but the correct ISO label, "KiB", must be used instead. Target: master Require-notes: yes Require-book: no Patch: https://rb.dcache.org/r/11087/ Acked-by: Albert Rossi --- .../java/org/dcache/util/ByteSizeParser.java | 214 ++++++++++ .../main/java/org/dcache/util/ByteUnits.java | 164 +++++++- .../org/dcache/util/ByteSizeParserTest.java | 382 ++++++++++++++++++ .../java/org/dcache/util/ByteUnitsTests.java | 278 +++++++++++++ .../SpaceManagerCommandLineInterface.java | 32 +- .../java/diskCacheV111/util/DiskSpace.java | 13 +- .../repository/RepositoryInterpreter.java | 7 +- 7 files changed, 1040 insertions(+), 50 deletions(-) create mode 100644 modules/common/src/main/java/org/dcache/util/ByteSizeParser.java create mode 100644 modules/common/src/test/java/org/dcache/util/ByteSizeParserTest.java diff --git a/modules/common/src/main/java/org/dcache/util/ByteSizeParser.java b/modules/common/src/main/java/org/dcache/util/ByteSizeParser.java new file mode 100644 index 00000000000..1d35366fc40 --- /dev/null +++ b/modules/common/src/main/java/org/dcache/util/ByteSizeParser.java @@ -0,0 +1,214 @@ +/* dCache - http://www.dcache.org/ + * + * Copyright (C) 2018 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.util; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.dcache.util.ByteUnits.Representation; + +import static java.util.Objects.requireNonNull; + +/** + * This class provides a fluent interface for parsing String representations + * of byte sizes. Examples include parsing {@literal "20 MiB"} to + * {@literal 20_971_520} and {@literal "3GB"} to either {@literal 3_000_000_000} + * or {@literal 3_221_225_472} depending on the representation (ISO or JEDEC, + * respectively). + */ +public class ByteSizeParser +{ + /** + * Allowed input types. + */ + public enum NumericalInput + { + INTEGER("[-+]?\\d*") { + @Override + protected long convert(String givenValue, ByteUnit givenUnits, ByteUnit targetUnits, Coercion coersion) { + return targetUnits.convert(Long.parseLong(givenValue), givenUnits); + } + }, + + FLOATING_POINT("[-+]?\\d*(?:\\.\\d*)?") { + @Override + protected long convert(String givenValue, ByteUnit givenUnits, ByteUnit targetUnits, Coercion coersion) { + double value = Double.parseDouble(givenValue); + return coersion.toLong(targetUnits.convert(value, givenUnits)); + } + }; + + private final String regularExpression; + + private NumericalInput(String re) + { + this.regularExpression = re; + } + + abstract long convert(String value, ByteUnit givenUnits, ByteUnit targetUnits, Coercion coersion); + } + + /** + * How to convert a floating point value to an integer value. + */ + public enum Coercion + { + CEIL { + @Override + public long toLong(double value) { + return (long) Math.ceil(value); + } + }, + + FLOOR { + @Override + public long toLong(double value) { + return (long) Math.floor(value); + } + }, + + ROUND { + @Override + public long toLong(double value) { + return (long) Math.round(value); + } + }; + + abstract long toLong(double value); + } + + /** + * Whether the units are required or are optional. If optional, a default + * of ByteUnit.BYTES is used. + */ + public enum UnitPresence + { + OPTIONAL, + + REQUIRED + } + + /** + * How to parse whitespace between the numerical part and the units. + */ + public enum Whitespace + { + REQUIRED(" "), OPTIONAL(" ?"), NOT_ALLOWED(""); + + private final String regularExpression; + + private Whitespace(String re) + { + this.regularExpression = re; + } + } + + public static class Builder + { + private final Representation representation; + private NumericalInput input = NumericalInput.FLOATING_POINT; + private Coercion coersion = Coercion.ROUND; + private Whitespace whitespace = Whitespace.OPTIONAL; + private UnitPresence unitPresence = UnitPresence.OPTIONAL; + private ByteUnit defaultUnits = ByteUnit.BYTES; + + private Builder(Representation representation) + { + this.representation = requireNonNull(representation); + } + + public Builder withCoersion(Coercion coersion) + { + this.coersion = requireNonNull(coersion); + return this; + } + + public Builder withInput(NumericalInput input) + { + this.input = requireNonNull(input); + return this; + } + + public Builder withWhitespace(Whitespace whitespace) + { + this.whitespace = requireNonNull(whitespace); + return this; + } + + public Builder withUnits(UnitPresence presence) + { + this.unitPresence = presence; + return this; + } + + /** + * Which ByteUnit to use if units are optional and the parse value + * does not specify any units. + */ + public Builder withDefaultUnits(ByteUnit units) + { + defaultUnits = units; + return this; + } + + public long parse(String value, ByteUnit targetUnits) throws NumberFormatException + { + String whitespaceAndUnit = whitespace.regularExpression + "(?\\p{Alpha}+)"; + String whitespaceAndUnitWithPresence = unitPresence == UnitPresence.REQUIRED + ? whitespaceAndUnit + : ("(?:" + whitespaceAndUnit + ")?"); + + Pattern pattern = Pattern.compile("(?" + input.regularExpression + ")" + + whitespaceAndUnitWithPresence); + Matcher m = pattern.matcher(value); + if (!m.matches()) { + throw new NumberFormatException("Bad input \"" + value + "\" does not match " + pattern); + } + + ByteUnit givenUnits = Optional.ofNullable(m.group("unit")) + .map(s -> representation.parse(s).orElseThrow(() -> new NumberFormatException("Unknown unit \"" + s + "\""))) + .orElse(defaultUnits); + + return input.convert(m.group("number"), givenUnits, targetUnits, coersion); + } + + public long parse(String value) throws NumberFormatException + { + return parse(value, ByteUnit.BYTES); + } + } + + private ByteSizeParser() + { + // prevent instantiation + } + + /** + * Create a new Builder that parses the input with the specified + * representation. By default, the parser accepts floating-point values, + * a space between numerical value and units is optional, and the numerical + * value is rounded to the nearest integer value. + * @param representation How a ByteUnit is represented. + * @return a Builder that may be configured before use in parsing a String. + */ + public static Builder using(Representation representation) + { + return new Builder(representation); + } +} diff --git a/modules/common/src/main/java/org/dcache/util/ByteUnits.java b/modules/common/src/main/java/org/dcache/util/ByteUnits.java index 3a75e537906..d75fee438a0 100644 --- a/modules/common/src/main/java/org/dcache/util/ByteUnits.java +++ b/modules/common/src/main/java/org/dcache/util/ByteUnits.java @@ -17,6 +17,8 @@ */ package org.dcache.util; +import java.util.Optional; + /** * Utility class to work with a ByteUnit */ @@ -33,16 +35,40 @@ private ByteUnits() } /** - * Any class that converts a ByteUnit into some String representation. + * Any class that describes a textual representation of ByteUnit. This + * allows conversion between the ByteUnit and that String representation. + *

+ * It is required that {@literal r.parse(r.of(unit)) == unit} for all + * {@literal ByteUnit unit} and {@literal Representation r} where + * {@literal r.of(unit)} returns non-exceptionally. + *

+ * It is allowed that the parse method returns the same ByteUnit value for + * different String values. This allows for common aliases; for example, + * parsing {@literal "kB"} (under JEDEC) as equivalent to the correct + * representation: {@literal "KB"}. */ public interface Representation { + /** + * Provide the String representation of a particular ByteUnit. + * @param unit the ByteUnit to represent + * @return the corresponding String representation. + * @throws UnsupportedOperationException if there is no representation for this ByteUnit. + */ String of(ByteUnit unit); + + /** + * Parse a representation of a ByteUnit. The value must match exactly. + * @param value The String representation + * @return The corresponding ByteUnit, if one matches. + * @throws NullPointerException if the value is null. + */ + Optional parse(String value); } /** - * Provides just the ISO prefix of a ByteUnit ("k", "Ki", "M", - * "Mi", ...). + * Just the ISO prefix of a ByteUnit ("k", "Ki", "M", "Mi", ...). The + * ByteUnit.BYTES unit is represented by the empty String. */ public static class IsoPrefix implements Representation { @@ -93,6 +119,41 @@ public String of(ByteUnit unit) throw new UnsupportedOperationException("no ISO prefix for " + unit.name()); } } + + @Override + public Optional parse(String value) + { + switch (value) { + case "": + return Optional.of(ByteUnit.BYTES); + // Note that the IEC symbol for kibi is defined as "Ki" and not "ki"! + case "Ki": + return Optional.of(ByteUnit.KiB); + case "Mi": + return Optional.of(ByteUnit.MiB); + case "Gi": + return Optional.of(ByteUnit.GiB); + case "Ti": + return Optional.of(ByteUnit.TiB); + case "Pi": + return Optional.of(ByteUnit.PiB); + case "Ei": + return Optional.of(ByteUnit.EiB); + case "k": + return Optional.of(ByteUnit.KB); + case "M": + return Optional.of(ByteUnit.MB); + case "G": + return Optional.of(ByteUnit.GB); + case "T": + return Optional.of(ByteUnit.TB); + case "P": + return Optional.of(ByteUnit.PB); + case "E": + return Optional.of(ByteUnit.EB); + } + return Optional.empty(); + } } /** @@ -148,10 +209,46 @@ public String of(ByteUnit unit) throw new UnsupportedOperationException("no ISO unit for " + unit.name()); } } + + @Override + public Optional parse(String value) + { + switch (value) { + case "B": + return Optional.of(ByteUnit.BYTES); + // Note that the IEC symbol for kibi is defined as "Ki" and not "ki"! + case "KiB": + return Optional.of(ByteUnit.KiB); + case "MiB": + return Optional.of(ByteUnit.MiB); + case "GiB": + return Optional.of(ByteUnit.GiB); + case "TiB": + return Optional.of(ByteUnit.TiB); + case "PiB": + return Optional.of(ByteUnit.PiB); + case "EiB": + return Optional.of(ByteUnit.EiB); + case "kB": + return Optional.of(ByteUnit.KB); + case "MB": + return Optional.of(ByteUnit.MB); + case "GB": + return Optional.of(ByteUnit.GB); + case "TB": + return Optional.of(ByteUnit.TB); + case "PB": + return Optional.of(ByteUnit.PB); + case "EB": + return Optional.of(ByteUnit.EB); + } + return Optional.empty(); + } } /** - * Provides the JEDEC prefix of a ByteUnit ("K", "M", "G", ...). + * Provides the JEDEC prefix of a ByteUnit ("K", "M", "G", ...). The + * ByteUnit.BYTES unit is represented by the empty String. */ public static class JedecPrefix implements Representation { @@ -185,6 +282,32 @@ public String of(ByteUnit unit) throw new UnsupportedOperationException("no JEDEC prefix for " + unit.name()); } } + + @Override + public Optional parse(String value) + { + switch (value) { + case "": + return Optional.of(ByteUnit.BYTES); + + // NB. JEDEC label is upper-case K, but as this is often confused + // we also accept lower-case k. + case "k": + case "K": + return Optional.of(ByteUnit.KiB); + case "M": + return Optional.of(ByteUnit.MiB); + case "G": + return Optional.of(ByteUnit.GiB); + case "T": + return Optional.of(ByteUnit.TiB); + case "P": + return Optional.of(ByteUnit.PiB); + case "E": + return Optional.of(ByteUnit.EiB); + } + return Optional.empty(); + } } /** @@ -222,12 +345,43 @@ public String of(ByteUnit unit) throw new UnsupportedOperationException("no JEDEC unit for " + unit.name()); } } + + @Override + public Optional parse(String value) + { + switch (value) { + case "B": + return Optional.of(ByteUnit.BYTES); + + // NB. JEDEC label is upper-case K, but as this is often confused + // we also accept lower-case k. + case "kB": + case "KB": + return Optional.of(ByteUnit.KiB); + + case "MB": + return Optional.of(ByteUnit.MiB); + + case "GB": + return Optional.of(ByteUnit.GiB); + + case "TB": + return Optional.of(ByteUnit.TiB); + + case "PB": + return Optional.of(ByteUnit.PiB); + + case "EB": + return Optional.of(ByteUnit.EiB); + } + return Optional.empty(); + } } /** * Provide the ISO prefix of a ByteUnit; i.e., without the final * units ('B'). Returns SI symbols (e.g., "k" for KILOBYTES, "M" for - * MEGABYTES) for Type.DECIMAL, and IEC symbols (e.g., "ki" KIBIBYTES, + * MEGABYTES) for Type.DECIMAL, and IEC symbols (e.g., "Ki" KIBIBYTES, * "Mi" for MEBIBYTES) for Type.BINARY. BYTES returns an empty string. */ public static Representation isoPrefix() diff --git a/modules/common/src/test/java/org/dcache/util/ByteSizeParserTest.java b/modules/common/src/test/java/org/dcache/util/ByteSizeParserTest.java new file mode 100644 index 00000000000..1c5f285e39a --- /dev/null +++ b/modules/common/src/test/java/org/dcache/util/ByteSizeParserTest.java @@ -0,0 +1,382 @@ +/* dCache - http://www.dcache.org/ + * + * Copyright (C) 2018 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.util; + +import org.junit.Test; + +import org.dcache.util.ByteSizeParser.Whitespace; + +import static org.dcache.util.ByteSizeParser.Coercion.*; +import static org.dcache.util.ByteSizeParser.NumericalInput.FLOATING_POINT; +import static org.dcache.util.ByteSizeParser.NumericalInput.INTEGER; +import static org.dcache.util.ByteSizeParser.UnitPresence.OPTIONAL; +import static org.dcache.util.ByteSizeParser.UnitPresence.REQUIRED; +import static org.dcache.util.ByteUnit.MiB; +import static org.dcache.util.ByteUnits.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +public class ByteSizeParserTest +{ + @Test + public void shouldParseIsoSymbolSimpleNumber() + { + long value = ByteSizeParser.using(isoSymbol()).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseIsoSymbolSpaceByteNumber() + { + long value = ByteSizeParser.using(isoSymbol()).parse("1 B"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseIsoSymbolNoSpaceByteNumber() + { + long value = ByteSizeParser.using(isoSymbol()).parse("1B"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseIsoSymbolNoSpaceKiloNumber() + { + long value = ByteSizeParser.using(isoSymbol()).parse("1kB"); + + assertThat(value, is(equalTo(1_000L))); + } + + @Test + public void shouldParseIsoSymbolNoSpaceKibiNumber() + { + long value = ByteSizeParser.using(isoSymbol()).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNonIsoSymbolKibi() + { + ByteSizeParser.using(isoSymbol()).parse("1Ki"); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNonIsoSymbolWrongKibi() + { + ByteSizeParser.using(isoSymbol()).parse("1kiB"); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNonIsoSymbolKilo() + { + ByteSizeParser.using(isoSymbol()).parse("1K"); + } + + @Test + public void shouldParseIsoPrefixSimpleNumber() + { + long value = ByteSizeParser.using(isoPrefix()).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseIsoPrefixNoSpaceKiloNumber() + { + long value = ByteSizeParser.using(isoPrefix()).parse("1k"); + + assertThat(value, is(equalTo(1_000L))); + } + + @Test + public void shouldParseIsoPrefixSpaceKiloNumber() + { + long value = ByteSizeParser.using(isoPrefix()).parse("1 k"); + + assertThat(value, is(equalTo(1_000L))); + } + + @Test + public void shouldParseIsoPrefixNoSpaceKibiNumber() + { + long value = ByteSizeParser.using(isoPrefix()).parse("1Ki"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseIsoPrefixSpaceKibiNumber() + { + long value = ByteSizeParser.using(isoPrefix()).parse("1 Ki"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecSymbolSimpleNumber() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseJedecSymbolSpaceByte() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1 B"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseJedecSymbolNoSpaceByte() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1B"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseJedecSymbolNoSpaceKiloNumber() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1kB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecSymbolSpaceKiloNumber() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1 kB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecSymbolNoSpaceKibiNumber() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1KB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecSymbolSpaceKibiNumber() + { + long value = ByteSizeParser.using(jedecSymbol()).parse("1 KB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecPrefixSimpleNumber() + { + long value = ByteSizeParser.using(jedecPrefix()).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseJedecPrefixNoSpaceKiloNumber() + { + long value = ByteSizeParser.using(jedecPrefix()).parse("1k"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecPrefixSpaceKiloNumber() + { + long value = ByteSizeParser.using(jedecPrefix()).parse("1 k"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecPrefixNoSpaceKibiNumber() + { + long value = ByteSizeParser.using(jedecPrefix()).parse("1K"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseJedecPrefixSpaceKibiNumber() + { + long value = ByteSizeParser.using(jedecPrefix()).parse("1 K"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseSimpleNumberWithDefaultUnits() + { + long value = ByteSizeParser.using(isoSymbol()).withDefaultUnits(MiB).parse("1"); + + assertThat(value, is(equalTo(1048576L))); + } + + @Test + public void shouldParseNumberWithUnitsWithDefaultUnits() + { + long value = ByteSizeParser.using(isoSymbol()).withDefaultUnits(MiB).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseIntegerWithIntegerInput() + { + long value = ByteSizeParser.using(isoSymbol()).withInput(INTEGER).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseFloatWithIntegerInput() + { + ByteSizeParser.using(isoSymbol()).withInput(INTEGER).parse("1.5KiB"); + } + + @Test + public void shouldParseIntegerWithFloatInput() + { + long value = ByteSizeParser.using(isoSymbol()).withInput(FLOATING_POINT).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseFloatWithFloatInput() + { + long value = ByteSizeParser.using(isoSymbol()).withInput(FLOATING_POINT).parse("1.5KiB"); + + assertThat(value, is(equalTo(1536L))); + } + + @Test + public void shouldParseNumberWithUnitsWithRequiredUnits() + { + long value = ByteSizeParser.using(isoSymbol()).withUnits(REQUIRED).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNumberWithoutUnitsWithRequiredUnits() + { + ByteSizeParser.using(isoSymbol()).withUnits(REQUIRED).parse("1"); + } + + @Test + public void shouldParseNumberWithUnitsWithOptionalUnits() + { + long value = ByteSizeParser.using(isoSymbol()).withUnits(OPTIONAL).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseNumberWithoutUnitsWithOptionalUnits() + { + long value = ByteSizeParser.using(isoSymbol()).withUnits(OPTIONAL).parse("1"); + + assertThat(value, is(equalTo(1L))); + } + + @Test + public void shouldParseNumberWithWhitespaceWithOptionalWhitespace() + { + long value = ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.OPTIONAL).parse("1 KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseNumberWithoutWhitespaceWithOptionalWhitespace() + { + long value = ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.OPTIONAL).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseNumberWithWhitespaceWithRequiredWhitespace() + { + long value = ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.REQUIRED).parse("1 KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNumberWithoutWhitespaceWithRequiredWhitespace() + { + ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.REQUIRED).parse("1KiB"); + } + + @Test(expected=NumberFormatException.class) + public void shouldNotParseNumberWithWhitespaceWithForbiddenWhitespace() + { + ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.NOT_ALLOWED).parse("1 KiB"); + } + + @Test + public void shouldParseNumberWithoutWhitespaceWithForbiddenWhitespace() + { + long value = ByteSizeParser.using(isoSymbol()).withWhitespace(Whitespace.NOT_ALLOWED).parse("1KiB"); + + assertThat(value, is(equalTo(1024L))); + } + + @Test + public void shouldParseNumberWithCeilingCoersion() + { + long value = ByteSizeParser.using(isoSymbol()).withCoersion(CEIL).parse("1.1KiB"); // 1126.4 + + assertThat(value, is(equalTo(1127L))); + } + + @Test + public void shouldParseNumberWithFloorCoersion() + { + long value = ByteSizeParser.using(isoSymbol()).withCoersion(FLOOR).parse("1.2KiB"); // 1228.8 + + assertThat(value, is(equalTo(1228L))); + } + + @Test + public void shouldParseNumberRoundUpWithRoundCoersion() + { + long value = ByteSizeParser.using(isoSymbol()).withCoersion(ROUND).parse("1.2KiB"); // 1228.8 + + assertThat(value, is(equalTo(1229L))); + } + + @Test + public void shouldParseNumberRoundDownWithRoundCoersion() + { + long value = ByteSizeParser.using(isoSymbol()).withCoersion(ROUND).parse("1.1KiB"); // 1126.4 + + assertThat(value, is(equalTo(1126L))); + } +} diff --git a/modules/common/src/test/java/org/dcache/util/ByteUnitsTests.java b/modules/common/src/test/java/org/dcache/util/ByteUnitsTests.java index 9b100761051..4d2b5fbe354 100644 --- a/modules/common/src/test/java/org/dcache/util/ByteUnitsTests.java +++ b/modules/common/src/test/java/org/dcache/util/ByteUnitsTests.java @@ -19,6 +19,8 @@ import org.junit.Test; +import java.util.Optional; + import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -343,4 +345,280 @@ public void shouldHaveCorrectExbiJedecSymbol() { assertThat(ByteUnits.jedecSymbol().of(ByteUnit.EiB), equalTo("EB")); } + + @Test + public void shouldParseIsoPrefixBytes() + { + assertThat(ByteUnits.isoPrefix().parse(""), equalTo(Optional.of(ByteUnit.BYTES))); + } + + @Test + public void shouldParseIsoPrefixKilo() + { + assertThat(ByteUnits.isoPrefix().parse("k"), equalTo(Optional.of(ByteUnit.KB))); + } + + @Test + public void shouldNotParseIsoSymbolKilo() + { + assertThat(ByteUnits.isoPrefix().parse("kB"), equalTo(Optional.empty())); + } + + @Test + public void shouldParseIsoPrefixKibi() + { + assertThat(ByteUnits.isoPrefix().parse("Ki"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldNotParseWrongIsoPrefixKibi() + { + assertThat(ByteUnits.isoPrefix().parse("ki"), equalTo(Optional.empty())); + } + + @Test + public void shouldParseIsoPrefixMega() + { + assertThat(ByteUnits.isoPrefix().parse("M"), equalTo(Optional.of(ByteUnit.MB))); + } + + @Test + public void shouldParseIsoPrefixMibi() + { + assertThat(ByteUnits.isoPrefix().parse("Mi"), equalTo(Optional.of(ByteUnit.MiB))); + } + + @Test + public void shouldParseIsoPrefixGiga() + { + assertThat(ByteUnits.isoPrefix().parse("G"), equalTo(Optional.of(ByteUnit.GB))); + } + + @Test + public void shouldParseIsoPrefixGibi() + { + assertThat(ByteUnits.isoPrefix().parse("Gi"), equalTo(Optional.of(ByteUnit.GiB))); + } + + @Test + public void shouldParseIsoPrefixTera() + { + assertThat(ByteUnits.isoPrefix().parse("T"), equalTo(Optional.of(ByteUnit.TB))); + } + + @Test + public void shouldParseIsoPrefixTibi() + { + assertThat(ByteUnits.isoPrefix().parse("Ti"), equalTo(Optional.of(ByteUnit.TiB))); + } + + @Test + public void shouldParseIsoPrefixPeta() + { + assertThat(ByteUnits.isoPrefix().parse("P"), equalTo(Optional.of(ByteUnit.PB))); + } + + @Test + public void shouldParseIsoPrefixPibi() + { + assertThat(ByteUnits.isoPrefix().parse("Pi"), equalTo(Optional.of(ByteUnit.PiB))); + } + + @Test + public void shouldParseIsoPrefixExa() + { + assertThat(ByteUnits.isoPrefix().parse("E"), equalTo(Optional.of(ByteUnit.EB))); + } + + @Test + public void shouldParseIsoPrefixEibi() + { + assertThat(ByteUnits.isoPrefix().parse("Ei"), equalTo(Optional.of(ByteUnit.EiB))); + } + + @Test + public void shouldParseIsoSymbolBytes() + { + assertThat(ByteUnits.isoSymbol().parse("B"), equalTo(Optional.of(ByteUnit.BYTES))); + } + + @Test + public void shouldNotParseIsoPrefixKilo() + { + assertThat(ByteUnits.isoSymbol().parse("k"), equalTo(Optional.empty())); + } + + @Test + public void shouldParseIsoSymbolKilo() + { + assertThat(ByteUnits.isoSymbol().parse("kB"), equalTo(Optional.of(ByteUnit.KB))); + } + + @Test + public void shouldParseIsoSymbolKibi() + { + assertThat(ByteUnits.isoSymbol().parse("KiB"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldNotParseWrongIsoSymbolKibi() + { + assertThat(ByteUnits.isoSymbol().parse("kiB"), equalTo(Optional.empty())); + } + + @Test + public void shouldParseIsoSymbolMega() + { + assertThat(ByteUnits.isoSymbol().parse("MB"), equalTo(Optional.of(ByteUnit.MB))); + } + + @Test + public void shouldParseIsoSymbolMibi() + { + assertThat(ByteUnits.isoSymbol().parse("MiB"), equalTo(Optional.of(ByteUnit.MiB))); + } + + @Test + public void shouldParseIsoSymbolGiga() + { + assertThat(ByteUnits.isoSymbol().parse("GB"), equalTo(Optional.of(ByteUnit.GB))); + } + + @Test + public void shouldParseIsoSymbolGibi() + { + assertThat(ByteUnits.isoSymbol().parse("GiB"), equalTo(Optional.of(ByteUnit.GiB))); + } + + @Test + public void shouldParseIsoSymbolTera() + { + assertThat(ByteUnits.isoSymbol().parse("TB"), equalTo(Optional.of(ByteUnit.TB))); + } + + @Test + public void shouldParseIsoSymbolTibi() + { + assertThat(ByteUnits.isoSymbol().parse("TiB"), equalTo(Optional.of(ByteUnit.TiB))); + } + + @Test + public void shouldParseIsoSymbolPeta() + { + assertThat(ByteUnits.isoSymbol().parse("PB"), equalTo(Optional.of(ByteUnit.PB))); + } + + @Test + public void shouldParseIsoSymbolPibi() + { + assertThat(ByteUnits.isoSymbol().parse("PiB"), equalTo(Optional.of(ByteUnit.PiB))); + } + + @Test + public void shouldParseIsoSymbolExa() + { + assertThat(ByteUnits.isoSymbol().parse("EB"), equalTo(Optional.of(ByteUnit.EB))); + } + + @Test + public void shouldParseIsoSymbolEibi() + { + assertThat(ByteUnits.isoSymbol().parse("EiB"), equalTo(Optional.of(ByteUnit.EiB))); + } + + @Test + public void shouldParseJedecPrefixBytes() + { + assertThat(ByteUnits.jedecPrefix().parse(""), equalTo(Optional.of(ByteUnit.BYTES))); + } + + @Test + public void shouldParseJedecPrefixKilo() + { + assertThat(ByteUnits.jedecPrefix().parse("K"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldParseJedecAlternativePrefixKilo() + { + assertThat(ByteUnits.jedecPrefix().parse("k"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldParseJedecPrefixMega() + { + assertThat(ByteUnits.jedecPrefix().parse("M"), equalTo(Optional.of(ByteUnit.MiB))); + } + + @Test + public void shouldParseJedecPrefixGiga() + { + assertThat(ByteUnits.jedecPrefix().parse("G"), equalTo(Optional.of(ByteUnit.GiB))); + } + + @Test + public void shouldParseJedecPrefixTera() + { + assertThat(ByteUnits.jedecPrefix().parse("T"), equalTo(Optional.of(ByteUnit.TiB))); + } + + @Test + public void shouldParseJedecPrefixPeta() + { + assertThat(ByteUnits.jedecPrefix().parse("P"), equalTo(Optional.of(ByteUnit.PiB))); + } + + @Test + public void shouldParseJedecPrefixExa() + { + assertThat(ByteUnits.jedecPrefix().parse("E"), equalTo(Optional.of(ByteUnit.EiB))); + } + + @Test + public void shouldParseJedecSymbolBytes() + { + assertThat(ByteUnits.jedecSymbol().parse("B"), equalTo(Optional.of(ByteUnit.BYTES))); + } + + @Test + public void shouldParseJedecSymbolKilo() + { + assertThat(ByteUnits.jedecSymbol().parse("KB"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldParseJedecAlternativeSymbolKilo() + { + assertThat(ByteUnits.jedecSymbol().parse("kB"), equalTo(Optional.of(ByteUnit.KiB))); + } + + @Test + public void shouldParseJedecSymbolMega() + { + assertThat(ByteUnits.jedecSymbol().parse("MB"), equalTo(Optional.of(ByteUnit.MiB))); + } + + @Test + public void shouldParseJedecSymbolGiga() + { + assertThat(ByteUnits.jedecSymbol().parse("GB"), equalTo(Optional.of(ByteUnit.GiB))); + } + + @Test + public void shouldParseJedecSymbolTera() + { + assertThat(ByteUnits.jedecSymbol().parse("TB"), equalTo(Optional.of(ByteUnit.TiB))); + } + + @Test + public void shouldParseJedecSymbolPeta() + { + assertThat(ByteUnits.jedecSymbol().parse("PB"), equalTo(Optional.of(ByteUnit.PiB))); + } + + @Test + public void shouldParseJedecSymbolExa() + { + assertThat(ByteUnits.jedecSymbol().parse("EB"), equalTo(Optional.of(ByteUnit.EiB))); + } } diff --git a/modules/dcache-spacemanager/src/main/java/diskCacheV111/services/space/SpaceManagerCommandLineInterface.java b/modules/dcache-spacemanager/src/main/java/diskCacheV111/services/space/SpaceManagerCommandLineInterface.java index 15fcc7c61f3..e05a8384dbd 100644 --- a/modules/dcache-spacemanager/src/main/java/diskCacheV111/services/space/SpaceManagerCommandLineInterface.java +++ b/modules/dcache-spacemanager/src/main/java/diskCacheV111/services/space/SpaceManagerCommandLineInterface.java @@ -9,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import java.io.Serializable; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -33,7 +32,7 @@ import dmg.util.command.Option; import org.dcache.auth.FQAN; -import org.dcache.util.ByteUnit; +import org.dcache.util.ByteSizeParser; import org.dcache.util.CDCExecutorServiceDecorator; import org.dcache.util.ColumnWriter; import org.dcache.util.SqlGlob; @@ -41,10 +40,7 @@ import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.Iterables.concat; import static com.google.common.primitives.Longs.tryParse; -import static java.lang.Double.parseDouble; -import static org.dcache.util.ByteUnit.BYTES; import static org.dcache.util.ByteUnits.isoPrefix; -import static org.dcache.util.ByteUnits.isoSymbol; public class SpaceManagerCommandLineInterface implements CellCommandListener { @@ -803,30 +799,8 @@ protected String executeInTransaction() throws DataAccessException private static long parseByteQuantity(String arg) { - // REVISIT: does this need really to be case insensitive? - - try { - String s = arg.endsWith("B") || arg.endsWith("b") ? - arg.substring(0, arg.length()-1).toUpperCase() : arg.toUpperCase(); - - ByteUnit units = Arrays.stream(ByteUnit.values()) - .skip(1) - .filter(u -> s.endsWith(isoPrefix().of(u).toUpperCase())) - .findFirst().orElse(BYTES); - - String num = (units == BYTES) ? s : - s.substring(0, s.length() - isoPrefix().of(units).length()); - - return checkNonNegative((long) (units.toBytes(parseDouble(num)) + 0.5)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot convert size specified (" + arg + ") to non-negative number. \n" - + "Valid definitions of size:\n" - + "\t\t - a number of bytes (long integer less than 2^64) \n" - + "\t\t - a number with a prefix; e.g., 100 k, 100 kB,\n" - + "\t\t 100 KiB, 100M, 100MB, 100MiB, 100G, 100GB,\n" - + "\t\t 100GiB, 10T, 10.5TB, 100TiB, 2P, 2.3PB, 1PiB\n" - + "see http://en.wikipedia.org/wiki/Gigabyte for an explanation."); - } + String s = arg.endsWith("B") ? arg.substring(0, arg.length()-1) : arg; + return checkNonNegative(ByteSizeParser.using(isoPrefix()).parse(s)); } private static long checkNonNegative(long size) diff --git a/modules/dcache/src/main/java/diskCacheV111/util/DiskSpace.java b/modules/dcache/src/main/java/diskCacheV111/util/DiskSpace.java index c6cb52f207c..d06c987c6dc 100644 --- a/modules/dcache/src/main/java/diskCacheV111/util/DiskSpace.java +++ b/modules/dcache/src/main/java/diskCacheV111/util/DiskSpace.java @@ -1,7 +1,6 @@ package diskCacheV111.util; -import java.util.Arrays; - +import org.dcache.util.ByteSizeParser; import org.dcache.util.ByteUnit; import org.dcache.util.ByteUnits.JedecPrefix; import org.dcache.util.ByteUnits.Representation; @@ -59,15 +58,7 @@ private static long parseUnitLong(String s) return Long.MAX_VALUE; } - String lastChar = s.substring(s.length()-1).toUpperCase(); - ByteUnit units = Arrays.stream(ByteUnit.values()) - .skip(1) - .filter(u -> u.hasType(BINARY) && lastChar.equals(jedecPrefix().of(u))) - .findFirst() - .orElse(BYTES); - - String number = units == BYTES ? s : s.substring(0, s.length() -1); - return units.toBytes(Long.parseLong(number)); + return ByteSizeParser.using(jedecPrefix()).parse(s.toUpperCase()); } @Override diff --git a/modules/dcache/src/main/java/org/dcache/pool/repository/RepositoryInterpreter.java b/modules/dcache/src/main/java/org/dcache/pool/repository/RepositoryInterpreter.java index b70e88019d5..74a46581fe4 100644 --- a/modules/dcache/src/main/java/org/dcache/pool/repository/RepositoryInterpreter.java +++ b/modules/dcache/src/main/java/org/dcache/pool/repository/RepositoryInterpreter.java @@ -1,7 +1,6 @@ package org.dcache.pool.repository; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +31,7 @@ import static java.util.stream.Collectors.joining; import static org.dcache.util.ByteUnit.*; +import static org.dcache.util.ByteUnits.jedecPrefix; public class RepositoryInterpreter implements CellCommandListener @@ -39,9 +39,6 @@ public class RepositoryInterpreter private static final Logger _log = LoggerFactory.getLogger(RepositoryInterpreter.class); - private static final Map STRING_TO_UNIT = - ImmutableMap.of("k", KiB, "m", MiB, "g", GiB, "t", TiB); - private Repository _repository; private final StatisticsListener _statisticsListener = new StatisticsListener(); @@ -233,7 +230,7 @@ public Serializable execute() throws CacheException, InterruptedException } else if (binary) { return listBinary(); } else if (stat != null) { - return listStatistics(STRING_TO_UNIT.getOrDefault(stat, BYTES)); + return listStatistics(jedecPrefix().parse(stat.toUpperCase()).orElse(BYTES)); } else { return listAll(); }