Skip to content

Commit eea61d6

Browse files
authored
Fixes uri, uri-reference, iri, iri-reference formats and does iri to uri conversion (#983)
* Fix uri format and uri-reference format * Fix iri format and iri-reference format * Convert iri to uri
1 parent 95911ba commit eea61d6

File tree

12 files changed

+702
-4
lines changed

12 files changed

+702
-4
lines changed

src/main/java/com/networknt/schema/format/IriFormat.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ protected boolean validate(URI uri) {
1616
return false;
1717
}
1818
}
19+
20+
String query = uri.getQuery();
21+
if (query != null) {
22+
// [ and ] must be percent encoded
23+
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
24+
return false;
25+
}
26+
}
1927
}
2028
return result;
2129
}

src/main/java/com/networknt/schema/format/IriReferenceFormat.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@
88
public class IriReferenceFormat extends AbstractRFC3986Format {
99
@Override
1010
protected boolean validate(URI uri) {
11+
String authority = uri.getAuthority();
12+
if (authority != null) {
13+
if (IPv6Format.PATTERN.matcher(authority).matches() ) {
14+
return false;
15+
}
16+
}
17+
String query = uri.getQuery();
18+
if (query != null) {
19+
// [ and ] must be percent encoded
20+
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
21+
return false;
22+
}
23+
}
1124
return true;
1225
}
1326

src/main/java/com/networknt/schema/format/UriFormat.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,21 @@
88
public class UriFormat extends AbstractRFC3986Format {
99
@Override
1010
protected boolean validate(URI uri) {
11-
return uri.isAbsolute();
11+
boolean result = uri.isAbsolute();
12+
if (result) {
13+
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
14+
result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
15+
if (result) {
16+
String query = uri.getQuery();
17+
if (query != null) {
18+
// [ and ] must be percent encoded
19+
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
20+
return false;
21+
}
22+
}
23+
}
24+
}
25+
return result;
1226
}
1327

1428
@Override

src/main/java/com/networknt/schema/format/UriReferenceFormat.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@
88
public class UriReferenceFormat extends AbstractRFC3986Format {
99
@Override
1010
protected boolean validate(URI uri) {
11-
return true;
11+
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
12+
boolean result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
13+
if (result) {
14+
String query = uri.getQuery();
15+
if (query != null) {
16+
// [ and ] must be percent encoded
17+
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
18+
return false;
19+
}
20+
}
21+
}
22+
return result;
1223
}
1324

1425
@Override

src/main/java/com/networknt/schema/resource/UriSchemaLoader.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,57 @@
1818
import java.io.IOException;
1919
import java.io.InputStream;
2020
import java.net.HttpURLConnection;
21+
import java.net.MalformedURLException;
2122
import java.net.URI;
2223
import java.net.URL;
2324
import java.net.URLConnection;
2425

2526
import com.networknt.schema.AbsoluteIri;
27+
import com.networknt.schema.utils.AbsoluteIris;
2628

2729
/**
2830
* Loads from uri.
2931
*/
3032
public class UriSchemaLoader implements SchemaLoader {
3133
@Override
3234
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
33-
URI uri = URI.create(absoluteIri.toString());
35+
URI uri = toURI(absoluteIri);
36+
URL url = toURL(uri);
3437
return () -> {
35-
URLConnection conn = uri.toURL().openConnection();
38+
URLConnection conn = url.openConnection();
3639
return this.openConnectionCheckRedirects(conn);
3740
};
3841
}
3942

