com.github.fge.jsonschema2avro.predicates.AvroPredicates.java Source code

Java tutorial

Introduction

Here is the source code for com.github.fge.jsonschema2avro.predicates.AvroPredicates.java

Source

/*
 * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
 *
 * This software is dual-licensed under:
 *
 * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
 *   later version;
 * - the Apache Software License (ASL) version 2.0.
 *
 * The text of both licenses is available under the src/resources/ directory of
 * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively).
 *
 * Direct link to the sources:
 *
 * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
 * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
 */

package com.github.fge.jsonschema2avro.predicates;

import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jackson.NodeType;
import com.github.fge.jsonschema.library.DraftV4Library;
import com.github.fge.jsonschema.processors.validation.ArraySchemaDigester;
import com.github.fge.jsonschema.processors.validation.ObjectSchemaDigester;
import com.github.fge.jsonschema2avro.AvroPayload;
import com.google.common.base.CharMatcher;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

import java.util.Set;

public final class AvroPredicates {
    private static final Set<String> KNOWN_KEYWORDS;
    private static final CharMatcher NAME_CHAR;
    private static final CharMatcher DIGIT;

    static {
        final CharMatcher letterOrUnderscore = CharMatcher.is('_').or(CharMatcher.inRange('a', 'z'))
                .or(CharMatcher.inRange('A', 'Z'));
        final CharMatcher digit = CharMatcher.inRange('0', '9');

        NAME_CHAR = letterOrUnderscore.or(digit).precomputed();
        DIGIT = digit.precomputed();

        /*
         * We don't care about keywords having only syntax checkers, only about
         * keywords having an actual use in validation
         */
        KNOWN_KEYWORDS = ImmutableSet.copyOf(DraftV4Library.get().getDigesters().entries().keySet());
    }

    private AvroPredicates() {
    }

    public static Predicate<AvroPayload> simpleType() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                final JsonNode node = schemaNode(input);
                final NodeType type = getType(node);
                if (type == null)
                    return false;
                return type != NodeType.ARRAY && type != NodeType.OBJECT;
            }
        };
    }

    public static Predicate<AvroPayload> array() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                final JsonNode node = schemaNode(input);
                final NodeType type = getType(node);
                if (NodeType.ARRAY != type)
                    return false;

                final JsonNode digest = ArraySchemaDigester.getInstance().digest(node);

                // FIXME: I should probably make digests POJOs here
                return digest.get("hasItems").booleanValue() ? !digest.get("itemsIsArray").booleanValue()
                        : digest.get("hasAdditional").booleanValue();
            }
        };
    }

    public static Predicate<AvroPayload> map() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                final JsonNode node = schemaNode(input);
                final NodeType type = getType(node);
                if (NodeType.OBJECT != type)
                    return false;

                final JsonNode digest = ObjectSchemaDigester.getInstance().digest(node);

                // FIXME: as for array digester, the result should really be
                // a POJO
                if (!digest.get("hasAdditional").booleanValue())
                    return false;

                return digest.get("properties").size() == 0 && digest.get("patternProperties").size() == 0;
            }
        };
    }

    public static Predicate<AvroPayload> isEnum() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                final JsonNode node = schemaNode(input);
                final Set<String> set = Sets.newHashSet(node.fieldNames());
                set.retainAll(KNOWN_KEYWORDS);
                if (!set.equals(ImmutableSet.of("enum")))
                    return false;

                // Test individual entries: they must be strings, and must be
                // the same "shape" as any Avro name
                for (final JsonNode element : node.get("enum")) {
                    if (!element.isTextual())
                        return false;
                    if (!isValidAvroName(element.textValue()))
                        return false;
                }

                return true;
            }
        };
    }

    public static Predicate<AvroPayload> record() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                final JsonNode node = schemaNode(input);
                final NodeType type = getType(node);
                if (NodeType.OBJECT != type)
                    return false;

                if (node.path("additionalProperties").asBoolean(true))
                    return false;

                if (node.has("patternProperties"))
                    return false;

                final JsonNode properties = node.path("properties");
                if (!properties.isObject())
                    return false;

                for (final String s : Sets.newHashSet(properties.fieldNames()))
                    if (!isValidAvroName(s))
                        return false;

                return true;
            }
        };
    }

    public static Predicate<AvroPayload> simpleUnion() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                // NOTE: enums within enums are forbidden. This is tested in
                // writers, not here.
                final JsonNode node = schemaNode(input);
                final Set<String> members = Sets.newHashSet(node.fieldNames());
                members.retainAll(KNOWN_KEYWORDS);
                return members.equals(ImmutableSet.of("anyOf")) || members.equals(ImmutableSet.of("oneOf"));
            }
        };
    }

    public static Predicate<AvroPayload> typeUnion() {
        return new Predicate<AvroPayload>() {
            @Override
            public boolean apply(final AvroPayload input) {
                return schemaNode(input).path("type").isArray();
            }
        };
    }

    private static JsonNode schemaNode(final AvroPayload payload) {
        return payload.getTree().getNode();
    }

    private static NodeType getType(final JsonNode node) {
        final JsonNode typeNode = node.path("type");
        return typeNode.isTextual() ? NodeType.fromName(typeNode.textValue()) : null;
    }

    private static boolean isValidAvroName(final String s) {
        if (s.isEmpty())
            return false;
        if (!NAME_CHAR.matchesAllOf(s))
            return false;
        return !DIGIT.matches(s.charAt(0));
    }
}