/*
* 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));
}
}