diff --git a/client/src/main/java/com/metamx/druid/query/filter/DimFilter.java b/client/src/main/java/com/metamx/druid/query/filter/DimFilter.java index 8da47da465c..5099f95b3d2 100644 --- a/client/src/main/java/com/metamx/druid/query/filter/DimFilter.java +++ b/client/src/main/java/com/metamx/druid/query/filter/DimFilter.java @@ -33,7 +33,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(name="selector", value=SelectorDimFilter.class), @JsonSubTypes.Type(name="extraction", value=ExtractionDimFilter.class), @JsonSubTypes.Type(name="regex", value=RegexDimFilter.class), - @JsonSubTypes.Type(name="search", value=SearchQueryDimFilter.class) + @JsonSubTypes.Type(name="search", value=SearchQueryDimFilter.class), + @JsonSubTypes.Type(name="javascript", value=JavaScriptDimFilter.class) }) public interface DimFilter { diff --git a/client/src/main/java/com/metamx/druid/query/filter/DimFilterCacheHelper.java b/client/src/main/java/com/metamx/druid/query/filter/DimFilterCacheHelper.java index 67f35a9bab9..8497a53b206 100644 --- a/client/src/main/java/com/metamx/druid/query/filter/DimFilterCacheHelper.java +++ b/client/src/main/java/com/metamx/druid/query/filter/DimFilterCacheHelper.java @@ -34,6 +34,7 @@ class DimFilterCacheHelper static final byte EXTRACTION_CACHE_ID = 0x4; static final byte REGEX_CACHE_ID = 0x5; static final byte SEARCH_QUERY_TYPE_ID = 0x6; + static final byte JAVASCRIPT_CACHE_ID = 0x7; static byte[] computeCacheKey(byte cacheIdKey, List filters) { diff --git a/client/src/main/java/com/metamx/druid/query/filter/JavaScriptDimFilter.java b/client/src/main/java/com/metamx/druid/query/filter/JavaScriptDimFilter.java new file mode 100644 index 00000000000..847d13c9bd4 --- /dev/null +++ b/client/src/main/java/com/metamx/druid/query/filter/JavaScriptDimFilter.java @@ -0,0 +1,48 @@ +package com.metamx.druid.query.filter; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Charsets; + +import java.nio.ByteBuffer; + +public class JavaScriptDimFilter implements DimFilter +{ + private final String dimension; + private final String function; + + @JsonCreator + public JavaScriptDimFilter( + @JsonProperty("dimension") String dimension, + @JsonProperty("function") String function + ) + { + this.dimension = dimension; + this.function = function; + } + + @JsonProperty + public String getDimension() + { + return dimension; + } + + @JsonProperty + public String getFunction() + { + return function; + } + + @Override + public byte[] getCacheKey() + { + final byte[] dimensionBytes = dimension.getBytes(Charsets.UTF_8); + final byte[] functionBytes = function.getBytes(Charsets.UTF_8); + + return ByteBuffer.allocate(1 + dimensionBytes.length + functionBytes.length) + .put(DimFilterCacheHelper.JAVASCRIPT_CACHE_ID) + .put(dimensionBytes) + .put(functionBytes) + .array(); + } +} diff --git a/server/src/main/java/com/metamx/druid/index/brita/Filters.java b/server/src/main/java/com/metamx/druid/index/brita/Filters.java index 0e4e4c4e8ea..f7c390c44a9 100644 --- a/server/src/main/java/com/metamx/druid/index/brita/Filters.java +++ b/server/src/main/java/com/metamx/druid/index/brita/Filters.java @@ -24,6 +24,7 @@ import com.google.common.collect.Lists; import com.metamx.druid.query.filter.AndDimFilter; import com.metamx.druid.query.filter.DimFilter; import com.metamx.druid.query.filter.ExtractionDimFilter; +import com.metamx.druid.query.filter.JavaScriptDimFilter; import com.metamx.druid.query.filter.NotDimFilter; import com.metamx.druid.query.filter.OrDimFilter; import com.metamx.druid.query.filter.RegexDimFilter; @@ -84,6 +85,10 @@ public class Filters final SearchQueryDimFilter searchQueryFilter = (SearchQueryDimFilter) dimFilter; filter = new SearchQueryFilter(searchQueryFilter.getDimension(), searchQueryFilter.getQuery()); + } else if (dimFilter instanceof JavaScriptDimFilter) { + final JavaScriptDimFilter javaScriptDimFilter = (JavaScriptDimFilter) dimFilter; + + filter = new JavaScriptFilter(javaScriptDimFilter.getDimension(), javaScriptDimFilter.getFunction()); } return filter; diff --git a/server/src/main/java/com/metamx/druid/index/brita/JavaScriptFilter.java b/server/src/main/java/com/metamx/druid/index/brita/JavaScriptFilter.java new file mode 100644 index 00000000000..f17de0b8d85 --- /dev/null +++ b/server/src/main/java/com/metamx/druid/index/brita/JavaScriptFilter.java @@ -0,0 +1,127 @@ +package com.metamx.druid.index.brita; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.metamx.common.guava.FunctionalIterable; +import it.uniroma3.mat.extendedset.intset.ImmutableConciseSet; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.ScriptableObject; + +import javax.annotation.Nullable; + + +public class JavaScriptFilter implements Filter +{ + private final JavaScriptPredicate predicate; + private final String dimension; + + public JavaScriptFilter(String dimension, final String script) + { + this.dimension = dimension; + this.predicate = new JavaScriptPredicate(script); + } + + @Override + public ImmutableConciseSet goConcise(final BitmapIndexSelector selector) + { + final Context cx = Context.enter(); + try { + ImmutableConciseSet conciseSet = ImmutableConciseSet.union( + FunctionalIterable.create(selector.getDimensionValues(dimension)) + .filter(new Predicate() + { + @Override + public boolean apply(@Nullable String input) + { + return predicate.applyInContext(cx, input); + } + }) + .transform( + new com.google.common.base.Function() + { + @Override + public ImmutableConciseSet apply(@Nullable String input) + { + return selector.getConciseInvertedIndex(dimension, input); + } + } + ) + ); + return conciseSet; + } finally { + Context.exit(); + } + } + + @Override + public ValueMatcher makeMatcher(ValueMatcherFactory factory) + { + // suboptimal, since we need create one context per call to predicate.apply() + return factory.makeValueMatcher(dimension, predicate); + } + + static class JavaScriptPredicate implements Predicate { + final ScriptableObject scope; + final Function fnApply; + final String script; + + public JavaScriptPredicate(final String script) { + Preconditions.checkNotNull(script, "script must not be null"); + this.script = script; + + final Context cx = Context.enter(); + try { + cx.setOptimizationLevel(9); + scope = cx.initStandardObjects(); + + fnApply = cx.compileFunction(scope, script, "script", 1, null); + } finally { + Context.exit(); + } + } + + @Override + public boolean apply(final String input) + { + // one and only one context per thread + final Context cx = Context.enter(); + try { + return applyInContext(cx, input); + } finally { + Context.exit(); + } + + } + + public boolean applyInContext(Context cx, String input) + { + return Context.toBoolean(fnApply.call(cx, scope, scope, new String[]{input})); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JavaScriptPredicate that = (JavaScriptPredicate) o; + + if (!script.equals(that.script)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return script.hashCode(); + } + } +}