Commit 55478ac0 authored by mokj's avatar mokj
Browse files

Added custom annotations

parent 2e2dc15f
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
lazy val commonSettings = Seq(
  organization := "com.kjetland",
  organizationName := "mbknor",
  version := "1.0.1-build4-SNAPSHOT",
  version := "1.0.1-build8-SNAPSHOT",
  scalaVersion := "2.11.8",
  publishMavenStyle := true,
  publishArtifact in Test := false,
@@ -55,7 +55,9 @@ lazy val deps = Seq(
  "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test",
  "ch.qos.logback" % "logback-classic" % "1.1.3" % "test",
  "com.github.fge" % "json-schema-validator" % "2.2.6" % "test",
  "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test"
  "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % jacksonModuleScalaVersion % "test",
  "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % "test",
  "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % "test"
)

lazy val root = (project in file("."))
+13 −0
Original line number Diff line number Diff line
package com.kjetland.jackson.jsonSchema.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, PARAMETER, TYPE })
@Retention(RUNTIME)
public @interface JsonSchemaDescription {
    String value();
}
+14 −0
Original line number Diff line number Diff line
package com.kjetland.jackson.jsonSchema.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, PARAMETER, TYPE })
@Retention(RUNTIME)
public @interface JsonSchemaFormat {
    String value();
}
+13 −0
Original line number Diff line number Diff line
package com.kjetland.jackson.jsonSchema.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, PARAMETER, TYPE })
@Retention(RUNTIME)
public @interface JsonSchemaTitle {
    String value();
}
+89 −6
Original line number Diff line number Diff line
package com.kjetland.jackson.jsonSchema

import java.lang.reflect.{Field, Method, ParameterizedType}
import java.time.{LocalDate, LocalDateTime, LocalTime, OffsetDateTime}
import java.util
import javax.validation.constraints.NotNull

@@ -12,18 +13,32 @@ import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, JacksonAnnotationIntrospector}
import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode}
import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaDescription, JsonSchemaFormat, JsonSchemaTitle}
import org.slf4j.LoggerFactory

object JsonSchemaGenerator {
  val JSON_SCHEMA_DRAFT_4_URL = "http://json-schema.org/draft-04/schema#"
}

class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = false) {
class JsonSchemaGenerator
(
  val rootObjectMapper: ObjectMapper,
  debug:Boolean = false,
  extraClazz2FormatMapping:Map[Class[_], String] = Map(),
  autoGenerateTitleForProperties:Boolean = true,
  defaultArrayFormat:Option[String] = Some("table")) {

  import scala.collection.JavaConversions._

  val log = LoggerFactory.getLogger(getClass)

  val clazz2FormatMapping = Map[Class[_], String](
    classOf[OffsetDateTime] -> "datetime",
    classOf[LocalDateTime]  -> "datetime-local",
    classOf[LocalDate]      -> "date",
    classOf[LocalTime]      -> "time"
  ) ++ extraClazz2FormatMapping

  trait MySerializerProvider {
    var provider: SerializerProvider = null

@@ -146,6 +161,10 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa

      node.put("type", "array")

      defaultArrayFormat.foreach {
        format => node.put("format", format)
      }

      val itemsNode = JsonNodeFactory.instance.objectNode()
      node.set("items", itemsNode)

@@ -302,9 +321,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa
                val childVisitor = createChild(objectNode)
                objectMapper.acceptJsonFormatVisitor(subType, childVisitor)

                // must inject the 'type'-param and value as enum with only one possible value
                val propertiesNode = objectNode.get("properties").asInstanceOf[ObjectNode]

                None
            }

@@ -312,8 +328,6 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa
            thisOneOfNode.put("$ref", definitionInfo.ref.get)
            anyOfArrayNode.add(thisOneOfNode)



        }

        null // Returning null to stop jackson from visiting this object since we have done it manually
@@ -327,6 +341,25 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa
            thisObjectNode.put("type", "object")
            thisObjectNode.put("additionalProperties", false)

            // If class is annotated with JsonSchemaFormat, we should add it
            val ac = AnnotatedClass.construct(_type, objectMapper.getDeserializationConfig())
            Option(ac.getAnnotations.get(classOf[JsonSchemaFormat])).map(_.value()).foreach {
              format =>
                thisObjectNode.put("format", format)
            }

            // If class is annotated with JsonSchemaDescription, we should add it
            Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])).map(_.value()).foreach {
              description =>
                thisObjectNode.put("description", description)
            }

            // If class is annotated with JsonSchemaTitle, we should add it
            Option(ac.getAnnotations.get(classOf[JsonSchemaTitle])).map(_.value()).foreach {
              title =>
                thisObjectNode.put("title", title)
            }

            val propertiesNode = JsonNodeFactory.instance.objectNode()
            thisObjectNode.set("properties", propertiesNode)

@@ -406,6 +439,30 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa
                  getRequiredArrayNode(thisObjectNode).add(propertyName)
                }

                resolvePropertyFormat(prop).foreach {
                  format =>
                    thisPropertyNode.put("format", format)
                }

                // Optionally add description
                Option(prop.getAnnotation(classOf[JsonSchemaDescription])).map {
                  jsonSchemaDescription =>
                    thisPropertyNode.put("description", jsonSchemaDescription.value())
                }

                // Optionally add title
                Option(prop.getAnnotation(classOf[JsonSchemaTitle])).map(_.value())
                  .orElse {
                    if (autoGenerateTitleForProperties) {
                      // We should generate 'pretty-name' based on propertyName
                      Some(generateTitleFromPropertyName(propertyName))
                    } else None
                  }
                  .map {
                  title =>
                    thisPropertyNode.put("title", title)
                }

              }

              override def optionalProperty(name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType): Unit = {
@@ -441,6 +498,32 @@ class JsonSchemaGenerator(val rootObjectMapper: ObjectMapper, debug:Boolean = fa

  }

  def generateTitleFromPropertyName(propertyName:String):String = {
    // Code found here: http://stackoverflow.com/questions/2559759/how-do-i-convert-camelcase-into-human-readable-names-in-java
    val s = propertyName.replaceAll(
      String.format("%s|%s|%s",
        "(?<=[A-Z])(?=[A-Z][a-z])",
        "(?<=[^A-Z])(?=[A-Z])",
        "(?<=[A-Za-z])(?=[^A-Za-z])"
      ),
      " "
    )

    // Make the first letter uppercase
    s.substring(0,1).toUpperCase() + s.substring(1)
  }

  def resolvePropertyFormat(prop: BeanProperty):Option[String] = {
    // Prefer format specified in annotation
    Option(prop.getAnnotation(classOf[JsonSchemaFormat])).map {
      jsonSchemaFormat =>
        jsonSchemaFormat.value()
    }.orElse {
      // Try to resolve format from type
      clazz2FormatMapping.get( prop.getType.getRawClass )
    }
  }

  def resolveType(prop: BeanProperty, objectMapper: ObjectMapper):JavaType = {
    val containedType = prop.getType.containedType(0)

Loading