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");
        }
    }

}