diff --git a/CHANGELOG.md b/CHANGELOG.md index c038f6e..95d1e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # MsgCodec Changelog +## 3.3.0 (?) + +### msgcodec-json-jaxrs +New module for simplified usage of JSON Codec in a REST API. + +### msgcodec-json-swagger +New module to generate Swagger documentation in a REST API. + +## 3.2.0 + +TODO: Missing + ## 3.1.0 ### msgcodec diff --git a/build.gradle b/build.gradle index 0f6d241..1c620b3 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,13 @@ subprojects { apply plugin: 'signing' apply plugin: 'eclipse' sourceCompatibility = '1.8' - ext.junit_version = '4.11' + ext { + junit_version = '4.11' + jaxrs_version = '2.0.1' + javax_annotation_version = '1.2' + swagger_version = '1.5.8' + } + compileTestJava.options.encoding = 'UTF-8' task javadocJar(type: Jar) { diff --git a/msgcodec-json-jaxrs/build.gradle b/msgcodec-json-jaxrs/build.gradle new file mode 100644 index 0000000..7250f0f --- /dev/null +++ b/msgcodec-json-jaxrs/build.gradle @@ -0,0 +1,10 @@ +description = 'JSON JAX-RS support' + +dependencies { + compile project(':msgcodec') + compile project(':msgcodec-json') + compile group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: jaxrs_version + compile group: 'javax.annotation', name: 'javax.annotation-api', version: javax_annotation_version + + testCompile group: 'junit', name: 'junit', version:junit_version +} diff --git a/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyReader.java b/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyReader.java new file mode 100644 index 0000000..63f6b22 --- /dev/null +++ b/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyReader.java @@ -0,0 +1,50 @@ +package com.cinnober.msgcodec.json.jaxrs; + +import com.cinnober.msgcodec.Schema; +import com.cinnober.msgcodec.json.JsonCodec; +import com.cinnober.msgcodec.json.JsonCodecFactory; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import javax.annotation.Priority; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.MessageBodyReader; + +/** + * MessageBodyReader implementation for parsing MsgCodec messages in JSON format. + * + * @author mikael.brannstrom@tradedoubler.com + */ +@Consumes(MediaType.APPLICATION_JSON) +@Priority(100) // Must be higher than JacksonMessageBodyProvider that is registered by default in e.g. DropWizard +public class JsonCodecMessageBodyReader implements MessageBodyReader { + + private final Schema schema; + private final JsonCodec codec; + + public JsonCodecMessageBodyReader(Schema schema) { + this.schema = schema; + codec = new JsonCodecFactory(schema).createCodec(); + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return mediaType.equals(MediaType.APPLICATION_JSON_TYPE) && schema.getGroup(type) != null; + } + + @Override + public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + try { + return codec.decodeStatic(type, entityStream); + } catch(Exception e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build()); + } + } + +} diff --git a/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyWriter.java b/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyWriter.java new file mode 100644 index 0000000..5a173e2 --- /dev/null +++ b/msgcodec-json-jaxrs/src/main/java/com/cinnober/msgcodec/json/jaxrs/JsonCodecMessageBodyWriter.java @@ -0,0 +1,49 @@ +package com.cinnober.msgcodec.json.jaxrs; + +import com.cinnober.msgcodec.Schema; +import com.cinnober.msgcodec.json.JsonCodec; +import com.cinnober.msgcodec.json.JsonCodecFactory; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import javax.annotation.Priority; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; + +/** + * MessageBodyWriter implementation for serializing MsgCodec messages in JSON format. + * + * @author mikael.brannstrom@tradedoubler.com + */ +@Produces(MediaType.APPLICATION_JSON) +@Priority(100) // Must be higher than JacksonMessageBodyProvider that is registered by default in e.g. DropWizard +public class JsonCodecMessageBodyWriter implements MessageBodyWriter { + + private final Schema schema; + private final JsonCodec codec; + + public JsonCodecMessageBodyWriter(Schema schema) { + this.schema = schema; + codec = new JsonCodecFactory(schema).createCodec(); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return mediaType.equals(MediaType.APPLICATION_JSON_TYPE) && schema.getGroup(type) != null; + } + + @Override + public long getSize(Object t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(Object t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + codec.encodeStatic(t, entityStream); + } + +} diff --git a/msgcodec-json-jaxrs/src/main/resources/.gitignore b/msgcodec-json-jaxrs/src/main/resources/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/msgcodec-json-jaxrs/src/test/java/.gitignore b/msgcodec-json-jaxrs/src/test/java/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/msgcodec-json-jaxrs/src/test/resources/.gitignore b/msgcodec-json-jaxrs/src/test/resources/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/msgcodec-json-swagger/build.gradle b/msgcodec-json-swagger/build.gradle new file mode 100644 index 0000000..9977851 --- /dev/null +++ b/msgcodec-json-swagger/build.gradle @@ -0,0 +1,11 @@ +description = 'JSON Swagger documentation support' + +dependencies { + compile project(':msgcodec') + compile project(':msgcodec-json') + compile group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: jaxrs_version +// compile group: 'javax.annotation', name: 'javax.annotation-api', version: javax_annotation_version + compile group: 'io.swagger', name: 'swagger-jersey2-jaxrs', version: swagger_version + + testCompile group: 'junit', name: 'junit', version:junit_version +} diff --git a/msgcodec-json-swagger/src/main/java/com/cinnober/msgcodec/json/swagger/JsonCodecSwaggerModelConverter.java b/msgcodec-json-swagger/src/main/java/com/cinnober/msgcodec/json/swagger/JsonCodecSwaggerModelConverter.java new file mode 100644 index 0000000..82edb02 --- /dev/null +++ b/msgcodec-json-swagger/src/main/java/com/cinnober/msgcodec/json/swagger/JsonCodecSwaggerModelConverter.java @@ -0,0 +1,229 @@ +package com.cinnober.msgcodec.json.swagger; + +import com.cinnober.msgcodec.FieldDef; +import com.cinnober.msgcodec.GroupDef; +import com.cinnober.msgcodec.Schema; +import com.cinnober.msgcodec.TypeDef; +import com.cinnober.msgcodec.util.TimeFormat; +import com.fasterxml.jackson.databind.type.SimpleType; +import io.swagger.converter.ModelConverter; +import io.swagger.converter.ModelConverterContext; +import io.swagger.models.ComposedModel; +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.RefModel; +import io.swagger.models.properties.AbstractProperty; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BaseIntegerProperty; +import io.swagger.models.properties.BinaryProperty; +import io.swagger.models.properties.BooleanProperty; +import io.swagger.models.properties.DateProperty; +import io.swagger.models.properties.DateTimeProperty; +import io.swagger.models.properties.DecimalProperty; +import io.swagger.models.properties.DoubleProperty; +import io.swagger.models.properties.FloatProperty; +import io.swagger.models.properties.IntegerProperty; +import io.swagger.models.properties.LongProperty; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import io.swagger.models.properties.StringProperty; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * The class converts a JSON msgcodec schema into a swagger model. + * + *

