Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContains;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Wildcard;
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
Expand All @@ -32,6 +33,7 @@ private static FunctionDefinition[][] functions() {
def(EndsWith.class, EndsWith::new, "endswith"),
def(Length.class, Length::new, "length"),
def(StartsWith.class, StartsWith::new, "startswith"),
def(StringContains.class, StringContains::new, "stringcontains"),
def(Substring.class, Substring::new, "substring"),
def(Wildcard.class, Wildcard::new, "wildcard"),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.Expressions;
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
import org.elasticsearch.xpack.ql.expression.gen.script.Scripts;
import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source;
import org.elasticsearch.xpack.ql.type.DataType;
import org.elasticsearch.xpack.ql.type.DataTypes;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import static java.lang.String.format;
import static org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor.doProcess;
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;

/**
* EQL specific stringContains function.
* stringContains(a, b)
* Returns true if b is a substring of a
*/
public class StringContains extends ScalarFunction {

private final Expression string, substring;

public StringContains(Source source, Expression string, Expression substring) {
super(source, Arrays.asList(string, substring));
this.string = string;
this.substring = substring;
}

@Override
protected TypeResolution resolveType() {
if (!childrenResolved()) {
return new TypeResolution("Unresolved children");
}

TypeResolution resolution = isStringAndExact(string, sourceText(), Expressions.ParamOrdinal.FIRST);
if (resolution.unresolved()) {
return resolution;
}

return isStringAndExact(substring, sourceText(), Expressions.ParamOrdinal.SECOND);
}

@Override
protected Pipe makePipe() {
return new StringContainsFunctionPipe(source(), this,
Expressions.pipe(string), Expressions.pipe(substring));
}

@Override
public boolean foldable() {
return string.foldable() && substring.foldable();
}

@Override
public Object fold() {
return doProcess(string.fold(), substring.fold());
}

@Override
protected NodeInfo<? extends Expression> info() {
return NodeInfo.create(this, StringContains::new, string, substring);
}

@Override
public ScriptTemplate asScript() {
return asScriptFrom(asScript(string), asScript(substring));
}

protected ScriptTemplate asScriptFrom(ScriptTemplate stringScript, ScriptTemplate substringScript) {
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s)"),
"stringContains",
stringScript.template(),
substringScript.template()),
paramsBuilder()
.script(stringScript.params())
.script(substringScript.params())
.build(), dataType());
}

@Override
public ScriptTemplate scriptWithField(FieldAttribute field) {
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
paramsBuilder().variable(field.exactAttribute().name()).build(),
dataType());
}

@Override
public DataType dataType() {
return DataTypes.BOOLEAN;
}

