blob: 78d144616f4b582735ab615ffe153bd5cd55b5f3 [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.language.grammar;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.tinkerpop.gremlin.process.traversal.Merge;
import org.apache.tinkerpop.gremlin.process.traversal.Pick;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.util.DatetimeHelper;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Visitor class to handle generic literal. All visitor methods return type is Object. It maybe used as a singleton
* in cases where a {@link Traversal} object is not expected, otherwise a new instance must be constructed.
*/
public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
/**
* Limit for integer range result count. It is used to avoid OOM in JVM.
*/
public static final int TOTAL_INTEGER_RANGE_RESULT_COUNT_LIMIT = 1_000_000;
protected final GremlinAntlrToJava antlr;
protected DefaultGremlinBaseVisitor<TraversalStrategy> traversalStrategyVisitor;
private static GenericLiteralVisitor instance;
private GenericLiteralVisitor() {
this.antlr = null;
}
public GenericLiteralVisitor(final GremlinAntlrToJava antlr) {
this.antlr = antlr;
}
public static GenericLiteralVisitor instance() {
if (instance == null) {
instance = new GenericLiteralVisitor();
}
return instance;
}
/**
* @deprecated As of release 3.5.2, replaced by {@link #instance()}.
*/
@Deprecated
public static GenericLiteralVisitor getInstance() {
return instance();
}
/**
* Parse a string literal context and return the string literal
*/
public static String getStringLiteral(final GremlinParser.StringBasedLiteralContext stringLiteral) {
return (String) (instance().visitStringBasedLiteral(stringLiteral));
}
/**
* Parse a map literal context and return the map literal
*/
public static Map getMapLiteral(final GremlinParser.GenericLiteralMapContext mapLiteral) {
return (Map) (instance().visitGenericLiteralMap(mapLiteral));
}
/**
* Parse a boolean literal context and return the boolean literal
*/
public static boolean getBooleanLiteral(final GremlinParser.BooleanLiteralContext booleanLiteral) {
return (boolean) (instance().visitBooleanLiteral(booleanLiteral));
}
/**
* Parse a String literal list context and return a string array
*/
public static String[] getStringLiteralList(final GremlinParser.StringLiteralListContext stringLiteralList) {
if (stringLiteralList == null || stringLiteralList.stringLiteralExpr() == null) {
return new String[0];
}
return stringLiteralList.stringLiteralExpr().stringBasedLiteral()
.stream()
.filter(Objects::nonNull)
.map(stringLiteral -> instance().visitStringBasedLiteral(stringLiteral))
.toArray(String[]::new);
}
/**
* Parse a generic literal list, and return an object array
*/
public static Object[] getGenericLiteralList(final GremlinParser.GenericLiteralListContext objectLiteralList) {
if (objectLiteralList == null || objectLiteralList.genericLiteralExpr() == null) {
return new Object[0];
}
return objectLiteralList.genericLiteralExpr().genericLiteral()
.stream()
.filter(Objects::nonNull)
.map(genericLiteral -> instance().visitGenericLiteral(genericLiteral))
.toArray(Object[]::new);
}
/**
* Parse a TraversalStrategy literal list context and return a string array
*/
public static TraversalStrategy[] getTraversalStrategyList(final GremlinParser.TraversalStrategyListContext traversalStrategyListContext,
final DefaultGremlinBaseVisitor<TraversalStrategy> traversalStrategyVisitor) {
if (traversalStrategyListContext == null || traversalStrategyListContext.traversalStrategyExpr() == null) {
return new TraversalStrategy[0];
}
return traversalStrategyListContext.traversalStrategyExpr().traversalStrategy()
.stream()
.filter(Objects::nonNull)
.map(tstrat -> traversalStrategyVisitor.visitTraversalStrategy(tstrat))
.toArray(TraversalStrategy[]::new);
}
/**
* Remove single/double quotes around String literal
*
* @param quotedString : quoted string
* @return quotes stripped string
*/
private static String stripQuotes(final String quotedString) {
return quotedString.substring(1, quotedString.length() - 1);
}
/**
* create an integer range from start to end, based on groovy syntax
* http://groovy-lang.org/operators.html#_range_operator
*
* @param start : start of range
* @param end : end of range
* @param range : original range string, for error message
* @return : return an object which is type of array of object, and each object is a Integer inside the range.
*/
private static Object createIntegerRange(final int start, final int end, final String range) {
final List<Object> results = new ArrayList<>();
int total_result_count = Math.abs(start - end);
// validate result count not exceeding limit
if (total_result_count > TOTAL_INTEGER_RANGE_RESULT_COUNT_LIMIT) {
throw new IllegalArgumentException("Range " + range + " is too wide. Current limit is " + TOTAL_INTEGER_RANGE_RESULT_COUNT_LIMIT + " items");
}
if (start <= end) {
// handle start <= end
int cur = start;
while (cur <= end) {
results.add(cur);
cur++;
}
} else {
// handle start > end
int cur = start;
while (cur >= end) {
results.add(cur);
cur--;
}
}
return results;
}
/**
* create a string range from start to end, based on groovy syntax
* http://groovy-lang.org/operators.html#_range_operator
* The start and end needs to have same length and share same prefix except the last character.
*
* @param start : start of range
* @param end : end of range
* @param range : original range string, for error message
* @return : return an object which is type of array of object, and each object is a String inside the range.
*/
private static Object createStringRange(final String start, final String end, final String range) {
final List<Object> results = new ArrayList<>();
// verify lengths of start and end are same.
if (start.length() != end.length()) {
throw new IllegalArgumentException("The start and end of Range " + range + " does not have same number of characters");
}
if (start.isEmpty()) {
// return empty result for empty string ranges
return results;
}
// verify start and end share same prefix
final String commonPrefix = start.substring(0, start.length() - 1);
if (!end.startsWith(commonPrefix)) {
throw new IllegalArgumentException("The start and end of Range " + range +
" does not share same prefix until the last character");
}
final char startLastCharacter = start.charAt(start.length() - 1);
final char endLastCharacter = end.charAt(end.length() - 1);
if (startLastCharacter <= endLastCharacter) {
// handle start <= end
char cur = startLastCharacter;
while (cur <= endLastCharacter) {
results.add(commonPrefix + cur);
cur++;
}
} else {
// handle start > end
char cur = startLastCharacter;
while (cur >= endLastCharacter) {
results.add(commonPrefix + cur);
cur--;
}
}
return results;
}
/**
* {@inheritDoc}
*/
@Override
public Object visitGenericLiteralList(final GremlinParser.GenericLiteralListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitGenericLiteralExpr(final GremlinParser.GenericLiteralExprContext ctx) {
final int childCount = ctx.getChildCount();
switch (childCount) {
case 0:
// handle empty expression
return new Object[0];
case 1:
// handle single generic literal
return visitGenericLiteral((GremlinParser.GenericLiteralContext) ctx.getChild(0));
default:
// handle multiple generic literal separated by comma
final List<Object> genericLiterals = new ArrayList<>();
int childIndex = 0;
while (childIndex < ctx.getChildCount()) {
genericLiterals.add(visitGenericLiteral(
(GremlinParser.GenericLiteralContext) ctx.getChild(childIndex)));
// skip comma
childIndex += 2;
}
return genericLiterals.toArray();
}
}
/**
* {@inheritDoc}
*/
@Override
public Object visitGenericLiteral(final GremlinParser.GenericLiteralContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitGenericLiteralMap(final GremlinParser.GenericLiteralMapContext ctx) {
if (ctx == null) {
return null;
}
final LinkedHashMap<Object, Object> literalMap = new LinkedHashMap<>();
// filter out tokens and just grab map entries
ctx.children.stream().filter(c -> c instanceof GremlinParser.MapEntryContext).forEach(c -> {
// [key : value] - index 0 is key, index 1 is COLON, index 2 is value
// also note that a child count of 5 indicates the key is an expression wrapped
// in parens as in `[(T.id): 1]`
final boolean isKeyExpression = c.getChildCount() == 5;
final Object kctx = isKeyExpression ? c.getChild(1) : c.getChild(0);
final Object key;
if (kctx instanceof GremlinParser.StringLiteralContext) {
key = visitStringLiteral((GremlinParser.StringLiteralContext) kctx);
} else if (kctx instanceof GremlinParser.NumericLiteralContext) {
key = visitNumericLiteral((GremlinParser.NumericLiteralContext) kctx);
} else if (kctx instanceof GremlinParser.TraversalTokenContext) {
key = visitTraversalToken((GremlinParser.TraversalTokenContext) kctx);
} else if (kctx instanceof GremlinParser.TraversalDirectionContext) {
key = visitTraversalDirection((GremlinParser.TraversalDirectionContext) kctx);
} else if (kctx instanceof GremlinParser.GenericLiteralCollectionContext) {
key = visitGenericLiteralCollection((GremlinParser.GenericLiteralCollectionContext) kctx);
} else if (kctx instanceof GremlinParser.GenericLiteralMapContext) {
key = visitGenericLiteralMap((GremlinParser.GenericLiteralMapContext) kctx);
} else if (kctx instanceof TerminalNode) {
key = ((TerminalNode) kctx).getText();
} else {
throw new GremlinParserException("Invalid key for map" + ((ParseTree) kctx).getText());
}
final int valueIndex = isKeyExpression ? 4 : 2;
final Object value = visitGenericLiteral((GremlinParser.GenericLiteralContext) c.getChild(valueIndex));
literalMap.put(key, value);
});
return literalMap;
}
/**
* {@inheritDoc}
*/
@Override
public Object visitNestedTraversal(final GremlinParser.NestedTraversalContext ctx) {
return antlr.tvisitor.visitNestedTraversal(ctx);
}
@Override
public Object visitStructureVertex(final GremlinParser.StructureVertexContext ctx) {
return StructureElementVisitor.instance().visitStructureVertex(ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTerminatedTraversal(final GremlinParser.TerminatedTraversalContext ctx) {
final Traversal traversal = antlr.tvisitor.visitRootTraversal(
(GremlinParser.RootTraversalContext) ctx.getChild(0));
return new TraversalTerminalMethodVisitor(traversal).visitTraversalTerminalMethod(
(GremlinParser.TraversalTerminalMethodContext) ctx.getChild(2));
}
/**
* {@inheritDoc}
*/
@Override
public Object visitIntegerLiteral(final GremlinParser.IntegerLiteralContext ctx) {
String integerLiteral = ctx.getText().toLowerCase().replace("_", "");
// handle suffixes for specific types
final int lastCharIndex = integerLiteral.length() - 1;
final char suffix = integerLiteral.charAt(lastCharIndex);
switch (suffix) {
case 'b':
integerLiteral = integerLiteral.substring(0, lastCharIndex);
return Byte.decode(integerLiteral);
case 's':
integerLiteral = integerLiteral.substring(0, lastCharIndex);
return Short.decode(integerLiteral);
case 'i':
integerLiteral = integerLiteral.substring(0, lastCharIndex);
return Integer.decode(integerLiteral);
case 'l':
integerLiteral = integerLiteral.substring(0, lastCharIndex);
return Long.decode(integerLiteral);
case 'n':
integerLiteral = integerLiteral.substring(0, lastCharIndex);
return new BigInteger(integerLiteral);
}
try {
// try to parse it as integer first
return Integer.decode(integerLiteral);
} catch (NumberFormatException ignoredExpection1) {
try {
// If range exceeds integer limit, try to parse it as long
return Long.decode(integerLiteral);
} catch (NumberFormatException ignoredExpection2) {
// If range exceeds Long limit, parse it as BigInteger
// as the literal range is longer than long, the number of character should be much more than 3,
// so we skip boundary check below.
// parse sign character
int startIndex = 0;
final char firstChar = integerLiteral.charAt(0);
final boolean negative = (firstChar == '-');
if ((firstChar == '-') || (firstChar == '+')) {
startIndex++;
}
// parse radix based on format
int radix = 10;
if (integerLiteral.charAt(startIndex + 1) == 'x') {
radix = 16;
startIndex += 2;
integerLiteral = integerLiteral.substring(startIndex);
if (negative) {
integerLiteral = '-' + integerLiteral;
}
} else if (integerLiteral.charAt(startIndex) == '0') {
radix = 8;
}
// create big integer
return new BigInteger(integerLiteral, radix);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public Object visitFloatLiteral(final GremlinParser.FloatLiteralContext ctx) {
final String floatLiteral = ctx.getText().toLowerCase();
// check suffix
final int lastCharIndex = floatLiteral.length() - 1;
final char lastCharacter = floatLiteral.charAt(lastCharIndex);
if (lastCharacter == 'm') {
// parse M/m or whatever which could be a parse exception
return new BigDecimal(floatLiteral.substring(0, lastCharIndex));
} else if (lastCharacter == 'f') {
// parse F/f suffix as Float
return new Float(ctx.getText());
} else if (lastCharacter == 'd'){
// parse D/d suffix as Double
return new Double(floatLiteral);
} else {
return new BigDecimal(floatLiteral);
}
}
/**
* {@inheritDoc}
*/
@Override
public Object visitBooleanLiteral(final GremlinParser.BooleanLiteralContext ctx) {
return Boolean.valueOf(ctx.getText());
}
/**
* {@inheritDoc}
*/
@Override
public Object visitDateLiteral(final GremlinParser.DateLiteralContext ctx) {
return DatetimeHelper.parse((String) visitStringLiteral(ctx.stringLiteral()));
}
/**
* {@inheritDoc}
*/
@Override
public Object visitNumericLiteral(final GremlinParser.NumericLiteralContext ctx) {
if (ctx.floatLiteral() != null) return visitFloatLiteral(ctx.floatLiteral());
if (ctx.integerLiteral() != null) return visitIntegerLiteral(ctx.integerLiteral());
throw new GremlinParserException("Invalid numeric");
}
/**
* {@inheritDoc}
*/
@Override
public Object visitStringBasedLiteral(final GremlinParser.StringBasedLiteralContext ctx) {
// Using Java string unescaping because it coincides with the Groovy rules:
// https://docs.oracle.com/javase/tutorial/java/data/characters.html
// http://groovy-lang.org/syntax.html#_escaping_special_characters
if (ctx.gremlinStringConstants() != null) {
return GremlinStringConstantsVisitor.instance().visitChildren(ctx);
}
if (ctx.NullLiteral() != null) {
return GremlinStringConstantsVisitor.instance().visitChildren(ctx);
}
return StringEscapeUtils.unescapeJava(stripQuotes(ctx.getText()));
}
/**
* {@inheritDoc}
*/
@Override
public Object visitStringLiteral(final GremlinParser.StringLiteralContext ctx) {
return StringEscapeUtils.unescapeJava(stripQuotes(ctx.getText()));
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTraversalToken(final GremlinParser.TraversalTokenContext ctx) {
return TraversalEnumParser.parseTraversalEnumFromContext(T.class, ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTraversalCardinality(final GremlinParser.TraversalCardinalityContext ctx) {
return TraversalEnumParser.parseTraversalEnumFromContext(VertexProperty.Cardinality.class, ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTraversalDirection(final GremlinParser.TraversalDirectionContext ctx) {
return TraversalEnumParser.parseTraversalDirectionFromContext(ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTraversalMerge(final GremlinParser.TraversalMergeContext ctx) {
return TraversalEnumParser.parseTraversalEnumFromContext(Merge.class, ctx);
}
/**
* {@inheritDoc}
*/
@Override
public Object visitTraversalPick(final GremlinParser.TraversalPickContext ctx) {
return TraversalEnumParser.parseTraversalEnumFromContext(Pick.class, ctx);
}
@Override
public Object visitTraversalStrategy(final GremlinParser.TraversalStrategyContext ctx) {
if (null == traversalStrategyVisitor)
traversalStrategyVisitor = new TraversalStrategyVisitor((DefaultGremlinBaseVisitor) antlr.tvisitor);
return traversalStrategyVisitor.visitTraversalStrategy(ctx);
}
/**
* Groovy range operator syntax is defined in http://groovy-lang.org/operators.html#_range_operator
* {@inheritDoc}
*/
@Override
public Object visitGenericLiteralRange(final GremlinParser.GenericLiteralRangeContext ctx) {
final int childIndexOfParameterStart = 0;
final int childIndexOfParameterEnd = 3;
final ParseTree startContext = ctx.getChild(childIndexOfParameterStart);
final ParseTree endContext = ctx.getChild(childIndexOfParameterEnd);
if (startContext instanceof GremlinParser.IntegerLiteralContext) {
// handle integer ranges.
final int start = Integer.valueOf(startContext.getText());
final int end = Integer.valueOf(endContext.getText());
return createIntegerRange(start, end, ctx.getText());
} else {
// handle string ranges.
final String start = stripQuotes(startContext.getText());
final String end = stripQuotes(endContext.getText());
return createStringRange(start, end, ctx.getText());
}
}
/**
* {@inheritDoc}
*/
@Override
public Object visitNullLiteral(final GremlinParser.NullLiteralContext ctx) {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Object visitNanLiteral(final GremlinParser.NanLiteralContext ctx) {
return Double.NaN;
}
/**
* {@inheritDoc}
*/
@Override
public Object visitInfLiteral(final GremlinParser.InfLiteralContext ctx) {
final String infLiteral = ctx.getText();
return infLiteral.charAt(0) == '-' ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
}
/**
* {@inheritDoc}
* Generic literal collection returns a list of {@code Object}
*/
@Override
public Object visitGenericLiteralCollection(final GremlinParser.GenericLiteralCollectionContext ctx) {
final List<Object> result = new ArrayList<>(ctx.getChildCount() / 2);
// first child is "[", so start from 2nd child
int childIndex = 1;
final int childCount = ctx.getChildCount();
if (childCount > 2) {
while (childIndex < childCount) {
result.add(visitGenericLiteral((GremlinParser.GenericLiteralContext) ctx.getChild(childIndex)));
// comma is also child, so we need skip it.
childIndex += 2;
}
}
return result;
}
}