diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef862 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.idea/ diff --git a/src/main/java/org/greenbytes/http/sfv/BooleanItem.java b/src/main/java/org/greenbytes/http/sfv/BooleanItem.java index f1738df..cdb71af 100644 --- a/src/main/java/org/greenbytes/http/sfv/BooleanItem.java +++ b/src/main/java/org/greenbytes/http/sfv/BooleanItem.java @@ -22,15 +22,20 @@ private BooleanItem(boolean value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.BOOLEAN; + } + /** * Creates a {@link BooleanItem} instance representing the specified * {@code boolean} value. - * + * * @param value * a {@code boolean} value. * @return a {@link BooleanItem} representing {@code value}. */ - public static BooleanItem valueOf(boolean value) { + public static BooleanItem of(boolean value) { return value ? TRUE : FALSE; } @@ -44,6 +49,11 @@ public BooleanItem withParams(Parameters params) { return new BooleanItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public BooleanItem withParamValuesOf(Object... obs) { + return new BooleanItem(this.value, Parameters.valueOf(obs)); + } + @Override public StringBuilder serializeTo(StringBuilder sb) { sb.append(value ? "?1" : "?0"); diff --git a/src/main/java/org/greenbytes/http/sfv/ByteSequenceItem.java b/src/main/java/org/greenbytes/http/sfv/ByteSequenceItem.java index 9760fc5..fdda778 100644 --- a/src/main/java/org/greenbytes/http/sfv/ByteSequenceItem.java +++ b/src/main/java/org/greenbytes/http/sfv/ByteSequenceItem.java @@ -24,6 +24,11 @@ private ByteSequenceItem(byte[] value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.BYTESEQUENCE; + } + /** * Creates a {@link ByteSequenceItem} instance representing the specified * {@code byte[]} value. @@ -41,6 +46,11 @@ public ByteSequenceItem withParams(Parameters params) { return new ByteSequenceItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public ByteSequenceItem withParamValuesOf(Object... obs) { + return new ByteSequenceItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; diff --git a/src/main/java/org/greenbytes/http/sfv/DateItem.java b/src/main/java/org/greenbytes/http/sfv/DateItem.java index b454d8b..817b3ca 100644 --- a/src/main/java/org/greenbytes/http/sfv/DateItem.java +++ b/src/main/java/org/greenbytes/http/sfv/DateItem.java @@ -25,6 +25,11 @@ private DateItem(long value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.DATE; + } + /** * Creates an {@link DateItem} instance representing the specified * {@code long} value. @@ -42,12 +47,17 @@ public DateItem withParams(Parameters params) { return new DateItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public DateItem withParamValuesOf(Object... obs) { + return new DateItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; } - public StringBuilder serializeToNoParams(StringBuilder sb) { + private StringBuilder serializeToNoParams(StringBuilder sb) { return sb.append('@').append(value); } @@ -61,6 +71,7 @@ public String serialize() { return serializeTo(new StringBuilder()).toString(); } + @Override public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function formatter) { String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : ""; String classn = formatter.apply(this.getClass()); diff --git a/src/main/java/org/greenbytes/http/sfv/DecimalItem.java b/src/main/java/org/greenbytes/http/sfv/DecimalItem.java index 70e5c62..266271c 100644 --- a/src/main/java/org/greenbytes/http/sfv/DecimalItem.java +++ b/src/main/java/org/greenbytes/http/sfv/DecimalItem.java @@ -36,6 +36,11 @@ private DecimalItem(long value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.DECIMAL; + } + /** * Creates a {@link DecimalItem} instance representing the specified * {@code long} value, where the implied divisor is {@code 1000}. @@ -61,17 +66,34 @@ public static DecimalItem valueOf(BigDecimal value) { return valueOf(permille.longValue()); } + /** + * Creates a {@link DecimalItem} instance representing the specified + * {@code Double} value, with potential rounding. + * + * @param value + * a {@code Double} value. + * @return a {@link DecimalItem} representing {@code value}. + */ + public static DecimalItem valueOf(double value) { + return valueOf(BigDecimal.valueOf(value)); + } + @Override public DecimalItem withParams(Parameters params) { return new DecimalItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public DecimalItem withParamValuesOf(Object... obs) { + return new DecimalItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; } - public StringBuilder serializeToNoParams(StringBuilder sb) { + private StringBuilder serializeToNoParams(StringBuilder sb) { String sign = value < 0 ? "-" : ""; @@ -105,6 +127,7 @@ public String serialize() { return serializeTo(new StringBuilder(20)).toString(); } + @Override public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function formatter) { String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : ""; String classn = formatter.apply(this.getClass()); diff --git a/src/main/java/org/greenbytes/http/sfv/Dictionary.java b/src/main/java/org/greenbytes/http/sfv/Dictionary.java index a977df5..a88c85d 100644 --- a/src/main/java/org/greenbytes/http/sfv/Dictionary.java +++ b/src/main/java/org/greenbytes/http/sfv/Dictionary.java @@ -1,7 +1,9 @@ package org.greenbytes.http.sfv; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.function.Function; /** @@ -19,6 +21,11 @@ private Dictionary(Map> value) { this.value = Collections.unmodifiableMap(Utils.checkKeys(value)); } + @Override + public SfDataType getType() { + return SfDataType.DICTIONARY; + } + /** * Creates a {@link Dictionary} instance representing the specified * {@code Map} value. @@ -31,10 +38,39 @@ private Dictionary(Map> value) { * a {@code Map} value * @return a {@link Dictionary} representing {@code value}. */ - public static Dictionary valueOf(Map> value) { + public static Dictionary of(Map> value) { return new Dictionary(value); } + /** + * Creates a {@link Dictionary} instance representing the values + * (key/value pairs) + * + * @param obs + * a sequence of key/value pairs + * @return a {@link Dictionary} representing the {@code values}. + */ + public static Dictionary valueOf(Object... obs) { + if (obs.length % 2 != 0) { + throw new IllegalArgumentException("requires even number of arguments, got: " + obs.length); + } else { + Map> map = new LinkedHashMap<>(); + for (int i = 0; i < obs.length; i += 2) { + String key = obs[i].toString(); + Object value = obs[i + 1]; + if (map.containsKey(key)) { + throw new IllegalArgumentException("key " + key + " already exists"); + } + if (value instanceof ListElement) { + map.put(key, (ListElement) value); + } else { + map.put(key, Utils.asItem(obs[i + 1])); + } + } + return of(map); + } + } + @Override public Map> get() { return value; @@ -79,4 +115,19 @@ public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Func } return sb; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Dictionary)) { + return false; + } else { + Dictionary that = (Dictionary) o; + return Objects.equals(value, that.value); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } diff --git a/src/main/java/org/greenbytes/http/sfv/DisplayStringItem.java b/src/main/java/org/greenbytes/http/sfv/DisplayStringItem.java index d791e27..16e5cf8 100755 --- a/src/main/java/org/greenbytes/http/sfv/DisplayStringItem.java +++ b/src/main/java/org/greenbytes/http/sfv/DisplayStringItem.java @@ -20,6 +20,11 @@ private DisplayStringItem(String value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.DISPLAYSTRING; + } + /** * Creates a {@link DisplayStringItem} instance representing the specified * {@code String} value. @@ -37,6 +42,11 @@ public DisplayStringItem withParams(Parameters params) { return new DisplayStringItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public DisplayStringItem withParamValuesOf(Object... obs) { + return new DisplayStringItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; diff --git a/src/main/java/org/greenbytes/http/sfv/InnerList.java b/src/main/java/org/greenbytes/http/sfv/InnerList.java index d3debc5..48c0983 100644 --- a/src/main/java/org/greenbytes/http/sfv/InnerList.java +++ b/src/main/java/org/greenbytes/http/sfv/InnerList.java @@ -1,8 +1,10 @@ package org.greenbytes.http.sfv; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; /** * Represents an Inner List. @@ -21,23 +23,56 @@ private InnerList(List> value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.INNERLIST; + } + /** * Creates an {@link InnerList} instance representing the specified * {@code List} value. - * + * * @param value * a {@code List} value. * @return a {@link InnerList} representing {@code value}. */ - public static InnerList valueOf(List> value) { + public static InnerList of(List> value) { return new InnerList(value, Parameters.EMPTY); } + /** + * Creates an {@link InnerList} instance representing the specified + * {@code Item} values. + * + * @param values + * {@code Item} values. + * @return a {@link InnerList} representing {@code values}. + */ + public static InnerList of(Item... values) { + return of(Arrays.stream(values).collect(Collectors.toList())); + } + + /** + * Creates an {@link InnerList} instance representing the specified + * {@linkplain Object} values after best-effort conversion to {@linkplain Item}s. + * + * @param values {@link Object}s to populate the list with + * @return a {@link InnerList} representing {@code values}. + */ + public static InnerList valueOf(Object... values) { + return of(Arrays.stream(values).map(Utils::asItem).collect(Collectors.toList())); + } + @Override public InnerList withParams(Parameters params) { return new InnerList(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public InnerList withParamValuesOf(Object... obs) { + return new InnerList(this.value, Parameters.valueOf(obs)); + } + private StringBuilder serializeToNoParams(StringBuilder sb) { String separator = ""; @@ -59,6 +94,7 @@ public StringBuilder serializeTo(StringBuilder sb) { return params.serializeTo(serializeToNoParams(sb)); } + @Override public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function formatter) { String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : ""; String classn = formatter.apply(this.getClass()); diff --git a/src/main/java/org/greenbytes/http/sfv/IntegerItem.java b/src/main/java/org/greenbytes/http/sfv/IntegerItem.java index 99a7eda..91b3a12 100644 --- a/src/main/java/org/greenbytes/http/sfv/IntegerItem.java +++ b/src/main/java/org/greenbytes/http/sfv/IntegerItem.java @@ -25,15 +25,20 @@ private IntegerItem(long value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.INTEGER; + } + /** * Creates an {@link IntegerItem} instance representing the specified * {@code long} value. - * + * * @param value * a {@code long} value. * @return a {@link IntegerItem} representing {@code value}. */ - public static IntegerItem valueOf(long value) { + public static IntegerItem of(long value) { return new IntegerItem(value, Parameters.EMPTY); } @@ -42,6 +47,11 @@ public IntegerItem withParams(Parameters params) { return new IntegerItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public IntegerItem withParamValuesOf(Object... obs) { + return new IntegerItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; diff --git a/src/main/java/org/greenbytes/http/sfv/OuterList.java b/src/main/java/org/greenbytes/http/sfv/OuterList.java index d74716e..009c5a1 100644 --- a/src/main/java/org/greenbytes/http/sfv/OuterList.java +++ b/src/main/java/org/greenbytes/http/sfv/OuterList.java @@ -1,8 +1,10 @@ package org.greenbytes.http.sfv; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; /** * Represents a List. @@ -18,18 +20,46 @@ private OuterList(List> value) { this.value = Objects.requireNonNull(value, "value must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.LIST; + } + /** * Creates an {@link OuterList} instance representing the specified - * {@code List} value. - * + * {@linkplain Object} values after best-effort conversion to {@linkplain Item}. + * + * @param values {@link Object}s to populate the list with + * @return a {@link OuterList} representing {@code values}. + */ + public static OuterList valueOf(Object... values) { + return of(Arrays.stream(values).map(Utils::asListElement).collect(Collectors.toList())); + } + + /** + * Creates an {@link OuterList} instance representing the specified + * {@linkplain List} value. + * * @param value * a {@code List} value. * @return a {@link OuterList} representing {@code value}. */ - public static OuterList valueOf(List> value) { + public static OuterList of(List> value) { return new OuterList(value); } + /** + * Creates an {@link OuterList} instance representing the specified + * {@code ListElement} values. + * + * @param values + * {@code ListItem} values. + * @return a {@link OuterList} representing {@code values}. + */ + public static OuterList of(ListElement... values) { + return of(Arrays.stream(values).collect(Collectors.toList())); + } + @Override public StringBuilder serializeTo(StringBuilder sb) { String separator = ""; diff --git a/src/main/java/org/greenbytes/http/sfv/Parameterizable.java b/src/main/java/org/greenbytes/http/sfv/Parameterizable.java index 4fe831f..76b803a 100644 --- a/src/main/java/org/greenbytes/http/sfv/Parameterizable.java +++ b/src/main/java/org/greenbytes/http/sfv/Parameterizable.java @@ -20,6 +20,28 @@ public interface Parameterizable extends Type { */ Parameterizable withParams(Parameters params); + /** + * Given an existing {@link Item}, return a new instance with the specified + * {@link Parameters}, specified as a sequence of {@linkplain Object}s. + *

+ * The sequence consists of name/value pairs, where the length needs to be + * even-numbered. Each pair consist of a parameter name (thus will + * be converted to a {@link String}), and an {@linkplain Object} that + * will be converted on a best-effort to an {@link Item}. + *

+ * Example from Section 2.1 of RFC 9651 + *

+     * IntegerItem fooWithParams = IntegerItem.of(2).
+     *                               withParamsOfValue("foourl", "https://foo.example.com");
+     * 
+ * + * @param obs sequence of name/valid pairs + * @return new instance with specified {@link Parameters}, as converted from + * the Object sequence.. + */ + Parameterizable withParamValuesOf(Object... obs); + /** * Get the {@link Parameters} of this {@link Item}. * diff --git a/src/main/java/org/greenbytes/http/sfv/Parameters.java b/src/main/java/org/greenbytes/http/sfv/Parameters.java index 6beb2f0..6233207 100644 --- a/src/main/java/org/greenbytes/http/sfv/Parameters.java +++ b/src/main/java/org/greenbytes/http/sfv/Parameters.java @@ -1,6 +1,5 @@ package org.greenbytes.http.sfv; -import java.math.BigDecimal; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -36,15 +35,41 @@ private Parameters(Map value) { * Note that the {@link Map} implementation that is used here needs to * iterate predictably based on insertion order, such as * {@link java.util.LinkedHashMap}. - * + * * @param value * a {@code Map} value * @return a {@link Parameters} representing {@code value}. */ - public static Parameters valueOf(Map value) { + public static Parameters of(Map value) { return new Parameters(value); } + /** + * Creates an unmodifiable {@link Parameters} instance representing + * the specified {@linkplain Object}s. + * @param obs (needs to be an even-number of {@linkplain Object}s) + * @return a {@link Parameters} representing {@code obs}. + */ + public static Parameters valueOf(Object... obs) { + if (obs.length == 1 && obs[0] instanceof Map) { + throw new IllegalArgumentException("requires even number of arguments, got: " + + obs[0].getClass().getName() + " - did you mean to use 'of()'?"); + } + if (obs.length % 2 != 0) { + throw new IllegalArgumentException("requires even number of arguments, got: " + obs.length); + } else { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < obs.length; i += 2) { + String key = obs[i].toString(); + if (map.containsKey(key)) { + throw new IllegalArgumentException("key " + key + " already exists"); + } + map.put(key, obs[i + 1]); + } + return of(map); + } + } + /** * Serialize this parameter to a {@linkplain StringBuilder} * @param sb to serialize to @@ -69,14 +94,23 @@ public String serialize() { return serializeTo(new StringBuilder()).toString(); } - public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function formatter) { + /** + * Serialize debug information to an existing {@link StringBuilder}. + * + * @param sb + * where to serialize to + * @param indentLevel how much to indent + * @param classFormatter to format the classname when desires (can be a function that returns an empty string) + * @return the {@link StringBuilder} so calls can be chained. + */ + public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function classFormatter) { if (!delegate.isEmpty()) { String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : ""; - String classn = formatter.apply(this.getClass()); + String classn = classFormatter.apply(this.getClass()); sb.append(indent).append(serialize()).append(classn).append("\n"); for (Map.Entry> e : delegate.entrySet()) { sb.append(" " + indent).append(e.getKey()).append(" -> "); - e.getValue().serializeToForDebug(sb, 0, formatter); + e.getValue().serializeToForDebug(sb, 0, classFormatter); } return sb; } else { @@ -89,7 +123,7 @@ private static Map> checkAndTransformMap(Map map Objects.requireNonNull(map, "Map must not be null").size()); for (Map.Entry entry : map.entrySet()) { String key = Utils.checkKey(entry.getKey()); - Item value = asItem(key, entry.getValue()); + Item value = Utils.asBareItem(entry.getValue()); if (!value.getParams().isEmpty()) { throw new IllegalArgumentException("Parameter value for '" + key + "' must be bare item (no parameters)"); } @@ -98,32 +132,6 @@ private static Map> checkAndTransformMap(Map map return result; } - private static Item asItem(String key, Object o) { - if (o instanceof Item) { - if (o instanceof Parameterizable) { - Parameterizable p = ((Parameterizable)o); - if (!p.getParams().isEmpty()) { - throw new IllegalArgumentException("Can't map value for parameter '" + key + "': " + o.getClass() + " carries parameters"); - } - } - return (Item) o; - } else if (o instanceof Integer) { - return IntegerItem.valueOf(((Integer) o).longValue()); - } else if (o instanceof Long) { - return IntegerItem.valueOf((Long) o); - } else if (o instanceof String) { - return StringItem.valueOf((String) o); - } else if (o instanceof Boolean) { - return BooleanItem.valueOf((Boolean) o); - } else if (o instanceof byte[]) { - return ByteSequenceItem.valueOf((byte[]) o); - } else if (o instanceof BigDecimal) { - return DecimalItem.valueOf((BigDecimal)o); - } else { - throw new IllegalArgumentException("Can't map value for parameter '" + key + "': " + o.getClass()); - } - } - // delegate methods, autogenerated public void clear() { @@ -160,10 +168,6 @@ public Set>> entrySet() { return delegate.entrySet(); } - public boolean equals(Object o) { - return Objects.equals(delegate, o); - } - @Override public void forEach(BiConsumer> action) { delegate.forEach(action); @@ -178,10 +182,6 @@ public Item getOrDefault(Object key, Item defaultValue) { return delegate.getOrDefault(key, defaultValue); } - public int hashCode() { - return delegate.hashCode(); - } - public boolean isEmpty() { return delegate.isEmpty(); } @@ -240,4 +240,19 @@ public int size() { public Collection> values() { return delegate.values(); } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Parameters)) { + return false; + } else { + Parameters that = (Parameters) o; + return Objects.equals(delegate, that.delegate); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } } diff --git a/src/main/java/org/greenbytes/http/sfv/Parser.java b/src/main/java/org/greenbytes/http/sfv/Parser.java index 2c85907..c8e8443 100644 --- a/src/main/java/org/greenbytes/http/sfv/Parser.java +++ b/src/main/java/org/greenbytes/http/sfv/Parser.java @@ -137,7 +137,7 @@ private DateItem internalParseBareDate() { return DateItem.valueOf(sign * l); } - private NumberItem internalParseBareIntegerOrDecimal() { + private NumberItem internalParseBareIntegerOrDecimal() { boolean isDecimal = false; int sign = 1; StringBuilder inputNumber = new StringBuilder(20); @@ -175,7 +175,7 @@ private NumberItem internalParseBareIntegerOrDecimal() { if (!isDecimal) { long l = Long.parseLong(inputNumber.toString()); - return IntegerItem.valueOf(sign * l); + return IntegerItem.of(sign * l); } else { long l = computeLong(inputNumber); return DecimalItem.valueOf(sign * l); @@ -240,7 +240,7 @@ private StringItem internalParseBareString() { outputString.append(c); } else { if (c == '"') { - return StringItem.valueOf(outputString.toString()); + return StringItem.of(outputString.toString()); } else if (c < 0x20 || c >= 0x7f) { throw complaint("Invalid character in String at position " + position()); } else { @@ -362,7 +362,7 @@ private TokenItem internalParseBareToken() { } } - return TokenItem.valueOf(outputString.toString()); + return TokenItem.of(outputString.toString()); } private TokenItem internalParseToken() { @@ -434,7 +434,7 @@ private BooleanItem internalParseBareBoolean() { throw complaint(String.format("Expected '0' or '1' in Boolean, found '%c'", c)); } - return BooleanItem.valueOf(c == '1'); + return BooleanItem.of(c == '1'); } private BooleanItem internalParseBoolean() { @@ -483,7 +483,7 @@ private Parameters internalParseParameters() { advance(); removeLeadingSP(); String name = internalParseKey(); - Item value = BooleanItem.valueOf(true); + Item value = BooleanItem.of(true); if (peek() == '=') { advance(); value = internalParseBareItem(); @@ -492,7 +492,7 @@ private Parameters internalParseParameters() { } } - return Parameters.valueOf(result); + return Parameters.of(result); } private Item internalParseBareItem() { @@ -582,7 +582,6 @@ private List> internalParseBareInnerList() { throw complaint("Expected SP or ')' in Inner List, got: " + format(c)); } } - } if (!done) { @@ -595,7 +594,7 @@ private List> internalParseBareInnerList() { private InnerList internalParseInnerList() { List> result = internalParseBareInnerList(); Parameters params = internalParseParameters(); - return InnerList.valueOf(result).withParams(params); + return InnerList.of(result).withParams(params); } private Dictionary internalParseDictionary() { @@ -612,7 +611,7 @@ private Dictionary internalParseDictionary() { advance(); member = internalParseItemOrInnerList(); } else { - member = BooleanItem.valueOf(true).withParams(internalParseParameters()); + member = BooleanItem.of(true).withParams(internalParseParameters()); } result.put(name, member); @@ -631,7 +630,7 @@ private Dictionary internalParseDictionary() { } } - return Dictionary.valueOf(result); + return Dictionary.of(result); } /** @@ -687,7 +686,7 @@ public OuterList parseList() { List> result = internalParseOuterList(); removeLeadingSP(); assertEmpty("Extra characters in string parsed as List"); - return OuterList.valueOf(result); + return OuterList.of(result); } /** @@ -744,7 +743,7 @@ public static OuterList parseList(String input) { Parser p = new Parser(input); List> result = p.internalParseOuterList(); p.assertEmpty("Extra characters in string parsed as List"); - return OuterList.valueOf(result); + return OuterList.of(result); } /** @@ -892,9 +891,9 @@ public static String parseKey(String input) { * "https://www.rfc-editor.org/rfc/rfc9651.html#parse-number">Section * 4.2.4 of RFC 9651 */ - public static NumberItem parseIntegerOrDecimal(String input) { + public static NumberItem parseIntegerOrDecimal(String input) { Parser p = new Parser(input); - NumberItem result = p.internalParseIntegerOrDecimal(); + NumberItem result = p.internalParseIntegerOrDecimal(); p.assertEmpty("Extra characters in string parsed as Integer or Decimal"); return result; } diff --git a/src/main/java/org/greenbytes/http/sfv/SfDataType.java b/src/main/java/org/greenbytes/http/sfv/SfDataType.java new file mode 100644 index 0000000..bd9db96 --- /dev/null +++ b/src/main/java/org/greenbytes/http/sfv/SfDataType.java @@ -0,0 +1,22 @@ +package org.greenbytes.http.sfv; + +/** + * Types of Structured Data + */ +public enum SfDataType { + // RFC 9651, Section 3.1 + LIST, + // RFC 9651, Section 3.1.1 + INNERLIST, + // RFC 9651, Section 3.2 + DICTIONARY, + // RFC 9651, Section 3.3 + BOOLEAN, + BYTESEQUENCE, + DATE, + DECIMAL, + DISPLAYSTRING, + INTEGER, + STRING, + TOKEN +} diff --git a/src/main/java/org/greenbytes/http/sfv/StringItem.java b/src/main/java/org/greenbytes/http/sfv/StringItem.java index 114380a..7cad4fd 100644 --- a/src/main/java/org/greenbytes/http/sfv/StringItem.java +++ b/src/main/java/org/greenbytes/http/sfv/StringItem.java @@ -19,15 +19,20 @@ private StringItem(String value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.STRING; + } + /** - * Creates a {@link StringItem} instance representing the specified - * {@code String} value. - * - * @param value - * a {@code String} value. - * @return a {@link StringItem} representing {@code value}. - */ - public static StringItem valueOf(String value) { + * Creates a {@link StringItem} instance representing the specified + * {@code String} value. + * + * @param value + * a {@code String} value. + * @return a {@link StringItem} representing {@code value}. + */ + public static StringItem of(String value) { return new StringItem(value, Parameters.EMPTY); } @@ -36,6 +41,11 @@ public StringItem withParams(Parameters params) { return new StringItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public StringItem withParamValuesOf(Object... obs) { + return new StringItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; diff --git a/src/main/java/org/greenbytes/http/sfv/TokenItem.java b/src/main/java/org/greenbytes/http/sfv/TokenItem.java index 16765dd..6856bdf 100644 --- a/src/main/java/org/greenbytes/http/sfv/TokenItem.java +++ b/src/main/java/org/greenbytes/http/sfv/TokenItem.java @@ -19,15 +19,20 @@ private TokenItem(String value, Parameters params) { this.params = Objects.requireNonNull(params, "params must not be null"); } + @Override + public SfDataType getType() { + return SfDataType.TOKEN; + } + /** * Creates a {@link TokenItem} instance representing the specified * {@code String} value. - * + * * @param value * a {@code String} value. * @return a {@link TokenItem} representing {@code value}. */ - public static TokenItem valueOf(String value) { + public static TokenItem of(String value) { return new TokenItem(value, Parameters.EMPTY); } @@ -36,6 +41,11 @@ public TokenItem withParams(Parameters params) { return new TokenItem(this.value, Objects.requireNonNull(params, "params must not be null")); } + @Override + public TokenItem withParamValuesOf(Object... obs) { + return new TokenItem(this.value, Parameters.valueOf(obs)); + } + @Override public Parameters getParams() { return params; @@ -53,6 +63,7 @@ public String serialize() { return serializeTo(new StringBuilder()).toString(); } + @Override public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function formatter) { String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : ""; String classn = formatter.apply(this.getClass()); diff --git a/src/main/java/org/greenbytes/http/sfv/Type.java b/src/main/java/org/greenbytes/http/sfv/Type.java index 7e59323..bd321f7 100644 --- a/src/main/java/org/greenbytes/http/sfv/Type.java +++ b/src/main/java/org/greenbytes/http/sfv/Type.java @@ -16,6 +16,11 @@ */ public interface Type extends Supplier { + /** + * @return Structured Field Data Type + */ + SfDataType getType(); + /** * Serialize to an existing {@link StringBuilder}. * @@ -26,7 +31,7 @@ public interface Type extends Supplier { StringBuilder serializeTo(StringBuilder sb); /** - * Serialize debubg information to an existing {@link StringBuilder}. + * Serialize debug information to an existing {@link StringBuilder}. * * @param sb * where to serialize to diff --git a/src/main/java/org/greenbytes/http/sfv/Utils.java b/src/main/java/org/greenbytes/http/sfv/Utils.java index 99d915b..4d4d786 100644 --- a/src/main/java/org/greenbytes/http/sfv/Utils.java +++ b/src/main/java/org/greenbytes/http/sfv/Utils.java @@ -1,5 +1,7 @@ package org.greenbytes.http.sfv; +import java.math.BigDecimal; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -11,25 +13,38 @@ public class Utils { private Utils() { } - /** check for character to be a decimal digit */ + /** + * Check for character to be a decimal digit. + * @param c character to check + * @return {@code true} if and only if a digit + */ protected static boolean isDigit(char c) { return c >= '0' && c <= '9'; } - /** check for character to be lowercase alphanumeric */ + /** + * Check for character to be lowercase ASCII alpha character. + * @param c character to check + * @return {@code true} if and only if lowercase ASCII alpha + */ protected static boolean isLcAlpha(char c) { return (c >= 'a' && c <= 'z'); } - /** check for character to be alphanumeric */ + /** + * Check for character to be lowercase or uppercase ASCII alpha character. + * @param c character to check + * @return {@code true} if and only if lowercase or uppercase ASCII alpha + */ protected static boolean isAlpha(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } /** - * Checks a key for validity + * Checks key for validity. * @param value to check * @return checked value + * @throws IllegalArgumentException when invalid */ protected static String checkKey(String value) { if (value == null || value.isEmpty()) { @@ -47,9 +62,11 @@ protected static String checkKey(String value) { } /** - * Checks all keys in map for validity + * Checks all keys + * in {@linkplain Map} for validity * @param value map to check * @return checked map + * @throws IllegalArgumentException when invalid key found */ protected static Map> checkKeys(Map> value) { for (String key : Objects.requireNonNull(value, "value must not be null").keySet()) { @@ -57,4 +74,86 @@ protected static Map> checkKeys(Map asBareItem(Object o) { + if (o instanceof Item) { + if (o instanceof Parameterizable) { + Parameterizable p = ((Parameterizable)o); + if (!p.getParams().isEmpty()) { + throw new IllegalArgumentException("Can't map value " + o + " (" + o.getClass() + "): carries parameters."); + } + } + } + return asItem(o); + } + + /** + * Converts an {@linkplain Object} to an {@link Item} (on a best-effort basis). + *

+ * Currently mapped: + *

    + *
  • {@linkplain Item} → {@linkplain Item}
  • + *
  • {@linkplain Integer} → {@linkplain IntegerItem}
  • + *
  • {@linkplain Long} → {@linkplain IntegerItem}
  • + *
  • {@linkplain String} → {@linkplain StringItem} or {@linkplain DisplayStringItem}
  • + *
  • {@linkplain Boolean} → {@linkplain BooleanItem}
  • + *
  • {@code byte[]} → {@linkplain ByteSequenceItem}
  • + *
  • {@linkplain BigDecimal} → {@linkplain DecimalItem}
  • + *
  • {@linkplain Double} → {@linkplain DecimalItem}
  • + *
  • {@linkplain Float} → {@linkplain DecimalItem}
  • + *
+ * Same as {@linkplain #asBareItem(Object)}, but allowing {@linkplain Parameters} + * @param o to convert + * @return converted to {@linkplain Item} + * @throws IllegalArgumentException when it can't be converted + */ + protected static Item asItem(Object o) { + if (o instanceof Item) { + return (Item) o; + } else if (o instanceof Integer) { + return IntegerItem.of(((Integer) o).longValue()); + } else if (o instanceof Long) { + return IntegerItem.of((Long) o); + } else if (o instanceof String) { + try { + return StringItem.of((String) o); + } catch (IllegalArgumentException ia) { + return DisplayStringItem.valueOf((String) o); + } + } else if (o instanceof Boolean) { + return BooleanItem.of((Boolean) o); + } else if (o instanceof byte[]) { + return ByteSequenceItem.valueOf((byte[]) o); + } else if (o instanceof BigDecimal) { + return DecimalItem.valueOf((BigDecimal)o); + } else if (o instanceof Double) { + return DecimalItem.valueOf(BigDecimal.valueOf((Double)o)); + } else if (o instanceof Float) { + return DecimalItem.valueOf(BigDecimal.valueOf((Float)o)); + } else { + throw new IllegalArgumentException("Can't map value " + o.toString() + " (" + o.getClass() + ")"); + } + } + + /** + * Converts an {@linkplain Object} to an {@linkplain List} of {@linkplain Item}s + * (on a best-effort basis). + * Same as {@linkplain #asItem(Object)}, but also allowing {@linkplain InnerList} + * @param o to convert + * @return convert to {@linkplain ListElement} + * @throws IllegalArgumentException when it can't be converted + */ + protected static ListElement asListElement(Object o) { + if (o instanceof InnerList) { + return (InnerList) o; + } else { + return asItem(o); + } + } } diff --git a/src/test/java/org/greenbytes/http/sfv/EqualityTest.java b/src/test/java/org/greenbytes/http/sfv/EqualityTest.java index 93f8c5a..743bf21 100644 --- a/src/test/java/org/greenbytes/http/sfv/EqualityTest.java +++ b/src/test/java/org/greenbytes/http/sfv/EqualityTest.java @@ -21,56 +21,56 @@ public void testParametersEquality() { m1.put("a", "b"); HashMap m2 = new LinkedHashMap<>(); m2.put("a", "b"); - Parameters p1 = Parameters.valueOf(m1); - Parameters p2 = Parameters.valueOf(m2); + Parameters p1 = Parameters.of(m1); + Parameters p2 = Parameters.of(m2); assertNotSame(p1, p2); assertEquals(p1, p2); } @Test public void testStringItemEquality() { - StringItem s1 = StringItem.valueOf("a"); - StringItem s2 = StringItem.valueOf("a"); + StringItem s1 = StringItem.of("a"); + StringItem s2 = StringItem.of("a"); assertNotSame(s1, s2); assertEquals(s1, s2); - StringItem s3 = StringItem.valueOf("b"); + StringItem s3 = StringItem.of("b"); assertNotEquals(s1, s3); - StringItem s4 = StringItem.valueOf("a").withParams(getParameters("a", "b")); + StringItem s4 = StringItem.of("a").withParams(getParameters("a", "b")); assertNotSame(s1, s4); assertNotEquals(s1, s4); HashMap m5 = new LinkedHashMap<>(); m5.put("c", "d"); - Parameters p5 = Parameters.valueOf(m5); - StringItem s5 = StringItem.valueOf("a").withParams(p5); + Parameters p5 = Parameters.of(m5); + StringItem s5 = StringItem.of("a").withParams(p5); assertNotSame(s4, s5); assertNotEquals(s4, s5); } @Test public void testTokenItemEquality() { - TokenItem t1 = TokenItem.valueOf("a"); - TokenItem t = TokenItem.valueOf("a"); + TokenItem t1 = TokenItem.of("a"); + TokenItem t = TokenItem.of("a"); assertNotSame(t1, t); assertEquals(t1, t); - TokenItem t3 = TokenItem.valueOf("b"); + TokenItem t3 = TokenItem.of("b"); assertNotEquals(t1, t3); - TokenItem t4 = TokenItem.valueOf("a").withParams(getParameters("a", "b")); + TokenItem t4 = TokenItem.of("a").withParams(getParameters("a", "b")); assertNotSame(t1, t4); assertNotEquals(t1, t4); - TokenItem t5 = TokenItem.valueOf("a").withParams(getParameters("c", "d")); + TokenItem t5 = TokenItem.of("a").withParams(getParameters("c", "d")); assertNotSame(t4, t5); assertNotEquals(t4, t5); } @Test public void testBooleanItemEquality() { - BooleanItem b1 = BooleanItem.valueOf(true); - BooleanItem b2 = BooleanItem.valueOf(true); - BooleanItem b3 = BooleanItem.valueOf(false); + BooleanItem b1 = BooleanItem.of(true); + BooleanItem b2 = BooleanItem.of(true); + BooleanItem b3 = BooleanItem.of(false); // FALSE and TRUE are singletons assertSame(b1, b2); @@ -79,7 +79,7 @@ public void testBooleanItemEquality() { assertNotSame(b1, b3); assertNotEquals(b1, b3); - BooleanItem b4 = BooleanItem.valueOf(false).withParams(getParameters("c", "d")); + BooleanItem b4 = BooleanItem.of(false).withParams(getParameters("c", "d")); assertNotSame(b1, b4); assertNotEquals(b1, b4); } @@ -154,9 +154,9 @@ public void testDisplayStringItemEquality() { @Test public void testIntegerItemEquality() { - IntegerItem i1 = IntegerItem.valueOf(1); - IntegerItem i2 = IntegerItem.valueOf(1); - IntegerItem i3 = IntegerItem.valueOf(2); + IntegerItem i1 = IntegerItem.of(1); + IntegerItem i2 = IntegerItem.of(1); + IntegerItem i3 = IntegerItem.of(2); assertNotSame(i1, i2); assertEquals(i1, i2); @@ -171,12 +171,12 @@ public void testIntegerItemEquality() { @Test public void testOuterListEquality() { - BooleanItem b = BooleanItem.valueOf(true); + BooleanItem b = BooleanItem.of(true); DecimalItem d = DecimalItem.valueOf(BigDecimal.valueOf(10.5)); - OuterList l1 = OuterList.valueOf((Arrays.asList(b, d))); - OuterList l2 = OuterList.valueOf((Arrays.asList(b, d))); - OuterList l3 = OuterList.valueOf((Arrays.asList(b, d, b))); + OuterList l1 = OuterList.of((Arrays.asList(b, d))); + OuterList l2 = OuterList.of((Arrays.asList(b, d))); + OuterList l3 = OuterList.of((Arrays.asList(b, d, b))); assertNotSame(l1, l2); assertEquals(l1, l2); @@ -188,6 +188,6 @@ public void testOuterListEquality() { private static Parameters getParameters(String a, String b) { HashMap m = new LinkedHashMap<>(); m.put(a, b); - return Parameters.valueOf(m); + return Parameters.of(m); } } diff --git a/src/test/java/org/greenbytes/http/sfv/ItemAPITests.java b/src/test/java/org/greenbytes/http/sfv/ItemAPITests.java index 39d1df3..8208e4c 100644 --- a/src/test/java/org/greenbytes/http/sfv/ItemAPITests.java +++ b/src/test/java/org/greenbytes/http/sfv/ItemAPITests.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Collectors; import org.junit.Test; @@ -17,11 +18,11 @@ public class ItemAPITests { @Test public void testBoolean() { - BooleanItem b0 = BooleanItem.valueOf(false); + BooleanItem b0 = BooleanItem.of(false); assertEquals(false, b0.get()); assertEquals("?0", b0.serialize()); - BooleanItem b1 = BooleanItem.valueOf(true); + BooleanItem b1 = BooleanItem.of(true); assertEquals(true, b1.get()); assertEquals("?1", b1.serialize()); } @@ -32,7 +33,7 @@ public void testInteger() { long[] tests = new long[] { 0L, -0L, 999999999999999L, -999999999999999L }; for (long l : tests) { - IntegerItem item = IntegerItem.valueOf(l); + IntegerItem item = IntegerItem.of(l); assertEquals(Long.valueOf(l), item.get()); assertEquals(l, item.getAsLong()); assertEquals(Long.valueOf(l).toString(), item.serialize()); @@ -47,7 +48,7 @@ public void testIntegerInvalid() { for (Long l : tests) { try { - IntegerItem item = IntegerItem.valueOf(l); + IntegerItem item = IntegerItem.of(l); fail("should fail for " + l + " but got '" + item.get() + "'"); } catch (IllegalArgumentException expected) { } @@ -108,7 +109,7 @@ public void testString() { String[] tests = new String[] { "", "'", "\"", "\\" }; for (String s : tests) { - StringItem item = StringItem.valueOf(s); + StringItem item = StringItem.of(s); assertEquals(s, item.get()); // TODO: figure out how to check the serialization without copying // the actual impl code @@ -122,7 +123,7 @@ public void testStringInvalid() { for (String s : tests) { try { - StringItem item = StringItem.valueOf(s); + StringItem item = StringItem.of(s); fail("should fail for '" + s + "' but got '" + item.get() + "'"); } catch (IllegalArgumentException expected) { } @@ -135,7 +136,7 @@ public void testToken() { String[] tests = new String[] { "*", "x", "*-/", "foo.bar-qux" }; for (String s : tests) { - TokenItem item = TokenItem.valueOf(s); + TokenItem item = TokenItem.of(s); assertEquals(s, item.get()); // TODO: figure out how to check the serialization without copying // the actual impl code @@ -149,7 +150,7 @@ public void testTokenInvalid() { for (String s : tests) { try { - TokenItem item = TokenItem.valueOf(s); + TokenItem item = TokenItem.of(s); fail("should fail for '" + s + "' but got '" + item.get() + "'"); } catch (IllegalArgumentException expected) { } @@ -181,21 +182,62 @@ public void testByteSequenceInvalid() { @Test public void testParameters() { + Parameters p = createParams1(); + + assertEquals(StringItem.class, p.get("*").getClass()); + assertEquals(IntegerItem.class, p.get("i").getClass()); + assertEquals(IntegerItem.class, p.get("l").getClass()); + assertEquals(BooleanItem.class, p.get("b").getClass()); + assertEquals(ByteSequenceItem.class, p.get("o").getClass()); + assertEquals(DecimalItem.class, p.get("d").getClass()); + } + + @Test + public void testParameters2() { + + Parameters p = createParams2(); - Map m = new LinkedHashMap<>(); - m.put("*", "star"); - m.put("i", 1); - m.put("l", 2L); - m.put("b", false); - m.put("o", new byte[0]); - m.put("d", new BigDecimal("0.1")); - Parameters p = Parameters.valueOf(m); assertEquals(StringItem.class, p.get("*").getClass()); assertEquals(IntegerItem.class, p.get("i").getClass()); assertEquals(IntegerItem.class, p.get("l").getClass()); assertEquals(BooleanItem.class, p.get("b").getClass()); assertEquals(ByteSequenceItem.class, p.get("o").getClass()); assertEquals(DecimalItem.class, p.get("d").getClass()); + assertEquals(DecimalItem.class, p.get("d2").getClass()); + + assertEquals(";*=\"star\";i=1;l=2;b=?0;o=::;d=0.155;d2=12345.0;d3=3.14", p.serialize()); + + Map sermap = p.entrySet().stream(). + collect(Collectors.toMap(Map.Entry::getKey, x -> x.getValue().serialize())); + + assertEquals("?0", sermap.get("b")); + assertEquals("12345.0", sermap.get("d2")); + } + + @Test + public void testParametersEquals() { + Parameters p1 = createParams1(); + Parameters p2 = createParams2(); + assertEquals(p1.serialize(), p2.serialize()); + assertEquals(p1, p2); + } + + private static Parameters createParams1() { + Map m = new LinkedHashMap<>(); + m.put("*", "star"); + m.put("i", 1); + m.put("l", 2L); + m.put("b", false); + m.put("o", new byte[0]); + m.put("d", new BigDecimal("0.155")); + m.put("d2", BigDecimal.valueOf(12345)); + m.put("d3", 3.14f); + return Parameters.of(m); + } + + private static Parameters createParams2() { + return Parameters.valueOf("*", "star", "i", 1, "l", 2L, "b", false, + "o", new byte[0], "d", 0.155d, "d2", BigDecimal.valueOf(12345), "d3", 3.14f); } @Test @@ -203,7 +245,7 @@ public void testParametersUnmodifiable() { Map m = new HashMap<>(); m.put("test", "test"); - Parameters p = Parameters.valueOf(m); + Parameters p = Parameters.of(m); assertThrows( UnsupportedOperationException.class, @@ -217,7 +259,7 @@ public void testInvalidParameterKeys() { Map m = new LinkedHashMap<>(); for (String key : tests) { m.clear(); - m.put(key, IntegerItem.valueOf(1)); + m.put(key, IntegerItem.of(1)); assertThrows("should throe", IllegalArgumentException.class, () -> Parameters.valueOf(m)); @@ -228,13 +270,13 @@ public void testInvalidParameterKeys() { public void testInvalidParameterValues() { Map itemParam = new LinkedHashMap<>(); - itemParam.put("foo", IntegerItem.valueOf(2)); - IntegerItem iitem = IntegerItem.valueOf(1).withParams(Parameters.valueOf(itemParam)); + itemParam.put("foo", IntegerItem.of(2)); + IntegerItem iitem = IntegerItem.of(1).withParams(Parameters.of(itemParam)); Map m = new LinkedHashMap<>(); m.put("bar", iitem); try { - Parameters test = Parameters.valueOf(m); + Parameters test = Parameters.of(m); fail("Parameters containing non-bare Item should fail, but got: " + test.serialize()); } catch (IllegalArgumentException expected) { } diff --git a/src/test/java/org/greenbytes/http/sfv/ParametersTest.java b/src/test/java/org/greenbytes/http/sfv/ParametersTest.java index fff5800..79bce38 100644 --- a/src/test/java/org/greenbytes/http/sfv/ParametersTest.java +++ b/src/test/java/org/greenbytes/http/sfv/ParametersTest.java @@ -12,7 +12,7 @@ public class ParametersTest { - Parameters params = Parameters.valueOf(new HashMap<>()); + Parameters params = Parameters.of(Collections.emptyMap()); // Test that all write operations fail @@ -65,14 +65,14 @@ public void testReplaceAll1() { @Test public void testParametersEquality() { Map m1 = new HashMap(); - m1.put("a", BooleanItem.valueOf(true)); + m1.put("a", BooleanItem.of(true)); Map m2 = new HashMap(); - m2.put("b", IntegerItem.valueOf(12)); - m2.put("c", StringItem.valueOf("hello")); + m2.put("b", IntegerItem.of(12)); + m2.put("c", StringItem.of("hello")); - Parameters p1 = Parameters.valueOf(m1); - Parameters p2 = Parameters.valueOf(m1); - Parameters p3 = Parameters.valueOf(m2); + Parameters p1 = Parameters.of(m1); + Parameters p2 = Parameters.of(m1); + Parameters p3 = Parameters.of(m2); assertNotSame(p1, p2); assertEquals(p1, p2); @@ -83,7 +83,7 @@ public void testParametersEquality() { @Test public void canRemoveParams() { - Parameters empty = Parameters.valueOf(Collections.emptyMap()); + Parameters empty = Parameters.of(Collections.emptyMap()); BooleanItem boi = Parser.parseBoolean("?1;b"); assertEquals("?1;b", boi.serialize()); @@ -121,13 +121,13 @@ public void canRemoveParams() { @Test(expected = IllegalArgumentException.class) public void testParamsStrictnessReParametersInValues() { Map map1 = new HashMap<>(); - map1.put("foo", BooleanItem.valueOf(false)); + map1.put("foo", BooleanItem.of(false)); Map map2 = new HashMap<>(); map2.put("qux", 0); - BooleanItem.valueOf(true).withParams(Parameters.valueOf(map2)); - map1.put("bar", BooleanItem.valueOf(true).withParams(Parameters.valueOf(map2))); + BooleanItem.of(true).withParams(Parameters.valueOf(map2)); + map1.put("bar", BooleanItem.of(true).withParams(Parameters.valueOf(map2))); // this needs to fail because the second parameter's value has parameters Parameters.valueOf(map1); diff --git a/src/test/java/org/greenbytes/http/sfv/RFC9651ExamplesTest.java b/src/test/java/org/greenbytes/http/sfv/RFC9651ExamplesTest.java new file mode 100755 index 0000000..31473b5 --- /dev/null +++ b/src/test/java/org/greenbytes/http/sfv/RFC9651ExamplesTest.java @@ -0,0 +1,414 @@ +package org.greenbytes.http.sfv; + +import org.junit.Test; + +import java.math.BigDecimal; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class RFC9651ExamplesTest { + + // RFC 9651, Section 3.1 + // Example-List: sugar, tea, rum + + @Test + public void testListConstructionBareItems() { + OuterList l1 = createListBareItems1(); + OuterList l2 = createListBareItems2(); + assertEquals("\"sugar\", \"tee\", \"rum\"", l2.serialize()); + assertEquals(l1.serialize(), l2.serialize()); + assertEquals(l1, l2); + } + + // chatty API + private static OuterList createListBareItems1() { + List> list = new ArrayList<>(); + list.add(StringItem.of("sugar")); + list.add(StringItem.of("tee")); + list.add(StringItem.of("rum")); + + return OuterList.of(list); + } + + // concise API + private static OuterList createListBareItems2() { + return OuterList.valueOf("sugar", "tee", "rum"); + } + + // RFC 9651, Section 3.1.1 + // Example-List: ("foo" "bar"), ("baz"), ("bat" "one"), () + + @Test + public void testListConstructionWithInnerLists() { + OuterList ol1 = createListBareInnerLists1(); + OuterList ol2 = createListBareInnerLists2(); + assertEquals("(\"foo\" \"bar\"), (\"baz\"), (\"bat\" \"one\"), ()", ol1.serialize()); + assertEquals("(\"foo\" \"bar\"), (\"baz\"), (\"bat\" \"one\"), ()", ol2.serialize()); + assertEquals(ol1, ol2); + } + + // chatty API + private static OuterList createListBareInnerLists1() { + List> inner1 = new ArrayList<>(); + inner1.add(StringItem.of("foo")); + inner1.add(StringItem.of("bar")); + + List> inner2 = Collections.singletonList(StringItem.of("baz")); + + List> inner3 = new ArrayList<>(); + inner3.add(StringItem.of("bat")); + inner3.add(StringItem.of("one")); + + List> inner4 = Collections.emptyList(); + + List> combined = new ArrayList<>(); + combined.add(InnerList.of(inner1)); + combined.add(InnerList.of(inner2)); + combined.add(InnerList.of(inner3)); + combined.add(InnerList.of(inner4)); + + return OuterList.of(combined); + } + + // concise API + private static OuterList createListBareInnerLists2() { + return OuterList.of(InnerList.valueOf("foo", "bar"), + InnerList.valueOf("baz"), + InnerList.valueOf("bat", "one"), + InnerList.of()); + } + + // RFC 9651, Section 3.1.1 + // Example-List: ("foo"; a=1;b=2);lvl=5, ("bar" "baz");lvl=1 + + @Test + public void testListConstructionWithInnerListsWithParams() { + OuterList ol1 = createParametrizedInnerLists1(); + OuterList ol2 = createParametrizedInnerLists2(); + assertEquals("(\"foo\";a=1;b=2);lvl=5, (\"bar\");lvl=1", ol1.serialize()); + assertEquals("(\"foo\";a=1;b=2);lvl=5, (\"bar\");lvl=1", ol2.serialize()); + assertEquals(ol1, ol2); + } + + // chatty API + private static OuterList createParametrizedInnerLists1() { + List> inner1 = new ArrayList<>(); + Map itemParam1 = new LinkedHashMap<>(); + itemParam1.put("a", 1); + itemParam1.put("b", 2); + inner1.add(StringItem.of("foo").withParams(Parameters.of(itemParam1))); + Map itemParamOuter1 = Collections.singletonMap("lvl", 5); + InnerList linner1 = InnerList.of(inner1).withParams(Parameters.of(itemParamOuter1)); + + List> inner2 = Collections.singletonList(StringItem.of("bar")); + Map itemParamOuter2 = new LinkedHashMap<>(); + itemParamOuter2.put("lvl", 1); + InnerList linner2 = InnerList.of(inner2).withParams(Parameters.of(itemParamOuter2)); + + List> combined = new ArrayList<>(); + combined.add(linner1); + combined.add(linner2); + + return OuterList.of(combined); + } + + // concise API + private static OuterList createParametrizedInnerLists2() { + InnerList linner1 = InnerList.of( + StringItem.of("foo").withParams(Parameters.valueOf("a", 1, "b", 2))) + .withParams(Parameters.valueOf("lvl", 5)); + + InnerList linner2 = InnerList.valueOf("bar").withParams(Parameters.valueOf("lvl", 1)); + + return OuterList.of(linner1, linner2); + } + + // RFC 9651, Section 3.1.2 + // Example-List: abc;a=1;b=2; cde_456, (ghi;jk=4 l);q="9";r=w + + @Test + public void testComplexListOfParams() { + OuterList ol1 = createComplexListOfParams1(); + OuterList ol2 = createComplexListOfParams2(); + assertEquals("abc;a=1;b=2;cde_456, (ghi;jk=4 l);q=\"9\";r=w", ol1.serialize()); + assertEquals("abc;a=1;b=2;cde_456, (ghi;jk=4 l);q=\"9\";r=w", ol2.serialize()); + assertEquals(ol1, ol2); + } + + // chatty API + private static OuterList createComplexListOfParams1() { + Map map1 = new LinkedHashMap<>(); + map1.put("a", IntegerItem.of(1)); + map1.put("b", IntegerItem.of(2)); + map1.put("cde_456", BooleanItem.of(true)); + TokenItem l1 = TokenItem.of("abc"). + withParams(Parameters.of(map1)); + + List> lc2 = new ArrayList<>(); + TokenItem t21 = TokenItem.of("ghi"). + withParams(Parameters.of( + Collections.singletonMap("jk", IntegerItem.of(4)))); + + lc2.add(t21); + lc2.add(TokenItem.of("l")); + + Map map2 = new LinkedHashMap<>(); + map2.put("q", StringItem.of("9")); + map2.put("r", TokenItem.of("w")); + Parameters p2 = Parameters.of(map2); + InnerList l2 = InnerList.of(lc2).withParams(p2); + + List> value = new LinkedList<>(); + value.add(l1); + value.add(l2); + + return OuterList.of(value); + } + + // concise API + private static OuterList createComplexListOfParams2() { + TokenItem l1 = TokenItem.of("abc"). + withParamValuesOf("a", 1, "b", 2, "cde_456", true); + + InnerList l2 = InnerList.valueOf( + TokenItem.of("ghi").withParamValuesOf("jk", 4), + TokenItem.of("l")). + withParamValuesOf("q", "9", "r", TokenItem.of("w")); + + return OuterList.valueOf(l1, l2); + } + + // RFC 9651, Section 3.2 + // Example-Dict: en="Applepie", da=:w4ZibGV0w6ZydGU=: + + @Test + public void testDictConstructionSimple() { + Dictionary dict1 = createDictionarySimple1(); + Dictionary dict2 = createDictionarySimple2(); + assertEquals("en=\"Applepie\", da=:w4ZibGV0w6ZydGU=:", dict1.serialize()); + assertEquals("en=\"Applepie\", da=:w4ZibGV0w6ZydGU=:", dict2.serialize()); + assertEquals(dict1, dict2); + } + + // chatty API + private static Dictionary createDictionarySimple1() { + Map> map = new LinkedHashMap<>(); + map.put("en", StringItem.of("Applepie")); + map.put("da", ByteSequenceItem.valueOf("Æbletærte".getBytes(StandardCharsets.UTF_8))); + return Dictionary.of(map); + } + + // concise API + private static Dictionary createDictionarySimple2() { + return Dictionary.valueOf( + "en", "Applepie", + "da", "Æbletærte".getBytes(StandardCharsets.UTF_8)); + } + + // RFC 9651, Section 3.2 + // Example-Dict: a=?0, b, c; foo=bar + + @Test + public void testDictConstruction() { + Dictionary dict1 = createDictionary1(); + Dictionary dict2 = createDictionary2(); + assertEquals("a=?0, b, c;foo=bar", dict1.serialize()); + assertEquals("a=?0, b, c;foo=bar", dict2.serialize()); + assertEquals(dict1, dict2); + } + + // chatty API + private static Dictionary createDictionary1() { + Map> map = new LinkedHashMap<>(); + map.put("a", BooleanItem.of(false)); + map.put("b", BooleanItem.of(true)); + map.put("c", BooleanItem.of(true).withParams(Parameters.of(Collections.singletonMap("foo", TokenItem.of("bar"))))); + return Dictionary.of(map); + } + + // concise API + private static Dictionary createDictionary2() { + return Dictionary.valueOf( + "a", false, + "b", true, + "c", BooleanItem.of(true).withParamValuesOf("foo", TokenItem.of("bar"))); + } + + // RFC 9651, Section 3.2 + // Example-Dict: rating=1.5, feelings=(joy sadness) + + @Test + public void testDictConstructionWithInnerList() { + Dictionary dict1 = createDictionaryWithInnerList1(); + Dictionary dict2 = createDictionaryWithInnerList2(); + assertEquals("rating=1.5, feelings=(joy sadness)", dict1.serialize()); + assertEquals("rating=1.5, feelings=(joy sadness)", dict2.serialize()); + assertEquals(dict1, dict2); + } + + // chatty API + private static Dictionary createDictionaryWithInnerList1() { + Map> map = new LinkedHashMap<>(); + map.put("rating", DecimalItem.valueOf(BigDecimal.valueOf(1.5f))); + List> li = new ArrayList<>(); + li.add(TokenItem.of("joy")); + li.add(TokenItem.of("sadness")); + map.put("feelings", InnerList.of(li)); + return Dictionary.of(map); + } + + // concise API + private static Dictionary createDictionaryWithInnerList2() { + return Dictionary.valueOf("rating", 1.5f, + "feelings", InnerList.of(TokenItem.of("joy"), TokenItem.of("sadness"))); + } + + // RFC 9651, Section 3.2 + // Example-Dict: a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid + + @Test + public void testDictConstructionMix() { + Dictionary dict1 = createDictionaryMix1(); + Dictionary dict2 = createDictionaryMix2(); + assertEquals("a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid", dict1.serialize()); + assertEquals("a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid", dict2.serialize()); + assertEquals(dict1, dict2); + } + + // chatty API + private static Dictionary createDictionaryMix1() { + Map> map = new LinkedHashMap<>(); + + List> inner1 = new ArrayList<>(); + inner1.add(IntegerItem.of(1)); + inner1.add(IntegerItem.of(2)); + InnerList linner1 = InnerList.of(inner1); + + Map p3 = new LinkedHashMap<>(); + p3.put("aa", TokenItem.of("bb")); + Parameters params3 = Parameters.of(p3); + + List> inner4 = new ArrayList<>(); + inner4.add(IntegerItem.of(5)); + inner4.add(IntegerItem.of(6)); + InnerList linner4 = InnerList.of(inner4); + + Map p4 = new LinkedHashMap<>(); + p4.put("valid", BooleanItem.of(true)); + Parameters params4 = Parameters.of(p4); + + map.put("a", linner1); + map.put("b", IntegerItem.of(3)); + map.put("c", IntegerItem.of(4).withParams(params3)); + map.put("d", linner4.withParams(params4)); + + return Dictionary.of(map); + } + + // concise API + private static Dictionary createDictionaryMix2() { + return Dictionary.valueOf("a", InnerList.valueOf(1, 2), + "b", 3, + "c", IntegerItem.of(4). + withParamValuesOf("aa", TokenItem.of("bb")), + "d", InnerList.valueOf(5, 6). + withParamValuesOf("valid", true)); + } + + // RFC 9651, Section 3.2 + // Foo-Example: 2; foourl="https://foo.example.com/" + // (parse and validate) + + @Test + public void testParseAndValidateExample() { + + { + Foo foo = parseAndValidateExample("2; foourl=\"https://foo.example.com/\"", null); + assertEquals(2, foo.amount); + assertEquals("https://foo.example.com/", foo.url.toString()); + } + + { + Foo foo = parseAndValidateExample("5", null); + assertEquals(5, foo.amount); + assertNull(foo.url); + } + + { + Foo foo = parseAndValidateExample("5; foourl=\"foo\"", URI.create("https://example.org/")); + assertEquals(5, foo.amount); + assertEquals("https://example.org/foo", foo.url.toString()); + } + + try { + parseAndValidateExample("11; foourl=\"https://foo.example.com/\"", null); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + parseAndValidateExample("9.0; foourl=\"https://foo.example.com/\"", null); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + parseAndValidateExample("2; foourl=\"https:oh no//foo.example.com/\"", null); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + parseAndValidateExample("2; foourl=\"relative\"", null); + fail(); + } catch (NullPointerException expected) { + } + } + + private static class Foo { + int amount; + URI url; + } + + private static Foo parseAndValidateExample(String serialization, URI baseUri) { + Item item = Parser.parseItem(serialization); + + if (SfDataType.INTEGER != item.getType()) { + throw new IllegalArgumentException("not a IntegerItem (was " + item.getClass().getSimpleName() + ")"); + } + + long amountOfFoo = ((IntegerItem) item).get(); + + if (amountOfFoo < 0 || amountOfFoo > 10) { + throw new IllegalArgumentException("invalid amountOfFoo (was " + amountOfFoo + ")"); + } + + Item fooURLParam = item.getParams().get("foourl"); + if (fooURLParam != null && SfDataType.INTEGER != item.getType()) { + throw new IllegalArgumentException("foourl not a StringItem (was " + fooURLParam.getClass().getSimpleName() + ")"); + } + + URI url = null; + if (fooURLParam != null) { + url = URI.create(((StringItem) fooURLParam).get()); + if (! url.isAbsolute()) { + url = baseUri.resolve(url); + } + } + + Foo foo = new Foo(); + foo.amount = (int) amountOfFoo; + foo.url = url; + return foo; + } +} diff --git a/src/test/java/org/greenbytes/http/sfv/UtilsTest.java b/src/test/java/org/greenbytes/http/sfv/UtilsTest.java new file mode 100644 index 0000000..75688e2 --- /dev/null +++ b/src/test/java/org/greenbytes/http/sfv/UtilsTest.java @@ -0,0 +1,27 @@ +package org.greenbytes.http.sfv; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class UtilsTest { + + @Test + public void testConversionFromStringWhichIsNotAValidSFString() { + // sanity check + Item simple = Utils.asBareItem("foobar"); + assertEquals(StringItem.class, simple.getClass()); + + Item notSimple = Utils.asBareItem("qux: \u83D0\uDCA9"); + assertEquals(DisplayStringItem.class, notSimple.getClass()); + } + + // TODO: when constructing Items, need to check validity of arguments + // Test below should fail, for instance + + @Test + public void testConversionFromStringWhichHasUnpairedSurrogates() { + Item what = Utils.asBareItem("qux: \u83D0\uDCA9 what?"); + assertEquals(DisplayStringItem.class, what.getClass()); + } +} \ No newline at end of file