This is an automated email from the ASF dual-hosted git repository. veithen pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/ws-axiom.git
The following commit(s) were added to refs/heads/master by this push: new ff9bec91a [AXIOM-506]: Design APIs to decouple OMMultipartWriter from Activation ff9bec91a is described below commit ff9bec91a4162996c9b77704b893597c2acf424a Author: Andreas Veithen <andreas.veit...@gmail.com> AuthorDate: Sat Nov 5 14:02:04 2022 +0000 [AXIOM-506]: Design APIs to decouple OMMultipartWriter from Activation --- axiom-api/pom.xml | 3 +- .../axiom/attachments/ConfigurableDataHandler.java | 37 ++++++++++++ .../xop/CombinedContentTransferEncodingPolicy.java | 50 +++++++++++++++++ .../format/xop/ContentTransferEncodingPolicy.java | 59 ++++++++++++++++++++ .../axiom/om/format/xop/ContentTypeProvider.java | 38 +++++++++++++ .../apache/axiom/om/impl/OMMultipartWriter.java | 65 ++++++++-------------- .../activation/DataHandlerContentTypeProvider.java | 58 +++++++++++++++++++ .../attachments/ConfigurableDataHandlerTest.java | 59 ++++++++++++++++++++ .../DataHandlerContentTypeProviderTest.java | 53 ++++++++++++++++++ 9 files changed, 378 insertions(+), 44 deletions(-) diff --git a/axiom-api/pom.xml b/axiom-api/pom.xml index 044057196..6de2c96cb 100644 --- a/axiom-api/pom.xml +++ b/axiom-api/pom.xml @@ -253,8 +253,9 @@ <ignore> <!-- Bad API design: a public API shouldn't depend on classes in an impl package in its interface --> org.apache.axiom.attachments.lifecycle.LifecycleManager -> org.apache.axiom.attachments.lifecycle.impl.FileAccessor, - <!-- TODO --> + <!-- TODO(AXIOM-506) --> org.apache.axiom.om.impl.OMMultipartWriter -> org.apache.axiom.attachments.ConfigurableDataHandler, + org.apache.axiom.util.base64.Base64Utils -> org.apache.axiom.util.activation.DataSourceUtils, <!-- o.a.a.soap should be a layer on top of o.a.a.om --> org.apache.axiom.om.OMAbstractFactory -> org.apache.axiom.soap.SOAPFactory, org.apache.axiom.om.OMMetaFactory -> org.apache.axiom.soap.SOAPFactory, diff --git a/axiom-api/src/main/java/org/apache/axiom/attachments/ConfigurableDataHandler.java b/axiom-api/src/main/java/org/apache/axiom/attachments/ConfigurableDataHandler.java index 72380291f..171947589 100644 --- a/axiom-api/src/main/java/org/apache/axiom/attachments/ConfigurableDataHandler.java +++ b/axiom-api/src/main/java/org/apache/axiom/attachments/ConfigurableDataHandler.java @@ -21,6 +21,15 @@ package org.apache.axiom.attachments; import javax.activation.DataHandler; import javax.activation.DataSource; + +import org.apache.axiom.blob.Blob; +import org.apache.axiom.mime.ContentTransferEncoding; +import org.apache.axiom.mime.ContentType; +import org.apache.axiom.om.format.xop.ContentTransferEncodingPolicy; +import org.apache.axiom.util.activation.DataHandlerUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import java.net.URL; /** @@ -36,6 +45,34 @@ import java.net.URL; * @see javax.activation.DataHandler */ public class ConfigurableDataHandler extends DataHandler { + private static final Log log = LogFactory.getLog(ConfigurableDataHandler.class); + + /** + * {@link ContentTransferEncodingPolicy} implementation that recognizes blobs created from + * {@link ConfigurableDataHandler} objects and returns the content transfer encoding + * set using {@link ConfigurableDataHandler#setTransferEncoding(String)}. + */ + public static final ContentTransferEncodingPolicy CONTENT_TRANSFER_ENCODING_POLICY = new ContentTransferEncodingPolicy() { + @Override + public ContentTransferEncoding getContentTransferEncoding(Blob blob, ContentType contentType) { + DataHandler dataHandler = DataHandlerUtils.toDataHandler(blob); + if (!(dataHandler instanceof ConfigurableDataHandler)) { + return null; + } + String cte = ((ConfigurableDataHandler)dataHandler).getTransferEncoding(); + if (cte == null) { + return null; + } + switch (cte) { + case "8bit": return ContentTransferEncoding.EIGHT_BIT; + case "binary": return ContentTransferEncoding.BINARY; + case "base64": return ContentTransferEncoding.BASE64; + default: + log.warn(String.format("Unrecognized content transfer encoding: %s", cte)); + return null; + } + } + }; private String transferEncoding; diff --git a/axiom-api/src/main/java/org/apache/axiom/om/format/xop/CombinedContentTransferEncodingPolicy.java b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/CombinedContentTransferEncodingPolicy.java new file mode 100644 index 000000000..b5b45866c --- /dev/null +++ b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/CombinedContentTransferEncodingPolicy.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.om.format.xop; + +import org.apache.axiom.blob.Blob; +import org.apache.axiom.mime.ContentTransferEncoding; +import org.apache.axiom.mime.ContentType; + +/** + * {@link ContentTransferEncodingPolicy} implementation that combines multiple other + * {@link ContentTransferEncodingPolicy} instances into a single policy. It returns the + * first non-null {@link ContentTransferEncoding}. + */ +public final class CombinedContentTransferEncodingPolicy implements ContentTransferEncodingPolicy { + private final ContentTransferEncodingPolicy[] policies; + + public CombinedContentTransferEncodingPolicy(ContentTransferEncodingPolicy... policies) { + this.policies = policies.clone(); + } + + @Override + public ContentTransferEncoding getContentTransferEncoding(Blob blob, ContentType contentType) { + if (blob == null) { + return null; + } + for (ContentTransferEncodingPolicy policy : policies) { + ContentTransferEncoding cte = policy.getContentTransferEncoding(blob, contentType); + if (cte != null) { + return cte; + } + } + return null; + } +} diff --git a/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTransferEncodingPolicy.java b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTransferEncodingPolicy.java new file mode 100644 index 000000000..329acfa51 --- /dev/null +++ b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTransferEncodingPolicy.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.om.format.xop; + +import org.apache.axiom.blob.Blob; +import org.apache.axiom.mime.ContentTransferEncoding; +import org.apache.axiom.mime.ContentType; + +/** + * Determines the content transfer encoding to use for (non-root) MIME parts in an XOP package. + * Note that in general, XOP encoded messages are sent over transport protocols that can handle + * arbitrary sequences of bytes (such as HTTP), and the default {@code binary} or {@code 8bit} + * encoding will work just fine. Therefore changing the content transfer encoding is only needed + * in very specific use cases where the transport may not be able to handle arbitrary sequences + * of bytes (such as SMTP). + */ +public interface ContentTransferEncodingPolicy { + /** + * Selects the {@code base64} content transfer encoding for parts that are not textual + * (as determined by {@link ContentType#isTextual()}. + */ + ContentTransferEncodingPolicy USE_BASE64_FOR_NON_TEXTUAL_PARTS = new ContentTransferEncodingPolicy() { + @Override + public ContentTransferEncoding getContentTransferEncoding(Blob blob, ContentType contentType) { + if (contentType == null) { + return null; + } + if (!contentType.isTextual()) { + return ContentTransferEncoding.BASE64; + } + return null; + } + }; + + /** + * Determine the content transfer encoding to use for a MIME part. + * + * @param blob the content of the MIME part; may be {@code null} + * @param contentType the content type of the MIME part (as determined by {@link ContentTypeProvider}; may be {@code null} + * @return the content transfer encoding, or {@code null} if no content transfer encoding is specified (in which case another {@link ContentTransferEncodingPolicy} may be consulted or a default is used) + */ + ContentTransferEncoding getContentTransferEncoding(Blob blob, ContentType contentType); +} diff --git a/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTypeProvider.java b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTypeProvider.java new file mode 100644 index 000000000..8892a6517 --- /dev/null +++ b/axiom-api/src/main/java/org/apache/axiom/om/format/xop/ContentTypeProvider.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.om.format.xop; + +import org.apache.axiom.blob.Blob; +import org.apache.axiom.mime.ContentType; + +/** + * Determines the content type for non-root MIME parts in an XOP package. Note that in an + * XOP package, the {@code Content-Type} header is only required for the root part, and the + * content type of non-root MIME parts is not part of the infoset. This API purely + * exists to customize the serialization of an XML infoset into an XOP package. + */ +public interface ContentTypeProvider { + /** + * Determine the content type to use for a MIME part. + * + * @param blob the content of the MIME part + * @return the content type, or {@code null} if there is no specific content type + */ + ContentType getContentType(Blob blob); +} diff --git a/axiom-api/src/main/java/org/apache/axiom/om/impl/OMMultipartWriter.java b/axiom-api/src/main/java/org/apache/axiom/om/impl/OMMultipartWriter.java index 3533e9079..a6cee0138 100644 --- a/axiom-api/src/main/java/org/apache/axiom/om/impl/OMMultipartWriter.java +++ b/axiom-api/src/main/java/org/apache/axiom/om/impl/OMMultipartWriter.java @@ -21,7 +21,6 @@ package org.apache.axiom.om.impl; import java.io.IOException; import java.io.OutputStream; -import java.text.ParseException; import java.util.List; import javax.activation.DataHandler; @@ -34,7 +33,11 @@ import org.apache.axiom.mime.Header; import org.apache.axiom.mime.MediaType; import org.apache.axiom.mime.MultipartBodyWriter; import org.apache.axiom.om.OMOutputFormat; +import org.apache.axiom.om.format.xop.CombinedContentTransferEncodingPolicy; +import org.apache.axiom.om.format.xop.ContentTransferEncodingPolicy; +import org.apache.axiom.om.format.xop.ContentTypeProvider; import org.apache.axiom.soap.SOAPVersion; +import org.apache.axiom.util.activation.DataHandlerContentTypeProvider; import org.apache.axiom.util.activation.DataHandlerUtils; /** @@ -45,7 +48,8 @@ import org.apache.axiom.util.activation.DataHandlerUtils; public class OMMultipartWriter { private final OMOutputFormat format; private final MultipartBodyWriter writer; - private final boolean useCTEBase64; + private final ContentTypeProvider contentTypeProvider; + private final ContentTransferEncodingPolicy contentTransferEncodingPolicy; private final ContentType rootPartContentType; public OMMultipartWriter(OutputStream out, OMOutputFormat format) { @@ -53,8 +57,14 @@ public class OMMultipartWriter { writer = new MultipartBodyWriter(out, format.getMimeBoundary()); - useCTEBase64 = format != null && Boolean.TRUE.equals( - format.getProperty(OMOutputFormat.USE_CTE_BASE64_FOR_NON_TEXTUAL_ATTACHMENTS)); + // TODO(AXIOM-506): make this configurable in OMOutputFormat + contentTypeProvider = DataHandlerContentTypeProvider.INSTANCE; + ContentTransferEncodingPolicy contentTransferEncodingPolicy = ConfigurableDataHandler.CONTENT_TRANSFER_ENCODING_POLICY; + if (format != null && Boolean.TRUE.equals( + format.getProperty(OMOutputFormat.USE_CTE_BASE64_FOR_NON_TEXTUAL_ATTACHMENTS))) { + contentTransferEncodingPolicy = new CombinedContentTransferEncodingPolicy(contentTransferEncodingPolicy, ContentTransferEncodingPolicy.USE_BASE64_FOR_NON_TEXTUAL_PARTS); + } + this.contentTransferEncodingPolicy = contentTransferEncodingPolicy; MediaType soapContentType; if (format.isSOAP11()) { @@ -76,12 +86,9 @@ public class OMMultipartWriter { } } - private ContentTransferEncoding getContentTransferEncoding(ContentType contentType) { - if (useCTEBase64 && !contentType.isTextual()) { - return ContentTransferEncoding.BASE64; - } else { - return ContentTransferEncoding.BINARY; - } + private ContentTransferEncoding getContentTransferEncoding(Blob blob, ContentType contentType) { + ContentTransferEncoding cte = contentTransferEncodingPolicy.getContentTransferEncoding(blob, contentType); + return cte == null ? ContentTransferEncoding.BINARY : cte; } /** @@ -121,7 +128,7 @@ public class OMMultipartWriter { * if an I/O error occurs when writing to the underlying stream */ public OutputStream writePart(ContentType contentType, String contentID) throws IOException { - return writer.writePart(contentType, getContentTransferEncoding(contentType), contentID, null); + return writer.writePart(contentType, getContentTransferEncoding(null, contentType), contentID, null); } /** @@ -140,7 +147,7 @@ public class OMMultipartWriter { * if an I/O error occurs when writing to the underlying stream */ public OutputStream writePart(ContentType contentType, String contentID, List<Header> extraHeaders) throws IOException { - return writer.writePart(contentType, getContentTransferEncoding(contentType), contentID, extraHeaders); + return writer.writePart(contentType, getContentTransferEncoding(null, contentType), contentID, extraHeaders); } /** @@ -158,37 +165,9 @@ public class OMMultipartWriter { * if an I/O error occurs when writing the part to the underlying stream */ public void writePart(DataHandler dataHandler, String contentID, List<Header> extraHeaders) throws IOException { - ContentType contentType; - String contentTypeString = dataHandler.getContentType(); - if (contentTypeString == null) { - contentType = null; - } else { - try { - contentType = new ContentType(contentTypeString); - } catch (ParseException ex) { - throw new RuntimeException(ex); - } - } - ContentTransferEncoding contentTransferEncoding = null; - if (dataHandler instanceof ConfigurableDataHandler) { - switch (((ConfigurableDataHandler)dataHandler).getTransferEncoding()) { - case "8bit": - contentTransferEncoding = ContentTransferEncoding.EIGHT_BIT; - break; - case "binary": - contentTransferEncoding = ContentTransferEncoding.BINARY; - break; - default: - contentTransferEncoding = ContentTransferEncoding.BASE64; - } - } - if (contentTransferEncoding == null && contentType != null) { - contentTransferEncoding = getContentTransferEncoding(contentType); - } - if (contentTransferEncoding == null) { - contentTransferEncoding = ContentTransferEncoding.BINARY; - } - writer.writePart(DataHandlerUtils.toBlob(dataHandler), contentType, contentTransferEncoding, contentID, extraHeaders); + Blob blob = DataHandlerUtils.toBlob(dataHandler); + ContentType contentType = contentTypeProvider.getContentType(blob); + writer.writePart(blob, contentType, getContentTransferEncoding(blob, contentType), contentID, extraHeaders); } /** diff --git a/axiom-api/src/main/java/org/apache/axiom/util/activation/DataHandlerContentTypeProvider.java b/axiom-api/src/main/java/org/apache/axiom/util/activation/DataHandlerContentTypeProvider.java new file mode 100644 index 000000000..2eb083ce8 --- /dev/null +++ b/axiom-api/src/main/java/org/apache/axiom/util/activation/DataHandlerContentTypeProvider.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.util.activation; + +import java.text.ParseException; + +import javax.activation.DataHandler; + +import org.apache.axiom.blob.Blob; +import org.apache.axiom.mime.ContentType; +import org.apache.axiom.om.format.xop.ContentTypeProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * {@link ContentTypeProvider} implementation that recognizes blobs created from {@link DataHandler} + * objects and returns the content type specified by {@link DataHandler#getContentType()}. + */ +public final class DataHandlerContentTypeProvider implements ContentTypeProvider { + private static final Log log = LogFactory.getLog(DataHandlerContentTypeProvider.class); + + public static final DataHandlerContentTypeProvider INSTANCE = new DataHandlerContentTypeProvider(); + + private DataHandlerContentTypeProvider() {} + + @Override + public ContentType getContentType(Blob blob) { + if (!(blob instanceof DataHandlerBlob)) { + return null; + } + String contentType = ((DataHandlerBlob) blob).getDataHandler().getContentType(); + if (contentType == null) { + return null; + } + try { + return new ContentType(contentType); + } catch (ParseException ex) { + log.warn("Couldn't parse content type returned by DataHandler", ex); + return null; + } + } +} diff --git a/axiom-api/src/test/java/org/apache/axiom/attachments/ConfigurableDataHandlerTest.java b/axiom-api/src/test/java/org/apache/axiom/attachments/ConfigurableDataHandlerTest.java new file mode 100644 index 000000000..e525885ea --- /dev/null +++ b/axiom-api/src/test/java/org/apache/axiom/attachments/ConfigurableDataHandlerTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.attachments; + +import static com.google.common.truth.Truth.assertThat; + +import org.apache.axiom.mime.ContentTransferEncoding; +import org.apache.axiom.mime.ContentType; +import org.apache.axiom.mime.MediaType; +import org.apache.axiom.util.activation.DataHandlerUtils; +import org.junit.Test; + +public class ConfigurableDataHandlerTest { + @Test + public void testContentTransferEncoding() { + ConfigurableDataHandler dh = new ConfigurableDataHandler(new byte[10], + "application/octet-stream"); + dh.setTransferEncoding("base64"); + assertThat(ConfigurableDataHandler.CONTENT_TRANSFER_ENCODING_POLICY + .getContentTransferEncoding(DataHandlerUtils.toBlob(dh), + new ContentType(MediaType.APPLICATION_OCTET_STREAM))) + .isSameInstanceAs(ContentTransferEncoding.BASE64); + } + + @Test + public void testContentTransferEncodingNotSet() { + ConfigurableDataHandler dh = new ConfigurableDataHandler(new byte[10], + "application/octet-stream"); + assertThat(ConfigurableDataHandler.CONTENT_TRANSFER_ENCODING_POLICY + .getContentTransferEncoding(DataHandlerUtils.toBlob(dh), + new ContentType(MediaType.APPLICATION_OCTET_STREAM))).isNull(); + } + + @Test + public void testContentTransferEncodingNotRecognized() { + ConfigurableDataHandler dh = new ConfigurableDataHandler(new byte[10], + "application/octet-stream"); + dh.setTransferEncoding("foobar"); + assertThat(ConfigurableDataHandler.CONTENT_TRANSFER_ENCODING_POLICY + .getContentTransferEncoding(DataHandlerUtils.toBlob(dh), + new ContentType(MediaType.APPLICATION_OCTET_STREAM))).isNull(); + } +} diff --git a/axiom-api/src/test/java/org/apache/axiom/util/activation/DataHandlerContentTypeProviderTest.java b/axiom-api/src/test/java/org/apache/axiom/util/activation/DataHandlerContentTypeProviderTest.java new file mode 100644 index 000000000..d25b5416c --- /dev/null +++ b/axiom-api/src/test/java/org/apache/axiom/util/activation/DataHandlerContentTypeProviderTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axiom.util.activation; + +import static com.google.common.truth.Truth.assertThat; + +import javax.activation.DataHandler; + +import org.apache.axiom.blob.Blobs; +import org.apache.axiom.mime.ContentType; +import org.apache.axiom.mime.MediaType; +import org.junit.Test; + +public class DataHandlerContentTypeProviderTest { + @Test + public void testNotDataHandler() { + assertThat(DataHandlerContentTypeProvider.INSTANCE + .getContentType(Blobs.createBlob(new byte[10]))).isNull(); + } + + @Test + public void testDataHandlerWithoutContentType() { + DataHandler dh = new DataHandler(new BlobDataSource(Blobs.createBlob(new byte[10]), null)); + assertThat( + DataHandlerContentTypeProvider.INSTANCE.getContentType(DataHandlerUtils.toBlob(dh))) + .isNull(); + } + + @Test + public void testDataHandlerWithContentType() { + DataHandler dh = new DataHandler("test", "text/plain"); + ContentType contentType = DataHandlerContentTypeProvider.INSTANCE + .getContentType(DataHandlerUtils.toBlob(dh)); + assertThat(contentType).isNotNull(); + assertThat(contentType.getMediaType()).isEqualTo(MediaType.TEXT_PLAIN); + } +}