blob: 7a483a1d4b42ed42be6802665f554631cec36bea [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.tinkerpop.gremlin.process.traversal.step.map;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.apache.tinkerpop.gremlin.process.traversal.Pop;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.step.ByModulating;
import org.apache.tinkerpop.gremlin.process.traversal.step.PathProcessor;
import org.apache.tinkerpop.gremlin.process.traversal.step.Scoping;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalRing;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Marko A. Rodriguez (http://markorodriguez.com)
*/
public final class MathStep<S> extends MapStep<S, Double> implements ByModulating, TraversalParent, Scoping, PathProcessor {
private static final String CURRENT = "_";
private final String equation;
private final TinkerExpression expression;
private TraversalRing<S, Number> traversalRing = new TraversalRing<>();
private Set<String> keepLabels;
public MathStep(final Traversal.Admin traversal, final String equation) {
super(traversal);
this.equation = equation;
this.expression = new TinkerExpression(equation, MathStep.getVariables(this.equation));
}
@Override
protected Traverser.Admin<Double> processNextStart() {
return PathProcessor.processTraverserPathLabels(super.processNextStart(), this.keepLabels);
}
@Override
protected Double map(final Traverser.Admin<S> traverser) {
final Expression localExpression = new Expression(this.expression.getExpression());
for (final String var : this.expression.getVariables()) {
final Object o = var.equals(CURRENT) ?
TraversalUtil.applyNullable(traverser, this.traversalRing.next()) :
TraversalUtil.applyNullable((S) this.getNullableScopeValue(Pop.last, var, traverser), this.traversalRing.next());
// it's possible for ElementValueTraversal to return null or something that is possibly not a Number.
// worth a check to try to return a nice error message. The TraversalRing<S, Number> is a bit optimistic
// given type erasure. It could easily end up otherwise.
if (!(o instanceof Number))
throw new IllegalStateException(String.format(
"The variable %s for math() step must resolve to a Number - it is instead of type %s with value %s",
var, Objects.isNull(o) ? "null" : o.getClass().getName(), o));
localExpression.setVariable(var, ((Number) o).doubleValue());
}
this.traversalRing.reset();
return localExpression.evaluate();
}
@Override
public void modulateBy(final Traversal.Admin<?, ?> selectTraversal) {
this.traversalRing.addTraversal(this.integrateChild(selectTraversal));
}
@Override
public ElementRequirement getMaxRequirement() {
// this is a trick i saw in DedupGlobalStep that allows ComputerVerificationStrategy to be happy for OLAP.
// it's a bit more of a hack here. in DedupGlobalStep, the dedup operation really only just needs the ID, but
// here the true max requirement is PROPERTIES, but because of how map() works in this implementation in
// relation to CURRENT, if we don't access the path labels, then we really only just operate on the stargraph
// and are thus OLAP safe. In tracing around the code a bit, I don't see a problem with taking this approach,
// but I suppose a better way might be make it more clear when this step is dealing with an actual path and
// when it is not and/or adjust ComputerVerificationStrategy to cope with the situation where math() is only
// dealing with the local stargraph.
return (this.expression.getVariables().contains(CURRENT) && this.expression.getVariables().size() == 1) ?
ElementRequirement.ID : PathProcessor.super.getMaxRequirement();
}
@Override
public String toString() {
return StringFactory.stepString(this, this.equation, this.traversalRing);
}
@Override
public int hashCode() {
return super.hashCode() ^ this.equation.hashCode() ^ this.traversalRing.hashCode();
}
@Override
public List<Traversal.Admin<S, Number>> getLocalChildren() {
return this.traversalRing.getTraversals();
}
@Override
public void reset() {
super.reset();
this.traversalRing.reset();
}
@Override
public MathStep<S> clone() {
final MathStep<S> clone = (MathStep<S>) super.clone();
clone.traversalRing = this.traversalRing.clone();
return clone;
}
@Override
public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
super.setTraversal(parentTraversal);
this.traversalRing.getTraversals().forEach(this::integrateChild);
}
@Override
public Set<TraverserRequirement> getRequirements() {
return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT, TraverserRequirement.SIDE_EFFECTS);
}
@Override
public Set<String> getScopeKeys() {
if (this.expression.getVariables().contains(CURRENT)) {
final Set<String> temp = new HashSet<>(this.expression.getVariables());
temp.remove(CURRENT);
return temp;
} else
return this.expression.getVariables();
}
@Override
public void setKeepLabels(final Set<String> labels) {
this.keepLabels = labels;
}
@Override
public Set<String> getKeepLabels() {
return this.keepLabels;
}
///
private static final String[] FUNCTIONS = new String[]{
"abs", "acos", "asin", "atan",
"cbrt", "ceil", "cos", "cosh",
"exp",
"floor",
"log", "log10", "log2",
"signum", "sin", "sinh", "sqrt",
"tan", "tanh"
};
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\b(?!" +
String.join("|", FUNCTIONS) + "|([0-9]+))([a-zA-Z_][a-zA-Z0-9_]*)\\b");
protected static final Set<String> getVariables(final String equation) {
final Matcher matcher = VARIABLE_PATTERN.matcher(equation);
final Set<String> variables = new LinkedHashSet<>();
while (matcher.find()) {
variables.add(matcher.group());
}
return variables;
}
/**
* A wrapper for the {@code Expression} class. That class is not marked {@code Serializable} and therefore gives
* problems in OLAP specifically with Spark. This wrapper allows the {@code Expression} to be serialized in that
* context with Java serialization.
*/
public static class TinkerExpression implements Serializable {
private transient Expression expression;
private final String equation;
private final Set<String> variables;
public TinkerExpression(final String equation, final Set<String> variables) {
this.variables = variables;
this.equation = equation;
}
public Expression getExpression() {
if (null == expression) {
this.expression = new ExpressionBuilder(this.equation)
.variables(this.variables)
.implicitMultiplication(false)
.build();
}
return expression;
}
public Set<String> getVariables() {
return variables;
}
}
}