Skip to content

Commit

Permalink
Fixes uri, uri-reference, iri, iri-reference formats and does iri to …
Browse files Browse the repository at this point in the history
…uri conversion (#983)

* Fix uri format and uri-reference format

* Fix iri format and iri-reference format

* Convert iri to uri
  • Loading branch information
justin-tay authored Mar 5, 2024
1 parent 95911ba commit eea61d6
Show file tree
Hide file tree
Showing 12 changed files with 702 additions and 4 deletions.
8 changes: 8 additions & 0 deletions src/main/java/com/networknt/schema/format/IriFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ protected boolean validate(URI uri) {
return false;
}
}

String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
return result;
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/networknt/schema/format/IriReferenceFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
public class IriReferenceFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
String authority = uri.getAuthority();
if (authority != null) {
if (IPv6Format.PATTERN.matcher(authority).matches() ) {
return false;
}
}
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
return true;
}

Expand Down
16 changes: 15 additions & 1 deletion src/main/java/com/networknt/schema/format/UriFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
public class UriFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
return uri.isAbsolute();
boolean result = uri.isAbsolute();
if (result) {
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
if (result) {
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
}
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@
public class UriReferenceFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
return true;
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
boolean result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
if (result) {
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
return result;
}

@Override
Expand Down
36 changes: 34 additions & 2 deletions src/main/java/com/networknt/schema/resource/UriSchemaLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,57 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;

import com.networknt.schema.AbsoluteIri;
import com.networknt.schema.utils.AbsoluteIris;

/**
* Loads from uri.
*/
public class UriSchemaLoader implements SchemaLoader {
@Override
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
URI uri = URI.create(absoluteIri.toString());
URI uri = toURI(absoluteIri);
URL url = toURL(uri);
return () -> {
URLConnection conn = uri.toURL().openConnection();
URLConnection conn = url.openConnection();
return this.openConnectionCheckRedirects(conn);
};
}

/**
* Converts an AbsoluteIRI to a URI.
* <p>
* Internationalized domain names will be converted using java.net.IDN.toASCII.
*
* @param absoluteIri the absolute IRI
* @return the URI
*/
protected URI toURI(AbsoluteIri absoluteIri) {
return URI.create(AbsoluteIris.toUri(absoluteIri));
}

/**
* Converts a URI to a URL.
* <p>
* This will throw if the URI is not a valid URL. For instance if the URI is not
* absolute.
*
* @param uri the URL
* @return the URL
*/
protected URL toURL(URI uri) {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}

// https://www.cs.mun.ca/java-api-1.5/guide/deployment/deployment-guide/upgrade-guide/article-17.html
protected InputStream openConnectionCheckRedirects(URLConnection c) throws IOException {
boolean redir;
Expand Down
172 changes: 172 additions & 0 deletions src/main/java/com/networknt/schema/utils/AbsoluteIris.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2024 the original author or authors.
*
* Licensed 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 com.networknt.schema.utils;

import java.io.UnsupportedEncodingException;
import java.net.IDN;
import java.net.URI;
import java.net.URLEncoder;

import com.networknt.schema.AbsoluteIri;

/**
* Utility functions for AbsoluteIri.
*/
public class AbsoluteIris {
/**
* Converts an IRI to a URI.
*
* @param iri the IRI to convert
* @return the URI string
*/
public static String toUri(AbsoluteIri iri) {
String iriString = iri.toString();
boolean ascii = isAscii(iriString);
if (ascii) {
int index = iriString.indexOf('?');
if (index == -1) {
return iriString;
}
String rest = iriString.substring(0, index + 1);
String query = iriString.substring(index + 1);
StringBuilder result = new StringBuilder(rest);
handleQuery(result, query);
return result.toString();
}
String[] parts = iriString.split(":"); // scheme + rest
if (parts.length == 2) {
StringBuilder result = new StringBuilder(parts[0]);
result.append(":");

String rest = parts[1];
if (rest.startsWith("//")) {
rest = rest.substring(2);
result.append("//");
} else if (rest.startsWith("/")) {
rest = rest.substring(1);
result.append("/");
}
String[] query = rest.split("\\?"); // rest ? query
String[] restParts = query[0].split("/");
for (int x = 0; x < restParts.length; x++) {
String p = restParts[x];
if (x == 0) {
// Domain
if (isAscii(p)) {
result.append(p);
} else {
result.append(unicodeToASCII(p));
}
} else {
result.append(p);
}
if (x != restParts.length - 1) {
result.append("/");
}
}
if (query[0].endsWith("/")) {
result.append("/");
}
if (query.length == 2) {
// handle query string
result.append("?");
handleQuery(result, query[1]);
}

return URI.create(result.toString()).toASCIIString();
}
return iriString;
}

/**
* Determine if a string is US ASCII.
*
* @param value to test
* @return true if ASCII
*/
static boolean isAscii(String value) {
return value.codePoints().allMatch(ch -> ch < 0x7F);
}

/**
* Ensures that the query parameters are properly URL encoded.
*
* @param result the string builder to add to
* @param query the query string
*/
static void handleQuery(StringBuilder result, String query) {
String[] queryParts = query.split("&");
for (int y = 0; y < queryParts.length; y++) {
String queryPart = queryParts[y];

String[] nameValue = queryPart.split("=");
try {
result.append(URLEncoder.encode(nameValue[0], "UTF-8"));
if (nameValue.length == 2) {
result.append("=");
result.append(URLEncoder.encode(nameValue[1], "UTF-8"));
}
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
if (y != queryParts.length - 1) {
result.append("&");
}
}
}

// The following routines are from apache commons validator routines
// DomainValidator
static String unicodeToASCII(final String input) {
try {
final String ascii = IDN.toASCII(input);
if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) {
return ascii;
}
final int length = input.length();
if (length == 0) { // check there is a last character
return input;
}
// RFC3490 3.1. 1)
// Whenever dots are used as label separators, the following
// characters MUST be recognized as dots: U+002E (full stop), U+3002
// (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61
// (halfwidth ideographic full stop).
final char lastChar = input.charAt(length - 1);// fetch original last char
switch (lastChar) {
case '\u002E': // "." full stop
case '\u3002': // ideographic full stop
case '\uFF0E': // fullwidth full stop
case '\uFF61': // halfwidth ideographic full stop
return ascii + "."; // restore the missing stop
default:
return ascii;
}
} catch (final IllegalArgumentException e) { // input is not valid
return input;
}
}

private static class IDNBUGHOLDER {
private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot();

private static boolean keepsTrailingDot() {
final String input = "a."; // must be a valid name
return input.equals(IDN.toASCII(input));
}
}

}
74 changes: 74 additions & 0 deletions src/test/java/com/networknt/schema/format/IriFormatTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 the original author or authors.
*
* Licensed 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 com.networknt.schema.format;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Set;

import org.junit.jupiter.api.Test;

import com.networknt.schema.InputFormat;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaValidatorsConfig;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.ValidationMessage;

class IriFormatTest {
@Test
void uriShouldPass() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf\"",
InputFormat.JSON);
assertTrue(messages.isEmpty());
}

@Test
void queryWithBracketsShouldFail() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf?filter[test]=1\"",
InputFormat.JSON);
assertFalse(messages.isEmpty());
}

@Test
void iriShouldPass() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/produktdatenblätter.pdf\"",
InputFormat.JSON);
assertTrue(messages.isEmpty());
}

}
Loading

0 comments on commit eea61d6

Please sign in to comment.