43+
/**
44+
* Converts an AbsoluteIRI to a URI.
45+
* <p>
46+
* Internationalized domain names will be converted using java.net.IDN.toASCII.
47+
*
48+
* @param absoluteIri the absolute IRI
49+
* @return the URI
50+
*/
51+
protected URI toURI(AbsoluteIri absoluteIri) {
52+
return URI.create(AbsoluteIris.toUri(absoluteIri));
53+
}
54+
55+
/**
56+
* Converts a URI to a URL.
57+
* <p>
58+
* This will throw if the URI is not a valid URL. For instance if the URI is not
59+
* absolute.
60+
*
61+
* @param uri the URL
62+
* @return the URL
63+
*/
64+
protected URL toURL(URI uri) {
65+
try {
66+
return uri.toURL();
67+
} catch (MalformedURLException e) {
68+
throw new IllegalArgumentException(e);
69+
}
70+
}
71+
4072
// https://www.cs.mun.ca/java-api-1.5/guide/deployment/deployment-guide/upgrade-guide/article-17.html
4173
protected InputStream openConnectionCheckRedirects(URLConnection c) throws IOException {
4274
boolean redir;
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.utils;
17+
18+
import java.io.UnsupportedEncodingException;
19+
import java.net.IDN;
20+
import java.net.URI;
21+
import java.net.URLEncoder;
22+
23+
import com.networknt.schema.AbsoluteIri;
24+
25+
/**
26+
* Utility functions for AbsoluteIri.
27+
*/
28+
public class AbsoluteIris {
29+
/**
30+
* Converts an IRI to a URI.
31+
*
32+
* @param iri the IRI to convert
33+
* @return the URI string
34+
*/
35+
public static String toUri(AbsoluteIri iri) {
36+
String iriString = iri.toString();
37+
boolean ascii = isAscii(iriString);
38+
if (ascii) {
39+
int index = iriString.indexOf('?');
40+
if (index == -1) {
41+
return iriString;
42+
}
43+
String rest = iriString.substring(0, index + 1);
44+
String query = iriString.substring(index + 1);
45+
StringBuilder result = new StringBuilder(rest);
46+
handleQuery(result, query);
47+
return result.toString();
48+
}
49+
String[] parts = iriString.split(":"); // scheme + rest
50+
if (parts.length == 2) {
51+
StringBuilder result = new StringBuilder(parts[0]);
52+
result.append(":");
53+
54+
String rest = parts[1];
55+
if (rest.startsWith("//")) {
56+
rest = rest.substring(2);
57+
result.append("//");
58+
} else if (rest.startsWith("/")) {
59+
rest = rest.substring(1);
60+
result.append("/");
61+
}
62+
String[] query = rest.split("\\?"); // rest ? query
63+
String[] restParts = query[0].split("/");
64+
for (int x = 0; x < restParts.length; x++) {
65+
String p = restParts[x];
66+
if (x == 0) {
67+
// Domain
68+
if (isAscii(p)) {
69+
result.append(p);
70+
} else {
71+
result.append(unicodeToASCII(p));
72+
}
73+
} else {
74+
result.append(p);
75+
}
76+
if (x != restParts.length - 1) {
77+
result.append("/");
78+
}
79+
}
80+
if (query[0].endsWith("/")) {
81+
result.append("/");
82+
}
83+
if (query.length == 2) {
84+
// handle query string
85+
result.append("?");
86+
handleQuery(result, query[1]);
87+
}
88+
89+
return URI.create(result.toString()).toASCIIString();
90+
}
91+
return iriString;
92+
}
93+
94+
/**
95+
* Determine if a string is US ASCII.
96+
*
97+
* @param value to test
98+
* @return true if ASCII
99+
*/
100+
static boolean isAscii(String value) {
101+
return value.codePoints().allMatch(ch -> ch < 0x7F);
102+
}
103+
104+
/**
105+
* Ensures that the query parameters are properly URL encoded.
106+
*
107+
* @param result the string builder to add to
108+
* @param query the query string
109+
*/
110+
static void handleQuery(StringBuilder result, String query) {
111+
String[] queryParts = query.split("&");
112+
for (int y = 0; y < queryParts.length; y++) {
113+
String queryPart = queryParts[y];
114+
115+
String[] nameValue = queryPart.split("=");
116+
try {
117+
result.append(URLEncoder.encode(nameValue[0], "UTF-8"));
118+
if (nameValue.length == 2) {
119+
result.append("=");
120+
result.append(URLEncoder.encode(nameValue[1], "UTF-8"));
121+
}
122+
} catch (UnsupportedEncodingException e) {
123+
throw new IllegalArgumentException(e);
124+
}
125+
if (y != queryParts.length - 1) {
126+
result.append("&");
127+
}
128+
}
129+
}
130+
131+
// The following routines are from apache commons validator routines
132+
// DomainValidator
133+
static String unicodeToASCII(final String input) {
134+
try {
135+
final String ascii = IDN.toASCII(input);
136+
if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) {
137+
return ascii;
138+
}
139+
final int length = input.length();
140+
if (length == 0) { // check there is a last character
141+
return input;
142+
}
143+
// RFC3490 3.1. 1)
144+
// Whenever dots are used as label separators, the following
145+
// characters MUST be recognized as dots: U+002E (full stop), U+3002
146+
// (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61
147+
// (halfwidth ideographic full stop).
148+
final char lastChar = input.charAt(length - 1);// fetch original last char
149+
switch (lastChar) {
150+
case '\u002E': // "." full stop
151+
case '\u3002': // ideographic full stop
152+
case '\uFF0E': // fullwidth full stop
153+
case '\uFF61': // halfwidth ideographic full stop
154+
return ascii + "."; // restore the missing stop
155+
default:
156+
return ascii;
157+
}
158+
} catch (final IllegalArgumentException e) { // input is not valid
159+
return input;
160+
}
161+
}
162+
163+
private static class IDNBUGHOLDER {
164+
private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot();
165+
166+
private static boolean keepsTrailingDot() {
167+
final String input = "a."; // must be a valid name
168+
return input.equals(IDN.toASCII(input));
169+
}
170+
}
171+
172+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.format;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
import java.util.Set;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import com.networknt.schema.InputFormat;
25+
import com.networknt.schema.JsonSchema;
26+
import com.networknt.schema.JsonSchemaFactory;
27+
import com.networknt.schema.SchemaValidatorsConfig;
28+
import com.networknt.schema.SpecVersion.VersionFlag;
29+
import com.networknt.schema.ValidationMessage;
30+
31+
class IriFormatTest {
32+
@Test
33+
void uriShouldPass() {
34+
String schemaData = "{\r\n"
35+
+ " \"format\": \"iri\"\r\n"
36+
+ "}";
37+
38+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
39+
config.setFormatAssertionsEnabled(true);
40+
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
41+
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf\"",
42+
InputFormat.JSON);
43+
assertTrue(messages.isEmpty());
44+
}
45+
46+
@Test
47+
void queryWithBracketsShouldFail() {
48+
String schemaData = "{\r\n"
49+
+ " \"format\": \"iri\"\r\n"
50+
+ "}";
51+
52+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
53+
config.setFormatAssertionsEnabled(true);
54+
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
55+
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf?filter[test]=1\"",
56+
InputFormat.JSON);
57+
assertFalse(messages.isEmpty());
58+
}
59+
60+
@Test
61+
void iriShouldPass() {
62+
String schemaData = "{\r\n"
63+
+ " \"format\": \"iri\"\r\n"
64+
+ "}";
65+
66+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
67+
config.setFormatAssertionsEnabled(true);
68+
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
69+
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/produktdatenblätter.pdf\"",
70+
InputFormat.JSON);
71+
assertTrue(messages.isEmpty());
72+
}
73+
74+
}

0 commit comments

Comments
 (0)