From b41d29121d16dd481fe85aa0d909d1536bd77b50 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 19 May 2021 10:21:09 +0300 Subject: [PATCH] Redesigned mime classes and interfaces (#23) For #22 - redesigned main interfaces and primary implementations, fixed tests and matchers. --- pom.xml | 1 + src/main/java/wtf/g4s8/mime/MimeType.java | 83 ++++++-- src/main/java/wtf/g4s8/mime/MimeTypeOf.java | 135 ------------- .../java/wtf/g4s8/mime/MimeTypeOfString.java | 181 ++++++++++++++++++ .../java/wtf/g4s8/mime/MimeTypeSmart.java | 74 ------- .../wtf/g4s8/mime/test/HmMimeHasParam.java | 33 +--- .../wtf/g4s8/mime/test/HmMimeHasSubType.java | 10 +- .../wtf/g4s8/mime/test/HmMimeHasType.java | 10 +- .../java/wtf/g4s8/mime/MimeTypeOfTest.java | 22 ++- .../java/wtf/g4s8/mime/MimeTypeSmartTest.java | 35 ---- src/test/java/wtf/g4s8/mime/Test.java | 2 +- 11 files changed, 280 insertions(+), 306 deletions(-) delete mode 100644 src/main/java/wtf/g4s8/mime/MimeTypeOf.java create mode 100644 src/main/java/wtf/g4s8/mime/MimeTypeOfString.java delete mode 100644 src/main/java/wtf/g4s8/mime/MimeTypeSmart.java delete mode 100644 src/test/java/wtf/g4s8/mime/MimeTypeSmartTest.java diff --git a/pom.xml b/pom.xml index 057ac0d..ab5c09d 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ org.hamcrest hamcrest + provided diff --git a/src/main/java/wtf/g4s8/mime/MimeType.java b/src/main/java/wtf/g4s8/mime/MimeType.java index 6764234..7876d8a 100644 --- a/src/main/java/wtf/g4s8/mime/MimeType.java +++ b/src/main/java/wtf/g4s8/mime/MimeType.java @@ -4,34 +4,91 @@ */ package wtf.g4s8.mime; -import java.io.IOException; -import java.util.Map; +import java.util.Optional; +import java.util.Set; /** * Media type (or MIME type). * - * @since 0.1 + * @since 2.0 */ public interface MimeType { /** * Type name. - * @return Type name string - * @throws IOException If failed to read + * @return Name string */ - String type() throws IOException; + String type(); /** * Subtype name. - * @return Subtype name string - * @throws IOException If failed to read + * @return Subtype string */ - String subtype() throws IOException; + String subtype(); /** - * Optional parameters. - * @return Parameter map - * @throws IOException If failed to read + * Optional parameters names. + * @return unordered case-insentetive set of strings */ - Map params() throws IOException; + Set params(); + + /** + * Parameter value for name. + * @param name Parameter name, case-insentetive + * @return Optional parameter value if present for the name + */ + Optional param(String name); + + /** + * Mime type of string source. + * @return Mime type implementation for string provided + */ + static MimeType of(final CharSequence src) { + return new MimeTypeOfString(src); + } + + /** + * Default decorator for mime type. + * @since 2.0 + */ + abstract class Wrap implements MimeType { + + /** + * Delegate. + */ + private final MimeType origin; + + /** + * Wraps origin. + * @param origin Delegate + */ + protected Wrap(final MimeType origin) { + this.origin = origin; + } + + @Override + public final String type() { + return this.origin.type(); + } + + @Override + public String subtype() { + return this.origin.subtype(); + } + + @Override + public Set params() { + return this.origin.params(); + } + + @Override + public Optional param(final String name) { + return this.origin.param(name); + } + + @Override + public String toString() { + return this.origin.toString(); + } + } } diff --git a/src/main/java/wtf/g4s8/mime/MimeTypeOf.java b/src/main/java/wtf/g4s8/mime/MimeTypeOf.java deleted file mode 100644 index bd822ac..0000000 --- a/src/main/java/wtf/g4s8/mime/MimeTypeOf.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2017-2021 Kirill Ch. - * https://github.com/g4s8/mime/LICENSE.txt - */ -package wtf.g4s8.mime; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * MIME type from string. - * - * @since 0.1 - */ -@SuppressWarnings("PMD.StaticAccessToStaticFields") -public final class MimeTypeOf implements MimeType { - - /** - * Type pattern. - */ - private static final Pattern PTN_TYPE; - - /** - * Parameters pattern. - */ - private static final Pattern PTN_PARAM; - - static { - final String token = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)"; - PTN_TYPE = Pattern.compile( - String.format("%s/%s", token, token) - ); - PTN_PARAM = Pattern.compile( - String.format( - ";\\s*(?:%s=(?:%s|\"([^\"]*)\"))?", - token, - token - ) - ); - } - - /** - * Source string. - */ - private final String src; - - /** - * Ctor. - * @param source Source string - */ - public MimeTypeOf(final String source) { - this.src = source; - } - - @Override - public String type() throws IOException { - return this.matcher().group(1).toLowerCase(Locale.US); - } - - @Override - public String subtype() throws IOException { - return this.matcher().group(2).toLowerCase(Locale.US); - } - - @Override - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public Map params() throws IOException { - final Matcher match = this.matcher(); - final Matcher param = MimeTypeOf.PTN_PARAM.matcher(this.src); - final Map map = new HashMap<>(1); - for (int id = match.end(); id < this.src.length(); id = param.end()) { - param.region(id, this.src.length()); - if (!param.lookingAt()) { - throw new IOException("Invalid mime-type params format"); - } - final String name = param.group(1); - if (map.containsKey(name)) { - throw new IOException( - String.format("Parameter %s may only exist once.", name) - ); - } - map.put(name, MimeTypeOf.paramValue(param)); - } - return map; - } - - @Override - public String toString() { - return this.src; - } - - /** - * Matcher for type/subtype. - * @return Pattern matcher - * @throws IOException If failed - */ - private Matcher matcher() throws IOException { - final Matcher matcher = MimeTypeOf.PTN_TYPE.matcher(this.src); - if (!matcher.lookingAt()) { - throw new IOException("Invalid mime-type format"); - } - return matcher; - } - - /** - * Read parameter value from matcher. - * @param matcher Pattern matcher - * @return Value - * @throws IOException If failed - * @checkstyle MagicNumberCheck (30 lines) - */ - private static String paramValue(final Matcher matcher) throws IOException { - final String token = matcher.group(2); - final String value; - if (token == null) { - value = matcher.group(3); - } else { - if (token.length() > 2 - && token.charAt(0) == '\'' - && token.charAt(token.length() - 1) == '\'') { - value = token.substring(1, token.length() - 1); - } else { - value = token; - } - } - if (value == null) { - throw new IOException("Bad parameter value"); - } - return value; - } -} diff --git a/src/main/java/wtf/g4s8/mime/MimeTypeOfString.java b/src/main/java/wtf/g4s8/mime/MimeTypeOfString.java new file mode 100644 index 0000000..e69091f --- /dev/null +++ b/src/main/java/wtf/g4s8/mime/MimeTypeOfString.java @@ -0,0 +1,181 @@ +/* + * The MIT License (MIT) Copyright (c) 2017-2021 Kirill Ch. + * https://github.com/g4s8/mime/LICENSE.txt + */ +package wtf.g4s8.mime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MIME type from string. + * + * @since 2.0 + */ +@SuppressWarnings("PMD.StaticAccessToStaticFields") +final class MimeTypeOfString implements MimeType { + + /** + * Type pattern. + */ + private static final Pattern PTN_TYPE; + + /** + * Parameters pattern. + */ + private static final Pattern PTN_PARAM; + + static { + final String token = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)"; + PTN_TYPE = Pattern.compile( + String.format("%s/%s", token, token) + ); + PTN_PARAM = Pattern.compile( + String.format( + "\\s*(?:%s=(?:%s|\"([^\"]*)\"))?", + token, + token + ) + ); + } + + + /** + * Source string. + */ + private final CharSequence src; + + /** + * Parameter's map cache. + */ + private volatile Map parmap; + + /** + * Ctor. + * @param source Source string + */ + public MimeTypeOfString(final CharSequence src) { + this.src = src; + } + + @Override + public String type() { + return this.typeMatcher().group(1).toLowerCase(Locale.US); + } + + @Override + public String subtype() { + return this.typeMatcher().group(2).toLowerCase(Locale.US); + } + + @Override + public Set params() { + return this.paramsMap().keySet(); + } + + @Override + public Optional param(final String name) { + return Optional.ofNullable(this.paramsMap().get(name)); + } + + @Override + public String toString() { + return this.src.toString(); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof MimeTypeOfString)) { + return false; + } + final MimeTypeOfString other = (MimeTypeOfString) obj; + return Objects.equals(this.src, other.src); + } + + @Override + public int hashCode() { + return this.src.hashCode(); + } + + /** + * Type and subtype matcher. + * @return Pattern matcher + */ + private Matcher typeMatcher() { + final Matcher matcher = MimeTypeOfString.PTN_TYPE.matcher(this.src); + if (!matcher.lookingAt()) { + throw new IllegalStateException("Invalid mime type format"); + } + return matcher; + } + + /** + * Parameter name to value map. + * @return Map of parameters values by names + */ + private Map paramsMap() { + if (this.parmap == null) { + final Matcher matcher = this.typeMatcher(); + final String str = this.src.toString().trim(); + final Matcher param = MimeTypeOfString.PTN_PARAM.matcher(str); + final Map map = new HashMap<>(); + int id = matcher.end(); + if (id + 1 <= str.length() && str.charAt(id) == ';') { + id++; + } else { + this.parmap = Collections.emptyMap(); + return this.parmap; + } + for (; id < str.length(); id = param.end()) { + param.region(id, str.length()); + if (!param.lookingAt()) { + throw new IllegalStateException( + String.format("Invalid mime-type params: %s", param) + ); + } + final String name = param.group(1).toLowerCase(Locale.US); + if (map.containsKey(name)) { + throw new IllegalStateException( + String.format("Parameter %s may only exist once.", name) + ); + } + map.put(name, MimeTypeOfString.paramValue(param)); + } + this.parmap = map; + } + return Collections.unmodifiableMap(this.parmap); + } + + /** + * Read parameter value from matcher. + * @param matcher Pattern matcher + * @return Value + * @checkstyle MagicNumberCheck (30 lines) + */ + private static String paramValue(final Matcher matcher) { + final String token = matcher.group(2); + final String value; + if (token == null) { + value = matcher.group(3); + } else { + if (token.length() > 2 + && token.charAt(0) == '\'' + && token.charAt(token.length() - 1) == '\'') { + value = token.substring(1, token.length() - 1); + } else { + value = token; + } + } + if (value == null) { + throw new IllegalStateException("Bad parameter value"); + } + return value.toLowerCase(Locale.US); + } +} diff --git a/src/main/java/wtf/g4s8/mime/MimeTypeSmart.java b/src/main/java/wtf/g4s8/mime/MimeTypeSmart.java deleted file mode 100644 index 95c9b1b..0000000 --- a/src/main/java/wtf/g4s8/mime/MimeTypeSmart.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2017-2021 Kirill Ch. - * https://github.com/g4s8/mime/LICENSE.txt - */ -package wtf.g4s8.mime; - -import java.io.IOException; -import java.util.Map; - -/** - * Smart MIME type. - *

- * Aware of some parameters, like `charset` etc. - *

- * - * @since 0.1 - */ -public final class MimeTypeSmart implements MimeType { - - /** - * Charset parameter. - */ - private static final String PARAM_CHARSET = "charset"; - - /** - * Origin MIME type. - */ - private final MimeType orig; - - /** - * Ctor. - * @param origin Origin MIME type - */ - public MimeTypeSmart(final MimeType origin) { - this.orig = origin; - } - - @Override - public String type() throws IOException { - return this.orig.type(); - } - - @Override - public String subtype() throws IOException { - return this.orig.subtype(); - } - - @Override - public Map params() throws IOException { - return this.orig.params(); - } - - /** - * Read charset from parameters. - * @return Charset string - * @throws IOException If not found or invalid. - */ - public String charset() throws IOException { - final Map params = this.params(); - if (!params.containsKey(MimeTypeSmart.PARAM_CHARSET)) { - throw new IOException("Charset is not provided"); - } - final String charset = params.get(MimeTypeSmart.PARAM_CHARSET); - if (charset == null || "".equals(charset)) { - throw new IOException("The charset parameter is not set"); - } - return charset; - } - - @Override - public String toString() { - return this.orig.toString(); - } -} diff --git a/src/main/java/wtf/g4s8/mime/test/HmMimeHasParam.java b/src/main/java/wtf/g4s8/mime/test/HmMimeHasParam.java index dae1244..a15e903 100644 --- a/src/main/java/wtf/g4s8/mime/test/HmMimeHasParam.java +++ b/src/main/java/wtf/g4s8/mime/test/HmMimeHasParam.java @@ -5,7 +5,7 @@ package wtf.g4s8.mime.test; import wtf.g4s8.mime.MimeType; -import java.io.IOException; +import java.util.Optional; import org.hamcrest.CoreMatchers; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -51,23 +51,14 @@ public HmMimeHasParam(final String name, final Matcher expected) { @Override public boolean matchesSafely(final MimeType type) { - try { - final String actual = type.params().get(this.name); - return this.expected.matches(actual); - } catch (final IOException err) { - throw new UnsupportedOperationException( - "Matcher failed due to error", - err - ); - } + return type.param(name).map(this.expected::matches).orElse(false); } @Override public void describeTo(final Description description) { - description.appendText("MIME with param ") - .appendDescriptionOf( - new ParamDescription(this.name, this.expected) - ); + description.appendText("MIME with param ").appendDescriptionOf( + new ParamDescription(this.name, this.expected) + ); } @Override @@ -75,16 +66,12 @@ public void describeMismatchSafely( final MimeType item, final Description desc ) { - try { + final Optional opt = item.param(name); + if (opt.isPresent()) { desc.appendText("was param ") - .appendDescriptionOf( - new ParamDescription( - this.name, - item.params().get(this.name) - ) - ); - } catch (final IOException err) { - desc.appendText("was not set"); + .appendDescriptionOf(new ParamDescription(this.name, opt.get())); + } else { + desc.appendText("no param `").appendValue(name).appendText("`"); } } diff --git a/src/main/java/wtf/g4s8/mime/test/HmMimeHasSubType.java b/src/main/java/wtf/g4s8/mime/test/HmMimeHasSubType.java index e419fb2..57004b4 100644 --- a/src/main/java/wtf/g4s8/mime/test/HmMimeHasSubType.java +++ b/src/main/java/wtf/g4s8/mime/test/HmMimeHasSubType.java @@ -5,7 +5,6 @@ package wtf.g4s8.mime.test; import wtf.g4s8.mime.MimeType; -import java.io.IOException; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; @@ -32,14 +31,7 @@ public HmMimeHasSubType(final String expected) { @Override public boolean matchesSafely(final MimeType type) { - try { - return this.expected.equals(type.subtype()); - } catch (final IOException err) { - throw new UnsupportedOperationException( - "Matcher failed due to error", - err - ); - } + return this.expected.equals(type.subtype()); } @Override diff --git a/src/main/java/wtf/g4s8/mime/test/HmMimeHasType.java b/src/main/java/wtf/g4s8/mime/test/HmMimeHasType.java index 51bf6f9..82e823c 100644 --- a/src/main/java/wtf/g4s8/mime/test/HmMimeHasType.java +++ b/src/main/java/wtf/g4s8/mime/test/HmMimeHasType.java @@ -5,7 +5,6 @@ package wtf.g4s8.mime.test; import wtf.g4s8.mime.MimeType; -import java.io.IOException; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; @@ -32,14 +31,7 @@ public HmMimeHasType(final String expected) { @Override public boolean matchesSafely(final MimeType type) { - try { - return this.expected.equals(type.type()); - } catch (final IOException err) { - throw new UnsupportedOperationException( - "Matcher failed due to error", - err - ); - } + return this.expected.equals(type.type()); } @Override diff --git a/src/test/java/wtf/g4s8/mime/MimeTypeOfTest.java b/src/test/java/wtf/g4s8/mime/MimeTypeOfTest.java index 3c2b161..eac29da 100644 --- a/src/test/java/wtf/g4s8/mime/MimeTypeOfTest.java +++ b/src/test/java/wtf/g4s8/mime/MimeTypeOfTest.java @@ -30,30 +30,38 @@ public MimeTypeOfTest() { Arrays.asList( new SimpleTest<>( "Parse type", - new MimeTypeOf("application/pdf"), + new MimeTypeOfString("application/pdf"), new HmMimeHasType("application") ), new SimpleTest<>( "Parse subtype", - new MimeTypeOf("image/bmp"), + new MimeTypeOfString("image/bmp"), new HmMimeHasSubType("bmp") ), new SimpleTest<>( "Parse boundary param", - new MimeTypeOf("multipart/byteranges; boundary=3d6b6a416f9b5"), + new MimeTypeOfString("multipart/byteranges; boundary=3d6b6a416f9b5"), new HmMimeHasParam("boundary", "3d6b6a416f9b5") ), new SimpleTest<>( - "Parse encoding param", - new MimeTypeOf("text/xml; encoding=utf-8"), + "Parse param ignore case", + new MimeTypeOfString("text/xml; EncOdinG=utf-8"), new HmMimeHasParam( "encoding", - Matchers.equalToIgnoringCase("UtF-8") + Matchers.equalTo("utf-8") + ) + ), + new SimpleTest<>( + "Parse multiple params", + new MimeTypeOfString("test/test; FoO=1 bAr=Two "), + Matchers.allOf( + new HmMimeHasParam("foo", "1"), + new HmMimeHasParam("bar", "two") ) ), new SimpleTest<>( "Convert toString", - new MimeTypeOf("text/plain"), + new MimeTypeOfString("text/plain"), Matchers.hasToString(String.join("/", "text", "plain")) ) ) diff --git a/src/test/java/wtf/g4s8/mime/MimeTypeSmartTest.java b/src/test/java/wtf/g4s8/mime/MimeTypeSmartTest.java deleted file mode 100644 index 7a64106..0000000 --- a/src/test/java/wtf/g4s8/mime/MimeTypeSmartTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2017-2021 Kirill Ch. - * https://github.com/g4s8/mime/LICENSE.txt - */ -package wtf.g4s8.mime; - -import org.hamcrest.Matchers; -import wtf.g4s8.oot.SequentialTests; -import wtf.g4s8.oot.SimpleTest; -import wtf.g4s8.oot.TestCase; - -/** - * Test case for {@link MimeTypeSmart}. - * - * @since 0.1 - */ -public final class MimeTypeSmartTest extends TestCase.Wrap { - - /** - * New test case. - */ - public MimeTypeSmartTest() { - super( - new SequentialTests( - new SimpleTest( - "Parse charset", - () -> new MimeTypeSmart( - new MimeTypeOf("text/html; charset=utf-8") - ).charset(), - Matchers.equalTo("utf-8") - ) - ) - ); - } -} diff --git a/src/test/java/wtf/g4s8/mime/Test.java b/src/test/java/wtf/g4s8/mime/Test.java index 02230f8..8a39075 100644 --- a/src/test/java/wtf/g4s8/mime/Test.java +++ b/src/test/java/wtf/g4s8/mime/Test.java @@ -22,7 +22,7 @@ public final class Test extends TestCase.Wrap { */ private Test() { super( - new SequentialTests(new MimeTypeOfTest(), new MimeTypeSmartTest()) + new SequentialTests(new MimeTypeOfTest()) ); }