Skip to content

Commit bbbb3c5

Browse files
committed
feat: ✨ Adds first implementation
1 parent c5308e7 commit bbbb3c5

File tree

8 files changed

+357
-1
lines changed

8 files changed

+357
-1
lines changed

README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
1-
# java-data-url-handler
1+
[![License MIT](https://img.shields.io/github/license/jycr/java-data-url-handler)](https://opensource.org/license/mit)
2+
[![Publish package to the Maven Central Repository](https://github.com/jycr/java-data-url-handler/actions/workflows/publish_to_maven_central.yml/badge.svg)](https://github.com/jycr/java-data-url-handler/actions/workflows/publish_to_maven_central.yml)
3+
[![Version on Maven Central](https://img.shields.io/maven-central/v/io.github.jycr/java-data-url-handler)](https://search.maven.org/artifact/io.github.jycr/java-data-url-handler)
4+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jycr_java-data-url-handler&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=jycr_java-data-url-handler)
5+
6+
# java-data-url-handler
7+
8+
When adding this library as a dependency in your project, you will be able to handle "data URLs" ([RFC-2397](https://datatracker.ietf.org/doc/html/rfc2397)) in your application.
9+
10+
## Prerequisites
11+
12+
You need Java >= 11 to use this library.
13+
14+
## Usage
15+
16+
When you want to get the content of a "data URL":
17+
18+
```java
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.net.URI;
22+
import java.nio.charset.StandardCharsets;
23+
24+
public class Test {
25+
public static void main(String[] args) throws IOException {
26+
URL url = URI.create("data:text/plain,Hello%2C%20World!").toURL();
27+
try(InputStream in = url.openStream()) {
28+
System.out.println(new String(in.readAllBytes(), StandardCharsets.UTF_8));
29+
}
30+
}
31+
}
32+
```
33+
34+
Without this library, you will get an error like this:
35+
36+
```
37+
Exception in thread "main" java.net.MalformedURLException: unknown protocol: data
38+
at java.base/java.net.URL.<init>(URL.java:779)
39+
at java.base/java.net.URL.<init>(URL.java:654)
40+
at java.base/java.net.URL.<init>(URL.java:590)
41+
```
42+
43+
With this library in your classpath, you can handle the data URL and get the content of the data URL.
44+
45+
The output of the code above will be:
46+
47+
```
48+
Hello, World!
49+
```
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.UnsupportedEncodingException;
7+
import java.net.MalformedURLException;
8+
import java.net.URL;
9+
import java.net.URLConnection;
10+
import java.net.URLDecoder;
11+
import java.nio.charset.Charset;
12+
import java.util.Base64;
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
15+
16+
import static java.nio.charset.StandardCharsets.US_ASCII;
17+
18+
/**
19+
* The data scheme URLConnection.
20+
* <p>The data URI scheme Data protocol Syntax:</p>
21+
* <pre>data:[<mediatype>][;base64],<data></pre>
22+
*
23+
* @see <a href="https://www.rfc-editor.org/rfc/rfc2397#section-2">RFC-2397</a>
24+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs">mdn web docs - Data URLs</a>
25+
*/
26+
public class DataUriConnection extends URLConnection {
27+
28+
/**
29+
* Syntax of data URL scheme:
30+
* <pre>
31+
* dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
32+
* mediatype := [ type "/" subtype ] *( ";" parameter )
33+
* data := *urlchar
34+
* parameter := attribute "=" value
35+
* </pre>
36+
*/
37+
private static final Pattern DATA_URL_SCHEME_PATTERN = Pattern.compile("data:(<mediatype>?(<contentType>?.*?/.*?)?(?:;(<paramKey>?.*?)=(<paramValue>?.*?))?)(?:;(<base64Flag>?base64)?)?,(<data>?.*)");
38+
39+
private static final Charset DEFAULT_CONTENT_CHARSET = US_ASCII;
40+
/**
41+
* Default mime type for data protocol.
42+
* See: <a href="https://www.rfc-editor.org/rfc/rfc2397#section-2">RFC-2397 - Description</a>
43+
*/
44+
private static final String DEFAULT_MEDIATYPE = "text/plain;charset=" + DEFAULT_CONTENT_CHARSET.name();
45+
46+
private final boolean valid;
47+
private final Charset charset;
48+
private final boolean isBase64;
49+
private final String data;
50+
private final String mediatype;
51+
52+
public DataUriConnection(final URL url) throws MalformedURLException {
53+
super(url);
54+
final Matcher matcher = DATA_URL_SCHEME_PATTERN.matcher(url.toString());
55+
this.valid = matcher.matches();
56+
if (!this.valid) {
57+
throw new MalformedURLException("Invalid data URL: " + url);
58+
}
59+
this.data = matcher.group("data");
60+
61+
String mediatypeGroup = matcher.group("mediatype");
62+
this.mediatype = (mediatypeGroup != null && !mediatypeGroup.isEmpty()) ? mediatypeGroup : DEFAULT_MEDIATYPE;
63+
this.isBase64 = "base64".equals(matcher.group("base64Flag"));
64+
65+
String paramKey = matcher.group("paramKey");
66+
String paramValue = matcher.group("paramValue");
67+
this.charset = "charset".equals(paramKey) ? Charset.forName(paramValue) : DEFAULT_CONTENT_CHARSET;
68+
}
69+
70+
@Override
71+
public void connect() {
72+
if (this.valid) {
73+
this.connected = true;
74+
}
75+
}
76+
77+
@Override
78+
public InputStream getInputStream() throws IOException {
79+
if (!connected) {
80+
throw new IOException();
81+
}
82+
return new ByteArrayInputStream(getData());
83+
}
84+
85+
/**
86+
* <p>Returns the value of the content-type defined in data URL.</p>
87+
* <p>This value is optional and if not defined, value is <code>{@value #DEFAULT_MEDIATYPE}</code></p>
88+
*/
89+
@Override
90+
public String getContentType() {
91+
if (!connected) {
92+
return null;
93+
}
94+
return mediatype;
95+
}
96+
97+
private byte[] getData() throws UnsupportedEncodingException {
98+
if (isBase64) {
99+
return Base64.getDecoder().decode(data);
100+
}
101+
return URLDecoder.decode(data, charset).getBytes(data);
102+
}
103+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.net.MalformedURLException;
4+
import java.net.URL;
5+
import java.net.URLConnection;
6+
import java.net.URLStreamHandler;
7+
8+
public class DataUriHandler extends URLStreamHandler {
9+
@Override
10+
protected URLConnection openConnection(final URL url) throws MalformedURLException {
11+
return new DataUriConnection(url);
12+
}
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.net.URLStreamHandler;
4+
import java.net.spi.URLStreamHandlerProvider;
5+
6+
public class DataUriHandlerProvider extends URLStreamHandlerProvider {
7+
public static final String DATA_PROTOCOL = "data";
8+
9+
@Override
10+
public URLStreamHandler createURLStreamHandler(String protocol) {
11+
if(DATA_PROTOCOL.equals(protocol)){
12+
return new DataUriHandler();
13+
}
14+
return null;
15+
}
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.github.jycr.javadataurlhandler.DataUriHandlerProvider
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.net.URI;
6+
import java.nio.charset.Charset;
7+
import java.nio.charset.StandardCharsets;
8+
9+
import org.assertj.core.api.AbstractObjectAssert;
10+
import org.assertj.core.api.SoftAssertions;
11+
import org.junit.jupiter.api.Test;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
class DataUriConnectionTest {
16+
@Test
17+
void testMinimal() throws IOException {
18+
assertDataUriAsString(
19+
"data:,A%20brief%20note",
20+
"text/plain;charset=US-ASCII",
21+
StandardCharsets.US_ASCII,
22+
12
23+
)
24+
.isEqualTo("A brief note");
25+
}
26+
27+
@Test
28+
void testImage() throws IOException {
29+
DataUriConnection connection = assertDataUri(
30+
"data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7",
31+
"image/gif",
32+
StandardCharsets.US_ASCII,
33+
273
34+
);
35+
assertThat(connection.getInputStream().readAllBytes())
36+
.isEqualTo(new byte[]{
37+
71, 73, 70, 56, 55, 97, 48, 0, 48, 0, -16, 0, 0, 0, 0, 0, -1, -1, -1, 44, 0, 0, 0, 0, 48, 0, 48, 0, 0, 2, -16, -116, -113, -87, -53,
38+
-19, -33, 0, -100, 14, 72, -117, 115, -80, -76, -85, 12, -122, 30, 20, -106, -90, 52, 46, -25, 42, -90, 9, -117, 26, -89, 43, -81, 81,
39+
73, 111, 56, -39, -114, 102, -41, -13, -128, 92, -63, -55, 48, 117, -47, 8, 49, 57, 29, 19, -88, 20, 30, -114, 20, 77, -52, -25, -28,
40+
84, -97, -71, 98, 37, 42, -83, 113, 121, 99, -103, 74, -121, -16, -34, -72, -41, 15, -25, 34, -42, 26, -63, 27, -76, -72, -114, 74,
41+
-106, -65, 76, -8, 59, 38, -110, 71, -57, 39, -89, 119, 53, -109, 5, -11, -12, 115, 19, -89, 40, -8, -32, 7, 56, -72, 118, 40, -89,
42+
88, 103, 100, 23, -55, 35, 117, -7, -14, 113, 6, 87, 101, -26, 6, 122, 57, -119, -107, -105, -122, -105, -40, -74, -119, -59, 106,
43+
104, -43, 90, -118, 84, -6, 23, -104, -119, -9, 73, -70, -102, -5, -109, 75, -125, -118, -45, -109, 52, -68, 57, 52, 67, -123, -44,
44+
-73, 11, -54, 59, 58, -36, 119, 120, -86, -10, -24, -84, 23, -51, 76, -44, -30, 28, 71, -71, 68, 123, 12, 28, 46, -98, -19, -72, -45,
45+
100, 51, 123, -115, -99, -83, -59, -40, -108, 5, -93, -34, -43, -82, 44, -65, 94, 94, 63, 127, 87, 31, -27, 85, -77, 95, 44, -90, -20,
46+
-15, -31, -121, 30, 29, 110, 6, 10, 0, 0, 59
47+
});
48+
}
49+
50+
@Test
51+
void testGreekCharaters() throws IOException {
52+
assertDataUriAsString(
53+
"data:text/plain;charset=iso-8859-7,%D6%EF%E9%ED%E9%EA%DE%E9%E1+%E3%F1%DC%EC%EC%E1%F4%E1",
54+
"text/plain;charset=ISO-8859-7",
55+
Charset.forName("ISO-8859-7"),
56+
18
57+
)
58+
.isEqualTo("Φοινικήια γράμματα");
59+
}
60+
61+
@Test
62+
void testApplicationData() throws IOException {
63+
assertDataUriAsString(
64+
"data:application/vnd-xxx-query,select_vcount,fcol_from_fieldtable/local",
65+
"application/vnd-xxx-query",
66+
StandardCharsets.US_ASCII,
67+
40
68+
)
69+
.isEqualTo("select_vcount,fcol_from_fieldtable/local");
70+
}
71+
72+
@Test
73+
void testPlainText() throws IOException {
74+
assertDataUriAsString(
75+
"data:text/plain,Hello%2C%20World!",
76+
"text/plain;charset=US-ASCII",
77+
StandardCharsets.US_ASCII,
78+
13
79+
)
80+
.isEqualTo("Hello, World!");
81+
}
82+
83+
private AbstractObjectAssert<?, String> assertDataUriAsString(
84+
String dataUri,
85+
String expectedContentType,
86+
Charset expectedCharset,
87+
long expectedContentLength
88+
) throws IOException {
89+
DataUriConnection connection = assertDataUri(dataUri, expectedContentType, expectedCharset, expectedContentLength);
90+
91+
return assertThat(connection.getContent())
92+
.describedAs("Content")
93+
.isInstanceOf(InputStream.class)
94+
.extracting(content -> {
95+
try {
96+
return ((InputStream) content).readAllBytes();
97+
} catch (IOException e) {
98+
throw new RuntimeException(e);
99+
}
100+
})
101+
.extracting(content -> new String(content, expectedCharset))
102+
;
103+
}
104+
105+
106+
private DataUriConnection assertDataUri(
107+
String dataUri,
108+
String expectedContentType,
109+
Charset expectedCharset,
110+
long expectedContentLength
111+
) throws IOException {
112+
DataUriConnection connection = new DataUriConnection(URI.create(dataUri).toURL());
113+
connection.connect();
114+
SoftAssertions assertions = new SoftAssertions();
115+
assertions.assertThat(connection.getContentType())
116+
.describedAs("Content type")
117+
.isEqualTo(expectedContentType);
118+
assertions.assertThat(connection.getCharset())
119+
.describedAs("Charset")
120+
.isEqualTo(expectedCharset);
121+
assertions.assertThat(connection.getContentLengthLong())
122+
.describedAs("Content length")
123+
.isEqualTo(expectedContentLength);
124+
assertions.assertThat(connection.getContentEncoding())
125+
.describedAs("Content encoding")
126+
.isNull();
127+
assertions.assertAll();
128+
129+
return connection;
130+
}
131+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.net.spi.URLStreamHandlerProvider;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
class DataUriHandlerProviderTest {
10+
11+
@Test
12+
void testCreateURLStreamHandler() {
13+
URLStreamHandlerProvider provider = new DataUriHandlerProvider();
14+
assertThat(provider.createURLStreamHandler("data")).isNotNull();
15+
assertThat(provider.createURLStreamHandler("foo")).isNull();
16+
assertThat(provider.createURLStreamHandler("http")).isNull();
17+
}
18+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.github.jycr.javadataurlhandler;
2+
3+
import java.net.MalformedURLException;
4+
import java.net.URI;
5+
import java.net.URL;
6+
import java.net.URLConnection;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
class DataUriHandlerTest {
13+
14+
@Test
15+
void testOpenConnection() throws MalformedURLException {
16+
// Given
17+
URL dataUrl = URI.create("data:text/plain,Hello%2C%20World!").toURL();
18+
DataUriHandler handler = new DataUriHandler();
19+
20+
// When
21+
URLConnection urlConnection = handler.openConnection(dataUrl);
22+
23+
// Then
24+
assertThat(urlConnection).isNotNull();
25+
}
26+
}

0 commit comments

Comments
 (0)