Mercurial > hg > thermostat-ng
view common/type-system/src/main/java/com/redhat/thermostat/lang/schema/internal/SchemaBuilderImpl.java @ 18:5100a9b678f4
Initial Schema related services
This patch adds initial support for schema validation and exposition services.
I am down as a reviewer since the initial code is from neugens, modified by me before this first commit.
Reviewed-by: neugens, stooke
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-August/024759.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024823.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024918.html
author | Simon Tooke <stooke@redhat.com> |
---|---|
date | Fri, 08 Sep 2017 13:24:37 -0400 |
parents | |
children |
line wrap: on
line source
/* * Copyright 2012-2017 Red Hat, Inc. * * This file is part of Thermostat. * * Thermostat is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2, or (at your * option) any later version. * * Thermostat is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Thermostat; see the file COPYING. If not see * <http://www.gnu.org/licenses/>. * * Linking this code with other modules is making a combined work * based on this code. Thus, the terms and conditions of the GNU * General Public License cover the whole combination. * * As a special exception, the copyright holders of this code give * you permission to link this code with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also * meet, for each linked independent module, the terms and conditions * of the license of that module. An independent module is a module * which is not derived from or based on this code. If you modify * this code, you may extend this exception to your version of the * library, but you are not obligated to do so. If you do not wish * to do so, delete this exception statement from your version. */ package com.redhat.thermostat.lang.schema.internal; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.redhat.thermostat.lang.schema.*; import com.redhat.thermostat.lang.schema.annotations.Maximum; import com.redhat.thermostat.lang.schema.annotations.Minimum; import com.redhat.thermostat.lang.schema.annotations.Schema; import com.redhat.thermostat.lang.schema.annotations.Type; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Service; import java.io.IOException; import java.lang.reflect.Field; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; @Component @Service(SchemaBuilder.class) public class SchemaBuilderImpl implements SchemaBuilder { private class FieldProperties { String name = ""; String description = ""; String type = ""; String parentType = ""; boolean hasMin; long boundsMin; boolean exclusiveMin; boolean hasMax; long boundsMax; boolean exclusiveMax; @Override public String toString() { return "FieldProperties {" + "name='" + name + '\'' + ", description='" + description + '\'' + ", type='" + type + '\'' + '}'; } } private class SchemaDefinition { FieldProperties field = new FieldProperties(); Set<FieldProperties> properties = new TreeSet<>(new Comparator<FieldProperties>() { @Override public int compare(FieldProperties o1, FieldProperties o2) { return o1.toString().compareTo(o2.toString()); } }); Set<String> required = new TreeSet<>(); } private GsonBuilder schemaBuilder; private Map<String, String> schemas; private Map<String, SchemaDefinition> definitions; public SchemaBuilderImpl() { this(true); } public SchemaBuilderImpl(boolean prettyPrinting) { definitions = new ConcurrentHashMap<>(); schemas = new HashMap<>(); schemaBuilder = new GsonBuilder(); schemaBuilder.registerTypeAdapter(Class.class, new SchemaSerializer()); if (prettyPrinting) { schemaBuilder.setPrettyPrinting(); } } private void checkTypeAndRegister(Class target) { if (schemas.containsKey(target.getName())) { return; } if (!target.isAnnotationPresent(Type.class)) { throw new IllegalArgumentException("Unrecognised Type: must be annotated with " + Type.class); } SchemaDefinition definition = new SchemaDefinition(); definition.field.name = target.getSimpleName(); Type annotation = (Type) target.getAnnotation(Type.class); if (!annotation.name().isEmpty()) { definition.field.name = annotation.name(); } definition.field.description = annotation.description(); if (!target.getSuperclass().equals(Object.class)) { definition.field.parentType = target.getSuperclass().getSimpleName(); } // let's see the rest of the schema for (Field field : target.getDeclaredFields()) { if (field.isAnnotationPresent(Schema.class)) { Schema schema = field.getAnnotation(Schema.class); FieldProperties fieldProperties = new FieldProperties(); fieldProperties.name = field.getName(); if (!schema.name().isEmpty()) { fieldProperties.name = schema.name(); } fieldProperties.description = schema.description(); fieldProperties.type = field.getType().getSimpleName(); if (field.isAnnotationPresent(Minimum.class)) { fieldProperties.hasMin = true; Minimum range = field.getAnnotation(Minimum.class); fieldProperties.boundsMin = range.value(); fieldProperties.exclusiveMin = range.exclusive(); } if (field.isAnnotationPresent(Maximum.class)) { fieldProperties.hasMax = true; Maximum range = field.getAnnotation(Maximum.class); fieldProperties.boundsMax = range.value(); fieldProperties.exclusiveMax = range.exclusive(); } if (fieldProperties.hasMax && fieldProperties.hasMin && fieldProperties.boundsMin > fieldProperties.boundsMax) { // perhaps a better error message? throw new SchemaValidationException("maximum and minimum bounds are incompatible: " + "min: " + fieldProperties.boundsMin + ", " + "max: " + fieldProperties.boundsMax); } definition.properties.add(fieldProperties); if (schema.required()) { definition.required.add(fieldProperties.name); } } } definitions.put(target.getName(), definition); String schema = schemaBuilder.create().toJson(target); schemas.put(target.getName(), schema); } @Override public void registerType(Class target) { checkTypeAndRegister(target); } @Override public String getSchema(Class target) { checkTypeAndRegister(target); return schemas.get(target.getName()); } private class SchemaSerializer extends TypeAdapter<Object> { @Override public void write(JsonWriter out, Object target) throws IOException { SchemaDefinition definition = definitions.get(((Class) target).getName()); out.beginObject(); out.name("$schema").value("http://json-schema.org/draft-04/schema#"); out.name("title").value(definition.field.name); out.name("description").value(definition.field.description); out.name("type").value("object"); if (!definition.field.parentType.isEmpty()) { out.name("extends").value(definition.field.parentType); } if (!definition.properties.isEmpty()) { out.name("properties"); out.beginObject(); for (FieldProperties property : definition.properties) { out.name(property.name); out.beginObject(); out.name("description").value(property.description); out.name("type").value(property.type); if (property.hasMin) { out.name("minimum").value(property.boundsMin); if (property.exclusiveMin) { out.name("exclusiveMinimum").value(true); } } if (property.hasMax) { out.name("maximum").value(property.boundsMax); if (property.exclusiveMax) { out.name("exclusiveMaximum").value(true); } } out.endObject(); } out.endObject(); } if (!definition.required.isEmpty()) { out.name("required"); out.beginArray(); for (String required : definition.required) { out.value(required); } out.endArray(); } out.endObject(); } @Override public Object read(JsonReader in) throws IOException { throw new UnsupportedOperationException("NIY"); } } }