diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyGenerationConfig.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyGenerationConfig.java index 94ef07f6..8e0e3c33 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyGenerationConfig.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyGenerationConfig.java @@ -2,6 +2,8 @@ import org.jsonschema2pojo.DefaultGenerationConfig; +import java.nio.charset.Charset; + public class PolyGenerationConfig extends DefaultGenerationConfig { @Override @@ -28,4 +30,9 @@ public boolean isUseLongIntegers() { public boolean isIncludeAdditionalProperties() { return false; } + + @Override + public String getOutputEncoding() { + return Charset.defaultCharset().name(); + } } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java index 3537cff1..3766c5a2 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java @@ -12,6 +12,8 @@ import org.jsonschema2pojo.SchemaMapper; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Optional; diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyFragmentResolver.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyFragmentResolver.java new file mode 100644 index 00000000..b2989c78 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyFragmentResolver.java @@ -0,0 +1,46 @@ +package io.polyapi.plugin.service.schema; + +import com.fasterxml.jackson.databind.JsonNode; +import org.jsonschema2pojo.FragmentResolver; +import org.jsonschema2pojo.JsonPointerUtils; + +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static org.apache.commons.lang3.StringUtils.split; + +/** + * Class that copies parent {@link FragmentResolver} but that adds a {@link URLDecoder#decode(String, Charset)} to the part to evaluate it. + */ +public class PolyFragmentResolver extends FragmentResolver { + public JsonNode resolve(JsonNode tree, String path, String refFragmentPathDelimiters) { + return resolve(tree, new ArrayList<>(asList(split(path, refFragmentPathDelimiters)))); + } + + private JsonNode resolve(JsonNode tree, List path) { + if (path.isEmpty()) { + return tree; + } else { + String part = path.remove(0); + if (tree.isArray()) { + try { + int index = Integer.parseInt(part); + return resolve(tree.get(index), path); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Not a valid array index: " + part); + } + } + String decodedPart = JsonPointerUtils.decodeReferenceToken(URLDecoder.decode(part.replace("+","%2B"), Charset.defaultCharset())); + if (tree.has(decodedPart)) { + return resolve(tree.get(decodedPart), path); + } else { + throw new IllegalArgumentException("Path not present: " + decodedPart); + } + } + + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyRuleFactory.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyRuleFactory.java index de9a52ec..c70d1e1e 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyRuleFactory.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolyRuleFactory.java @@ -15,7 +15,7 @@ public class PolyRuleFactory extends RuleFactory { private NameHelper overwrittingNameHelper; public PolyRuleFactory(GenerationConfig config) { - super(config, new Jackson2Annotator(config), new SchemaStore()); + super(config, new Jackson2Annotator(config), new PolySchemaStore()); this.overwrittingNameHelper = new JsonSchemaNameHelper(config); } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolySchemaStore.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolySchemaStore.java new file mode 100644 index 00000000..63bfb125 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/PolySchemaStore.java @@ -0,0 +1,106 @@ +package io.polyapi.plugin.service.schema; + +import com.fasterxml.jackson.databind.JsonNode; +import org.jsonschema2pojo.FragmentResolver; +import org.jsonschema2pojo.Schema; +import org.jsonschema2pojo.SchemaStore; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.apache.commons.lang3.StringUtils.*; + +/** + * This class is a copy of the parent {@link SchemaStore} with the difference that uses a {@link PolyFragmentResolver} instead of a {@link FragmentResolver}. + */ +public class PolySchemaStore extends SchemaStore { + protected final FragmentResolver fragmentResolver = new PolyFragmentResolver(); + + /** + * Create or look up a new schema which has the given ID and read the + * contents of the given ID as a URL. If a schema with the given ID is + * already known, then a reference to the original schema will be returned. + * + * @param id the id of the schema being created + * @param refFragmentPathDelimiters A string containing any characters + * that should act as path delimiters when resolving $ref fragments. + * @return a schema object containing the contents of the given path + */ + public synchronized Schema create(URI id, String refFragmentPathDelimiters) { + + URI normalizedId = id.normalize(); + + if (!schemas.containsKey(normalizedId)) { + + URI baseId = removeFragment(id).normalize(); + if (!schemas.containsKey(baseId)) { + logger.debug("Reading schema: " + baseId); + final JsonNode baseContent = contentResolver.resolve(baseId); + schemas.put(baseId, new Schema(baseId, baseContent, null)); + } + + final Schema baseSchema = schemas.get(baseId); + if (normalizedId.toString().contains("#")) { + JsonNode childContent = fragmentResolver.resolve(baseSchema.getContent(), '#' + id.getFragment(), refFragmentPathDelimiters); + schemas.put(normalizedId, new Schema(normalizedId, childContent, baseSchema)); + } + } + + return schemas.get(normalizedId); + } + + /** + * Create or look up a new schema using the given schema as a parent and the + * path as a relative reference. If a schema with the given parent and + * relative path is already known, then a reference to the original schema + * will be returned. + * + * @param parent the schema which is the parent of the schema to be created. + * @param path the relative path of this schema (will be used to create a + * complete URI by resolving this path against the parent + * schema's id) + * @param refFragmentPathDelimiters A string containing any characters + * that should act as path delimiters when resolving $ref fragments. + * @return a schema object containing the contents of the given path + */ + @SuppressWarnings("PMD.UselessParentheses") + public Schema create(Schema parent, String path, String refFragmentPathDelimiters) { + if (!path.equals("#")) { + // if path is an empty string then resolving it below results in jumping up a level. e.g. "/path/to/file.json" becomes "/path/to" + path = stripEnd(path, "#?&/"); + } + + // encode the fragment for any funny characters + if (path.contains("#")) { + String pathExcludingFragment = substringBefore(path, "#"); + String fragment = substringAfter(path, "#"); + URI fragmentURI; + try { + fragmentURI = new URI(null, null, fragment); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid fragment: " + fragment + " in path: " + path); + } + path = pathExcludingFragment + "#" + fragmentURI.getRawFragment(); + } + URI id = (parent == null || parent.getId() == null) ? URI.create(path) : parent.getId().resolve(path); + String stringId = id.toString(); + if (stringId.endsWith("#")) { + try { + id = new URI(stripEnd(stringId, "#")); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Bad path: " + stringId); + } + } + if (selfReferenceWithoutParentFile(parent, path) || substringBefore(stringId, "#").isEmpty()) { + JsonNode parentContent = parent.getGrandParent().getContent(); + if (schemas.containsKey(id)) { + return schemas.get(id); + } else { + Schema schema = new Schema(id, fragmentResolver.resolve(parentContent, path, refFragmentPathDelimiters), parent.getGrandParent()); + schemas.put(id, schema); + return schema; + } + } + return create(id, refFragmentPathDelimiters); + } +} diff --git a/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java b/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java index df636ad5..c0a73ffa 100644 --- a/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java +++ b/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java @@ -39,7 +39,8 @@ public static Stream generateSource() { createArguments(12, "Schema with enum with '-' in one of the options.", "Identifier", "TestResponse"), createArguments(13, "Schema with different types that have the same enum.", "Identifier", DEFAULT_RESPONSE_NAME, "Data"), createArguments(14, "Schema that is an Integer."), - createArguments(15, "Schema with multiple enums with the same name and properties.", DEFAULT_RESPONSE_NAME, "DashMinusstyle", "DashMinusstyle_", "Other")); + createArguments(15, "Schema with multiple enums with the same name and properties.", DEFAULT_RESPONSE_NAME, "DashMinusstyle", "DashMinusstyle_", "Other"), + createArguments(16, "Schema with json property yhat has a space in it.", "Data", DEFAULT_RESPONSE_NAME)); } public static Stream getTypeSource() { diff --git a/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 16.schema.json b/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 16.schema.json new file mode 100644 index 00000000..46805174 --- /dev/null +++ b/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 16.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "Data": { + "type": "object", + "additionalProperties": false, + "properties": { + "hell o": { + "type": "string" + } + }, + "required": [ + "hell o" + ], + "title": "Data" + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/definitions/Data" + } + }, + "required": [ + "data" + ], + "title": "ResponseType" +} \ No newline at end of file