mercateo / rest-schemagen Goto Github PK
View Code? Open in Web Editor NEWJersey add-on for dynamic link and schema building
License: Apache License 2.0
Jersey add-on for dynamic link and schema building
License: Apache License 2.0
The current version uses Strings by default. In case of boolean and number values the real type should be used.
The default values are inconsistent with the allowed values:
{
"type": "boolean",
"default": false,
"enum": ["true"]
}
This should enable wrapper types like ObjectWithSchema and the like to be usable as response types for springs RestTemplate
Consider a situation
class Container<T> {
public final ObjectWithSchema<T> root;
}
Now, when trying to construct a schema for ObjectWithSchema<Container<ObjectWithSchema<Foo>>>
the algorithm will recursively drill into the generic, but fail to see the difference between ObjectWithSchema<Foo>
and ObjectWithSchema<Container<...>>
, resulting in a pummelled schema
{
"properties": {
"root": {
"$ref":"#"
}
}
}
Possible solution: always use the whole type with all type arguments when caching a schema for recursive use.
In order to set the allowed values independent of the current value used in the returned link, the Parameter
class should have a field currentValue
which is returned in the get
method.
For example, if the field currentValue
is added and the line
return allowedValues.isEmpty() ? defaultValue : allowedValues.get(0);
is replaced by
if (currentValue != null) {
return currentValue
} else {
return allowedValues.isEmpty() ? defaultValue : allowedValues.get(0);
}
then everything should be working as before, with the option to provide a currentValue
different from any of the allowedValues
.
Currently, the schema generator only supports public fields when determining the object schema. This behaviour should be expanded to public getter methods.
In LinkCreator.createFor
(line 133) the schema is only generated for the last resource on the path, information from previous resources is ignored.
Using PUT for creating a resource with client side id's leads to problems when creating the link to the resource. Probably the BeanParam should not be treated as payload in com.mercateo.common.rest.schemagen.RestJsonSchemaGenerator#createInputSchema
.
@PUT
@Path("{app-id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ObjectWithSchema<Void> createResource(
@NotNull @Valid @BeanParam ResourceIdParameterBean idParameterBean,
@NonNull @Valid ResourceJson resourceJson) { ... }
15:37:59 ERROR com.mercateo.rest.jersey.utils.exception.RFCExceptionMapper - Sending error to client
java.lang.IllegalStateException: multiple properties named <> found
at com.mercateo.common.rest.schemagen.RestJsonSchemaGenerator.createInputSchema(RestJsonSchemaGenerator.java:132)
at com.mercateo.common.rest.schemagen.link.LinkCreator.addSchemaIfNeeded(LinkCreator.java:216)
at com.mercateo.common.rest.schemagen.link.LinkCreator.createFor(LinkCreator.java:120)
at com.mercateo.common.rest.schemagen.link.LinkCreator.createFor(LinkCreator.java:79)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:143)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:124)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:64)
...
JSON Hyperschema doesn't allow for keys to be dynamic, so when we have something like
public Map<Foo, Bar> someMap;
with example JSON
{
'key1':'val1',
'key2':'val2',
}
we cannot do
{
'type':'map', /* this doesn't exist )-: */
'keys': {
'type':'string',
'enum': [ 'key1', 'key2' ],
}
'values': {
'type':'string'
}
}
But iff the keyset is finite, i.e., for practical reasons, an enum, like
public enum Keys = { KEY_1, KEY_2 };
public Map<Keys, String> someMap;
we can do the following:
'someMap': {
'type':'object',
'properties': {
'KEY_1': {
'type':'string' },
'KEY_2': {
'type':'string' },
},
}
Therefore, the SchemaGenerator needs to, when encountering a Map
, check whether the keyClazz is instanceof Enum and if so, unwind the whole thing. Otherwise it should emit object
and not touch it any further.
Currently the ENUMs are generated for all fields except for fileds of type UUID.
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
class ExampleJson {
@NotNull
private final UUID id;
@NotNull
private final UUID appId;
@Nullable
@Size(min = 1, max = 35)
private final String contactName;
@NotNull
@Size(min = 1, max = 35)
private final String city;
@NotNull
private final CountryCode countryCode;
@NotNull
@Size(min = 1, max = 35)
private final String street;
@NotNull
@Size(min = 1, max = 10)
private final String zipcode;
}
Optional<Link> getUpdateLink(ExampleData exampleData) {
ExampleJson exampleJson = new ExampleJson(exampleData.getId(), exampleData
.getAppId(), "contactName", "city", exampleData.getCountryCode(), "street", "zipcode");
final CallContext context = Parameter.createContext();
final Parameter.Builder<ExampleJson > exampleJsonBuilder = context.builderFor(
ExampleJson.class);
final Parameter<ExampleJson> updateJson = exampleJsonBuilder
.allowValues(exampleJson)
.defaultValue(exampleJson).build();
return linkFactory.forCall(Rel.UPDATE, r -> r.update(
new IdParameterBean(exampleData.getId()
.toString()), updateJson), context);
}
Result:
{
"href": "http://localhost:8080/api/addresses/48278ad2-9ba0-42c1-9ba4-c33ba8684666",
"schema": {
"type": "object",
"properties": {
"appId": {
"type": "string",
"format": "uuid",
"pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
},
"city": {
"type": "string",
"default": "city",
"enum": [
"city"
],
"minLength": 1,
"maxLength": 35
},
"contactName": {
"type": "string",
"default": "",
"enum": [
""
],
"minLength": 1,
"maxLength": 35
},
"countryCode": {
"type": "string",
"default": "DE",
"enum": [
"DE"
]
},
"id": {
"type": "string",
"format": "uuid",
"pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
},
"street": {
"type": "string",
"default": "street",
"enum": [
"street"
],
"minLength": 1,
"maxLength": 35
},
"zipcode": {
"type": "string",
"default": "zipcode",
"enum": [
"zipcode"
],
"minLength": 1,
"maxLength": 10
}
},
"required": [
"appId",
"city",
"companyName",
"countryCode",
"id",
"street",
"zipcode"
]
},
"rel": "update",
"mediaType": "application/json",
"method": "PUT"
}
When creating a Link for a Response, query parameters are not encoded correctly.
Easiest way to reproduce: try to pass some Json {"key": "value"}
as a query parameter value.
java.lang.IllegalArgumentException: Illegal character """ at position 5 is not allowed as a start of a name in a path template "fq={ "foo": {"bar": "zort","narf": "poit" }}".
at org.glassfish.jersey.uri.internal.UriTemplateParser.parseName(UriTemplateParser.java:330)
at org.glassfish.jersey.uri.internal.UriTemplateParser.parse(UriTemplateParser.java:255)
at org.glassfish.jersey.uri.internal.UriTemplateParser.<init>(UriTemplateParser.java:114)
at org.glassfish.jersey.uri.UriTemplate.createUriComponent(UriTemplate.java:1004)
at org.glassfish.jersey.uri.UriTemplate.createURIWithStringValues(UriTemplate.java:970)
at org.glassfish.jersey.uri.UriTemplate.createURIWithStringValues(UriTemplate.java:827)
at org.glassfish.jersey.uri.UriTemplate.createURI(UriTemplate.java:790)
at org.glassfish.jersey.uri.internal.JerseyUriBuilder._buildFromMap(JerseyUriBuilder.java:822)
at org.glassfish.jersey.uri.internal.JerseyUriBuilder.buildFromMap(JerseyUriBuilder.java:801)
at com.mercateo.common.rest.schemagen.link.LinkCreator.mergeUri(LinkCreator.java:125)
at com.mercateo.common.rest.schemagen.link.LinkCreator.createFor(LinkCreator.java:110)
at com.mercateo.common.rest.schemagen.link.LinkCreator.createFor(LinkCreator.java:79)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:143)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:124)
at com.mercateo.common.rest.schemagen.link.LinkFactory.forCall(LinkFactory.java:64)
at com.mercateo.content.articleservice.rest.resource.article.ArticlesResource.lambda$createPaginatedResponse$2(ArticlesResource.java:166)
at com.mercateo.common.rest.schemagen.PaginationLinkBuilder.createSelfLink(PaginationLinkBuilder.java:50)
at com.mercateo.common.rest.schemagen.PaginationLinkBuilder.generateLinks(PaginationLinkBuilder.java:40)
at com.mercateo.common.rest.schemagen.types.PaginatedResponseBuilder.build(PaginatedResponseBuilder.java:44)
For the JSON-type boolean, always a default value is generated in the schema, see
com.mercateo.common.rest.schemagen.json.mapper.BooleanJsonPropertyMapper
This is not the case for Integer
com.mercateo.common.rest.schemagen.json.mapper.IntegerJsonPropertyMapper
Unfortunately this not always correct. In the following example there should be no default value:
/users
gives the list of all users
/users?admin=true
gives the list of all admin users
/users?admin=false
gives the list of all non-admin users
In Java Code the admin flag would be of type Boolean and its value would be null, if the query parameter is not given.
One solution would be, to remove the default value of all primitives and use only default values, if given by annotations.
Another solution would be to differentiate between the Java types boolean and Boolean (and similarly for other primitives) and use the java default values only for boolean, int, etc. If this solution is chosen, one should also think about if this should only happen in the schema or if it should also happen in the target_schema. Maybe one should leave it out for the target_schema as the Java primitives will always have a value when being returned and no default value would be needed.
Interesting places could be:
com.mercateo.common.rest.schemagen.SchemaPropertyGenerator#builtins
com.mercateo.common.rest.schemagen.PropertyTypeMapper#TYPE_MAP
com.mercateo.common.rest.schemagen.json.mapper.PropertyJsonSchemaMapperForRoot#primitivePropertyMappers
With the introduction of com.mercateo:reflection
with 0.18.5
one can easily bring a service down with OOMs in Metaspace (or Compressed Class Space, Code Cache) when these pools are configured with an upper bound (e.g. with -XX:MaxMetaspaceSize
) or unbound until the host system memory is depleted. (Metaspace is unbound since Java8 and leaks like this might go unnoticed without proper monitoring.)
Reproduce:
Construct LinkFactory
with LinkMetaFactory.createFactoryFor(MyResource.class)
in the method body of your resource endpoint. For every call a new type/class is created with the following naming pattern: sun.reflect.GeneratedSerializationConstructorAccessorxxxx
where xxxx is a increasing number. (A VisualVM Heapdump is sufficient to get a picture of the described behaviour.)
When a resource is called a dozen or hundred times a second the GC can't hold up scanning and unloading all the temporary created types from Metaspace (holding the type defintions) and will lead to Metaspace-OOM and service starvation.
Remediation/Workaround:
Keep a reference to the created proxy LinkFactory.
Multiple Query parameters like in
@GET
public void multipleQueryParameters(@QueryParam("test") Iterable<String> parameters) {
...
}
are not handled correctly, as their string representation is added as a single parameter:
basePath/resource?test=%5Bfoo%2C+bar%2C+baz%5D
One would expect to get
basePath/resource?test=foo&tes=bar&test=baz
instead.
Templated links are URI-Escaped at the moment, while in http://json-schema.org/latest/json-schema-hypermedia.html the "{" and "}" are not escaped.
In fact, an URI-Template is not an URI :-) , so the escaping makes no sense.
If a class A has multiple fields of class B and the fields are annotated with @JsonUnwrapped
(with different "prefix"-attribute), then the above method throws an IllegalStateException with the message "recursion detected while unwrapping field ...".
But, this is no recursion.
Example:
@Value
public class A {
@JsonUnwrapped(prefix="b1.")
private final B b1;
@JsonUnwrapped(prefix="b2.")
private final B b2;
}
The method detects class B for field b1 first and adds it to the unwrappedTypes
HashSet without problems. Field b2 having the same class will result in this exception.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.