Skip to content

Commit

Permalink
Support comparing to another fields (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
hbxie committed Aug 8, 2018
1 parent 4229e56 commit 15c7a90
Show file tree
Hide file tree
Showing 15 changed files with 488 additions and 135 deletions.
44 changes: 16 additions & 28 deletions src/main/java/com/yahoo/bullet/parsing/FieldTypeAdapterFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
Expand All @@ -16,13 +17,10 @@
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;

/**
* Adapted from * Google GSON's RuntimeTypeAdapterFactory to support a field based adapter. Instead of adding a new
Expand All @@ -43,39 +41,34 @@
*/
public class FieldTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<T> base;
private final Map<Class<?>, Set<String>> registeredTypes = new LinkedHashMap<>();
private final Function<JsonElement, String> extractor;
private final Map<Class<?>, Predicate<JsonObject>> registeredTypes = new LinkedHashMap<>();

private FieldTypeAdapterFactory(Class<T> base, Function<JsonElement, String> extractor) {
private FieldTypeAdapterFactory(Class<T> base) {
this.base = base;
this.extractor = extractor;
}

/**
* Creates a FieldTypeAdapterFactory of this type.
*
* @param base The base type for all types that this factory handles.
* @param fieldExtractor A {@link Function} that takes a JSONElement and returns the extracted String field that
* determines the type.
* @param <T> The base type.
* @return The created factory.
*/
public static <T> FieldTypeAdapterFactory<T> of(Class<T> base, Function<JsonElement, String> fieldExtractor) {
return new FieldTypeAdapterFactory<>(base, fieldExtractor);
public static <T> FieldTypeAdapterFactory<T> of(Class<T> base) {
return new FieldTypeAdapterFactory<>(base);
}

/**
* Register a subtype for the factory with the values it is to support. If the list of matchingValues is not
* disjoint across the subtypes, the order in which this method was called will determine which subType is matched.
* Register a subtype for the factory with the values it is to support.
*
* @param subType A subtype to handle.
* @param matchingValues The matching values that will decide this class
* @param condition The {@link Predicate} that will decide this class
* @return this object for chaining.
*/
public FieldTypeAdapterFactory<T> registerSubType(Class<? extends T> subType, List<String> matchingValues) {
public FieldTypeAdapterFactory<T> registerSubType(Class<? extends T> subType, Predicate<JsonObject> condition) {
Objects.requireNonNull(subType);
Objects.requireNonNull(matchingValues);
registeredTypes.put(subType, new HashSet<>(matchingValues));
Objects.requireNonNull(condition);
registeredTypes.put(subType, condition);
return this;
}

Expand All @@ -88,26 +81,22 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
for (Class<?> clazz : registeredTypes.keySet()) {
registeredAdapters.put(clazz, gson.getAdapter(clazz));
}
return new FieldTypeAdapter<>(extractor, registeredAdapters, registeredTypes);
return new FieldTypeAdapter<>(registeredAdapters, registeredTypes);
}

// Type checking for R's super type has already happened at registration. It's safe to ignore type check warnings.
@SuppressWarnings("unchecked")
private static class FieldTypeAdapter<R> extends TypeAdapter<R> {
private final Function<JsonElement, String> extractor;
private Map<Class<?>, TypeAdapter<?>> adapters;
private Map<Class<?>, Set<String>> types;
private Map<Class<?>, Predicate<JsonObject>> types;

/**
* Constructor for the adapter that takes an extraction mechanism and map of adapters and types.
*
* @param extractor A {@link Function} that takes a {@link JsonElement} and returns the string field from it.
* @param adapters A Map of Class to TypeAdapters for that Class.
* @param types A Map of Class to the Set of Strings that are to be matched against the output of extractor.
*/
public FieldTypeAdapter(Function<JsonElement, String> extractor, Map<Class<?>, TypeAdapter<?>> adapters,
Map<Class<?>, Set<String>> types) {
this.extractor = extractor;
public FieldTypeAdapter(Map<Class<?>, TypeAdapter<?>> adapters, Map<Class<?>, Predicate<JsonObject>> types) {
this.adapters = adapters;
this.types = types;
}
Expand All @@ -122,9 +111,8 @@ public void write(JsonWriter out, R value) throws IOException {
}

private TypeAdapter<R> getAdapterFor(JsonElement element) {
String field = extractor.apply(element);
for (Map.Entry<Class<?>, Set<String>> entry : types.entrySet()) {
if (entry.getValue().contains(field)) {
for (Map.Entry<Class<?>, Predicate<JsonObject>> entry : types.entrySet()) {
if (entry.getValue().test(element.getAsJsonObject())) {
return (TypeAdapter<R>) adapters.get(entry.getKey());
}
}
Expand Down
33 changes: 22 additions & 11 deletions src/main/java/com/yahoo/bullet/parsing/FilterClause.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,45 @@
import static com.yahoo.bullet.parsing.Clause.Operation.REGEX_LIKE;

@Slf4j @Getter @Setter
public class FilterClause extends Clause {
public abstract class FilterClause<T> extends Clause {
@Expose
private String field;
protected String field;

@Expose
private List<String> values;
protected List<T> values;

// An optimization to cache the compiled patterns per FilterClause rather than redoing it per record
private List<Pattern> patterns;
protected List<Pattern> patterns;

public static final String VALUES_FIELD = "values";

/**
* Default Constructor. GSON recommended.
*/
public FilterClause() {
field = null;
values = null;
operation = null;
values = null;
}

@Override
public void configure(BulletConfig configuration) {
if (operation == REGEX_LIKE) {
patterns = values.stream().map(FilterClause::compile).filter(Objects::nonNull).collect(Collectors.toList());
}
public String toString() {
return "{" + super.toString() + ", " + "field: " + field + ", " + "values: " + values + "}";
}

/**
* Get the value string from an object.
*
* @param value The value object to get from.
* @return The value string.
*/
public abstract String getValue(T value);

@Override
public String toString() {
return "{" + super.toString() + ", " + "field: " + field + ", " + "values: " + values + "}";
public void configure(BulletConfig configuration) {
if (operation == REGEX_LIKE) {
patterns = values.stream().map(v -> FilterClause.compile(getValue(v))).filter(Objects::nonNull).collect(Collectors.toList());
}
}

private static Pattern compile(String value) {
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/yahoo/bullet/parsing/ObjectFilterClause.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2018, Oath Inc.
* Licensed under the terms of the Apache License, Version 2.0.
* See the LICENSE file associated with the project for terms.
*/
package com.yahoo.bullet.parsing;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;
import java.util.stream.Collectors;

public class ObjectFilterClause extends FilterClause<ObjectFilterClause.Value> {
@Getter @AllArgsConstructor
public static class Value {
public enum Kind {
@SerializedName("VALUE")
VALUE,
@SerializedName("FIELD")
FIELD,
@SerializedName("CAST")
CAST
}
@Expose
private Kind kind;
@Expose
private String value;

@Override
public String toString() {
return "{kind: " + kind + ", " + "value: " + value + "}";
}
}

/**
* Default Constructor. GSON recommended.
*/
public ObjectFilterClause() {
super();
}

/**
* Constructor takes a {@link StringFilterClause} object to construct from.
*
* @param stringFilterClause The {@link StringFilterClause} object tor construct from.
*/
public ObjectFilterClause(StringFilterClause stringFilterClause) {
this.operation = stringFilterClause.operation;
this.field = stringFilterClause.field;
this.patterns = stringFilterClause.patterns;
List<String> stringValues = stringFilterClause.getValues();
if (stringValues != null) {
values = stringValues.stream().map(s -> s == null ? null : new Value(Value.Kind.VALUE, s)).collect(Collectors.toList());
}
}

@Override
public String getValue(Value value) {
return value.getValue();
}
}
36 changes: 33 additions & 3 deletions src/main/java/com/yahoo/bullet/parsing/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,47 @@

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.yahoo.bullet.common.BulletConfig;

public class Parser {
private static final FieldTypeAdapterFactory<Clause> CLAUSE_FACTORY =
FieldTypeAdapterFactory.of(Clause.class, t -> t.getAsJsonObject().get(Clause.OPERATION_FIELD).getAsString())
.registerSubType(FilterClause.class, Clause.Operation.RELATIONALS)
.registerSubType(LogicalClause.class, Clause.Operation.LOGICALS);
FieldTypeAdapterFactory.of(Clause.class)
.registerSubType(ObjectFilterClause.class, Parser::isObjectFilterClause)
.registerSubType(StringFilterClause.class, Parser::isStringFilterClause)
.registerSubType(LogicalClause.class, Parser::isLogicalClause);
private static final Gson GSON = new GsonBuilder().registerTypeAdapterFactory(CLAUSE_FACTORY)
.excludeFieldsWithoutExposeAnnotation()
.create();

private static Boolean isFilterClause(JsonObject jsonObject) {
JsonElement jsonElement = jsonObject.get(Clause.OPERATION_FIELD);
return jsonElement != null && Clause.Operation.RELATIONALS.contains(jsonElement.getAsString());
}

private static Boolean isStringFilterClause(JsonObject jsonObject) {
if (!isFilterClause(jsonObject)) {
return false;
}
JsonArray values = (JsonArray) jsonObject.get(FilterClause.VALUES_FIELD);
return values != null && values.size() != 0 && values.get(0).isJsonPrimitive();
}

private static Boolean isObjectFilterClause(JsonObject jsonObject) {
if (!isFilterClause(jsonObject)) {
return false;
}
JsonArray values = (JsonArray) jsonObject.get(FilterClause.VALUES_FIELD);
return values != null && values.size() != 0 && values.get(0).isJsonObject();
}

private static Boolean isLogicalClause(JsonObject jsonObject) {
JsonElement jsonElement = jsonObject.get(Clause.OPERATION_FIELD);
return jsonElement != null && Clause.Operation.LOGICALS.contains(jsonElement.getAsString());
}

/**
* Parses a Query out of the query string.
*
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/yahoo/bullet/parsing/StringFilterClause.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2018, Oath Inc.
* Licensed under the terms of the Apache License, Version 2.0.
* See the LICENSE file associated with the project for terms.
*/
package com.yahoo.bullet.parsing;

public class StringFilterClause extends FilterClause<String> {
/**
* Default Constructor. GSON recommended.
*/
public StringFilterClause() {
super();
}

@Override
public String getValue(String value) {
return value;
}
}
Loading

0 comments on commit 15c7a90

Please sign in to comment.