Usage: + *

+ * ModelConverters.getInstance().addConverter(new JsonCodecSwaggerModelConverter(schema));
+ * 
+ * + * @author mikael.brannstrom@tradedoubler.com + */ +public class JsonCodecSwaggerModelConverter implements ModelConverter { + + private static final String DOC_ANOT = "doc"; + private static final String JSTYPE_FIELD = "$type"; + + private final Schema schema; + + public JsonCodecSwaggerModelConverter(Schema schema) { + this.schema = Objects.requireNonNull(schema); + } + + private Type unbox(Type type) { + if (type instanceof SimpleType) { + SimpleType stype = (SimpleType) type; + return stype.getRawClass(); + } + return type; + } + + + @Override + public Property resolveProperty(Type type, ModelConverterContext context, Annotation[] annotations, Iterator chain) { + if (chain.hasNext()) { + return chain.next().resolveProperty(type, context, annotations, chain); + } + return null; + } + + @Override + public Model resolve(Type type, ModelConverterContext context, Iterator chain) { + Type javaType = unbox(type); + GroupDef group = schema.getGroup(javaType); + if (group != null) { + return createStaticModel(group, context); + } + if (chain.hasNext()) { + return chain.next().resolve(type, context, chain); + } + return null; + } + + private ModelImpl createStaticModel(GroupDef group, ModelConverterContext context) { + ModelImpl model = new ModelImpl() + .name("static_"+group.getName()) + .type("object") + .description(group.getAnnotation(DOC_ANOT)); + addProperties(model, group, true, context); + return model; + } + + private void addProperties(ModelImpl model, GroupDef group, boolean parents, ModelConverterContext context) { + if (parents) { + String superGroupName = group.getSuperGroup(); + if (superGroupName != null) { + GroupDef superGroup = schema.getGroup(superGroupName); + addProperties(model, superGroup, parents, context); + // PENDING: refer to the super group properties using "allOf", instead of copying them + } + } + for (FieldDef field : group.getFields()) { + Property prop = createProperty(field.getType(), context); + prop.setName(field.getName()); + prop.setRequired(field.isRequired()); + prop.setDescription(field.getAnnotation(DOC_ANOT)); + model.addProperty(field.getName(), prop); + } + } + + private String registerDynamicModel(GroupDef group, ModelConverterContext context) { + if (group != null) { + + ComposedModel model = new ComposedModel(); + model.setDescription(group.getAnnotation(DOC_ANOT)); + String supergroup = group.getSuperGroup(); + model.child(new RefModel(supergroup != null ? supergroup : "any")); + + registerDynamicModel(supergroup != null ? schema.getGroup(supergroup) : null, context); + + ModelImpl selfModel = new ModelImpl().type("object"); + addProperties(selfModel, group, false, context); + model.child(selfModel); + + context.defineModel(group.getName(), model); + return group.getName(); + } else { + ModelImpl model = new ModelImpl().type("object").name("any").discriminator(JSTYPE_FIELD); + Property prop = new StringProperty().required(true); + prop.setName(JSTYPE_FIELD); + model.addProperty(JSTYPE_FIELD, prop); + + context.defineModel("any", model); + return "any"; + } + } + + private Property createProperty(TypeDef type, ModelConverterContext context) { + type = schema.resolveToType(type, true); + GroupDef group = schema.resolveToGroup(type); + switch (type.getType()) { + case BIGDECIMAL: + return new DecimalProperty("bigdecimal"); + case DECIMAL: + return new DecimalProperty("decimal"); + case BIGINT: + return new BaseIntegerProperty("biginteger"); + case INT8: + return new BaseIntegerProperty("int8");//.minimum((double) (-1 << 7)).maximum((double) 0x7f); + case UINT8: + return new BaseIntegerProperty("uint8");//.minimum(0.0).maximum((double) 0xff); + case INT16: + return new BaseIntegerProperty("int16");//.minimum((double) (-1 << 15)).maximum((double) 0x7fff); + case UINT16: + return new BaseIntegerProperty("uint16");//.minimum(0.0).maximum((double) 0xffff); + case INT32: + return new IntegerProperty(); + case UINT32: + return new BaseIntegerProperty("uint32");//.minimum(0.0).maximum((double) 0xffffffffL); + case INT64: + return new LongProperty(); + case UINT64: + return new BaseIntegerProperty("uint64");//.minimum(0.0); + case FLOAT32: + return new FloatProperty(); + case FLOAT64: + return new DoubleProperty(); + case BOOLEAN: + return new BooleanProperty(); + case BINARY: + return new BinaryProperty(); + case STRING: + return new StringProperty(); + case TIME: { + TypeDef.Time timeType = (TypeDef.Time) type; + AbstractProperty prop; + if (timeType.getUnit() == TimeUnit.DAYS) { + prop = new DateProperty(); + } else { + prop = new DateTimeProperty(); + } + long exampleValue = System.currentTimeMillis(); + if (timeType.getUnit().compareTo(TimeUnit.MILLISECONDS) < 0) { + exampleValue *= timeType.getUnit().toMillis(1); + } else { + exampleValue /= timeType.getUnit().toMillis(1); + } + String example = TimeFormat.getTimeFormat(timeType.getUnit(), timeType.getEpoch()) + .format(exampleValue); + prop.setExample(example); + if (timeType.getTimeZone() != null) { + prop.setVendorExtension("x-timezone", timeType.getTimeZone().getID()); + } + return prop; + } + case ENUM: { + TypeDef.Enum enumType = (TypeDef.Enum) type; + return new StringProperty("enum")._enum( + enumType.getSymbols().stream().map(TypeDef.Symbol::getName).collect(Collectors.toList())); + } + case SEQUENCE: { + TypeDef.Sequence sequenceType = (TypeDef.Sequence) type; + return new ArrayProperty(createProperty(sequenceType.getComponentType(), context)); + } + case REFERENCE: { + //TypeDef.Reference refType = (TypeDef.Reference) type; + ModelImpl model = createStaticModel(group, context); + context.defineModel(model.getName(), model); + return new RefProperty(model.getName()); + } + case DYNAMIC_REFERENCE: { + TypeDef.DynamicReference refType = (TypeDef.DynamicReference) type; + String name = registerDynamicModel(group, context); + for (GroupDef instanceGroup: schema.getDynamicGroups(refType.getRefType())) { + registerDynamicModel(instanceGroup, context); + } + RefProperty prop = new RefProperty(name); + return prop; + } + default: + throw new RuntimeException("Unhandled type: " + type.getType()); + } + } + +} diff --git a/msgcodec-json-swagger/src/main/resources/.gitignore b/msgcodec-json-swagger/src/main/resources/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/msgcodec-json-swagger/src/test/java/.gitignore b/msgcodec-json-swagger/src/test/java/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/msgcodec-json-swagger/src/test/resources/.gitignore b/msgcodec-json-swagger/src/test/resources/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/readme.md b/readme.md index b79959b..4eb00fe 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,8 @@ The code is divided into the following projects: - `msgcodec`: contains annotations etc required for defining messages and protocols - `msgcodec-json`: JSON codec +- `msgcodec-json-jaxrs`: JSON codec JAX-RS support +- `msgcodec-json-swagger`: JSON codec Swagger support - `msgcodec-xml`: XML codec - `msgcodec-blink`: Blink (compact format) codec - `msgcodec-javadoc`: Javadoc doclet for extracting javadoc comments from messages. diff --git a/settings.gradle b/settings.gradle index 1e04d15..9e972f8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,8 @@ include 'msgcodec' include 'msgcodec-blink' include 'msgcodec-json' +include 'msgcodec-json-jaxrs' +include 'msgcodec-json-swagger' include 'msgcodec-xml' include 'msgcodec-javadoc' include 'msgcodec-test'