@Override
public Expression replaceChildren(List<Expression> newChildren) {
if (newChildren.size() != 2) {
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
}

return new StringContains(source(), newChildren.get(0), newChildren.get(1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.xpack.ql.execution.search.QlSourceBuilder;
import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public class StringContainsFunctionPipe extends Pipe {

private final Pipe string, substring;

public StringContainsFunctionPipe(Source source, Expression expression, Pipe string, Pipe substring) {
super(source, expression, Arrays.asList(string, substring));
this.string = string;
this.substring = substring;
}

@Override
public final Pipe replaceChildren(List<Pipe> newChildren) {
if (newChildren.size() != 2) {
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
}
return replaceChildren(newChildren.get(0), newChildren.get(1));
}

@Override
public final Pipe resolveAttributes(AttributeResolver resolver) {
Pipe newString = string.resolveAttributes(resolver);
Pipe newSubstring = substring.resolveAttributes(resolver);
if (newString == string && newSubstring == substring) {
return this;
}
return replaceChildren(newString, newSubstring);
}

@Override
public boolean supportedByAggsOnlyQuery() {
return string.supportedByAggsOnlyQuery() && substring.supportedByAggsOnlyQuery();
}

@Override
public boolean resolved() {
return string.resolved() && substring.resolved();
}

protected Pipe replaceChildren(Pipe string, Pipe substring) {
return new StringContainsFunctionPipe(source(), expression(), string, substring);
}

@Override
public final void collectFields(QlSourceBuilder sourceBuilder) {
string.collectFields(sourceBuilder);
substring.collectFields(sourceBuilder);
}

@Override
protected NodeInfo<StringContainsFunctionPipe> info() {
return NodeInfo.create(this, StringContainsFunctionPipe::new, expression(), string, substring);
}

@Override
public StringContainsFunctionProcessor asProcessor() {
return new StringContainsFunctionProcessor(string.asProcessor(), substring.asProcessor());
}

public Pipe string() {
return string;
}

public Pipe substring() {
return substring;
}


@Override
public int hashCode() {
return Objects.hash(source(), string(), substring());
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

StringContainsFunctionPipe other = (StringContainsFunctionPipe) obj;
return Objects.equals(source(), other.source())
&& Objects.equals(string(), other.string())
&& Objects.equals(substring(), other.substring());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.eql.EqlIllegalArgumentException;
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;

import java.io.IOException;
import java.util.Objects;

public class StringContainsFunctionProcessor implements Processor {

public static final String NAME = "sstc";

private final Processor string, substring;

public StringContainsFunctionProcessor(Processor string, Processor substring) {
this.string = string;
this.substring = substring;
}

public StringContainsFunctionProcessor(StreamInput in) throws IOException {
string = in.readNamedWriteable(Processor.class);
substring = in.readNamedWriteable(Processor.class);
}

@Override
public final void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(string);
out.writeNamedWriteable(substring);
}

@Override
public Object process(Object input) {
return doProcess(string.process(input), substring.process(input));
}

public static Object doProcess(Object string, Object substring) {
if (string == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (string == null) {
if (string == null || substring == null) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwing exception for now if the second param is null, consistent across all 3 functions that I implemented so far

Copy link
Contributor Author

@aleksmaus aleksmaus Apr 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we leaning towards returning nulls more often and not throwing exceptions?

return null;
}

throwIfNotString(string);
throwIfNotString(substring);

String strString = string.toString();
String strSubstring = substring.toString();
return StringUtils.stringContains(strString, strSubstring);
}

private static void throwIfNotString(Object obj) {
if (!(obj instanceof String || obj instanceof Character)) {
throw new EqlIllegalArgumentException("A string/char is required; received [{}]", obj);
}
}

protected Processor string() {
return string;
}

public Processor substring() {
return substring;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

StringContainsFunctionProcessor other = (StringContainsFunctionProcessor) obj;
return Objects.equals(string(), other.string())
&& Objects.equals(substring(), other.substring());
}

@Override
public int hashCode() {
return Objects.hash(string(), substring());
}


@Override
public String getWriteableName() {
return NAME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ static String between(String string, String left, String right, boolean greedy,
return string.substring(start, idx);
}

/**
* Checks if {@code string} contains {@code substring} string.
*
* @param string string to search through.
* @param substring string to search for.
* @return {@code true} if {@code string} string contains {@code substring} string.
*/
static boolean stringContains(String string, String substring) {
if (hasLength(string) == false || hasLength(substring) == false) {
return false;
}

string = string.toLowerCase(Locale.ROOT);
substring = substring.toLowerCase(Locale.ROOT);
return string.contains(substring);
}

/**
* Returns a substring using the Python slice semantics, meaning
* start and end can be negative
Expand All @@ -70,7 +87,7 @@ static String substringSlice(String string, int start, int end) {
}

int length = string.length();

// handle first negative values
if (start < 0) {
start += length;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@ public Expression replaceChildren(List<Expression> newChildren) {

return new Substring(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWithFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor;
import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils;

/*
Expand Down Expand Up @@ -38,6 +39,10 @@ public static Boolean startsWith(String s, String pattern) {
return (Boolean) StartsWithFunctionProcessor.doProcess(s, pattern);
}

public static Boolean stringContains(String string, String substring) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before substring for alphabetical order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved.

return (Boolean) StringContainsFunctionProcessor.doProcess(string, substring);
}

public static String substring(String s, Number start, Number end) {
return (String) SubstringFunctionProcessor.doProcess(s, start, end);
}
Expand Down
Loading