/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.analyze.symbol.format;
import io.crate.analyze.relations.RelationPrinter;
import io.crate.analyze.symbol.*;
import io.crate.metadata.*;
import io.crate.operation.operator.any.AnyOperator;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Singleton;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Locale;
import static io.crate.analyze.symbol.format.SymbolPrinter.Strings.*;
@Singleton
public class SymbolPrinter {
public static final SymbolPrinter INSTANCE = new SymbolPrinter(null);
private static final FunctionFormatSpec SIMPLE_FUNCTION_FORMAT_SPEC = new FunctionFormatSpec() {
@Override
public boolean formatArgs(Function function) {
return true;
}
@Override
public String beforeArgs(Function function) {
return function.info().ident().name() + PAREN_OPEN;
}
@Override
public String afterArgs(Function function) {
return PAREN_CLOSE;
}
};
private static final OperatorFormatSpec SIMPLE_OPERATOR_FORMAT_SPEC = new OperatorFormatSpec() {
@Override
public String operator(Function function) {
assert function.info().ident().name().startsWith("op_") :
"function.info().ident().name() must start with 'op_'";
return function.info().ident().name().substring(3).toUpperCase(Locale.ENGLISH);
}
};
public static final com.google.common.base.Function<? super Symbol, String> FUNCTION = new com.google.common.base.Function<Symbol, String>() {
@Nullable
@Override
public String apply(@Nullable Symbol input) {
return input == null ? null : INSTANCE.printSimple(input);
}
};
public enum Style {
SIMPLE(SymbolPrinterContext.DEFAULT_MAX_DEPTH, SymbolPrinterContext.DEFAULT_FULL_QUALIFIED, SymbolPrinterContext.DEFAULT_FAIL_IF_MAX_DEPTH_REACHED),
PARSEABLE(100, true, true),
FULL_QUALIFIED(SymbolPrinterContext.DEFAULT_MAX_DEPTH, true, false),
PARSEABLE_NOT_QUALIFIED(100, false, true);
private final int maxDepth;
private final boolean fullQualified;
private final boolean failIfMaxDepthReached;
Style(int maxDepth, boolean fullQualified, boolean failIfMaxDepthReached) {
this.maxDepth = maxDepth;
this.fullQualified = fullQualified;
this.failIfMaxDepthReached = failIfMaxDepthReached;
}
SymbolPrinterContext context() {
return new SymbolPrinterContext(maxDepth, fullQualified, failIfMaxDepthReached);
}
}
private final SymbolPrintVisitor symbolPrintVisitor;
@Inject
public SymbolPrinter(@Nullable Functions functions) {
this.symbolPrintVisitor = new SymbolPrintVisitor(functions);
}
public String printSimple(Symbol symbol) {
return print(symbol, Style.SIMPLE);
}
public String printFullQualified(Symbol symbol) {
return print(symbol, Style.FULL_QUALIFIED);
}
/**
* format the given symbol
*
* @param symbol the symbol to format
* @param maxDepth the max depth to print, if maxDepth is reached, "..." is printed for the rest
* @param fullQualified if references should be fully qualified (contain schema and table name)
*/
public String print(Symbol symbol, int maxDepth, boolean fullQualified, boolean failIfMaxDepthReached) {
SymbolPrinterContext context = new SymbolPrinterContext(maxDepth, fullQualified, failIfMaxDepthReached);
symbolPrintVisitor.process(symbol, context);
return context.formatted();
}
/**
* format a symbol with the given style
*/
public String print(Symbol symbol, Style formatStyle) {
SymbolPrinterContext context = formatStyle.context();
symbolPrintVisitor.process(symbol, context);
return context.formatted();
}
private static final class SymbolPrintVisitor extends SymbolVisitor<SymbolPrinterContext, Void> {
@Nullable
private final Functions functions;
private SymbolPrintVisitor(@Nullable Functions functions) {
this.functions = functions;
}
@Override
protected Void visitSymbol(Symbol symbol, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
context.builder.append(symbol.toString());
return null;
}
@Override
public Void visitAggregation(Aggregation symbol, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
context.builder.append(symbol.functionIdent().name()).append(PAREN_OPEN);
printArgs(symbol.inputs(), context);
context.builder.append(PAREN_CLOSE);
return null;
}
@Override
public Void visitFunction(Function function, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
// handle special functions
String functionName = function.info().ident().name();
if (functionName.startsWith(AnyOperator.OPERATOR_PREFIX)) {
printAnyOperator(function, context);
} else {
printGenericFunction(function, context);
}
return null;
}
private void printGenericFunction(Function function, SymbolPrinterContext context) {
FunctionFormatSpec functionFormatSpec = null;
OperatorFormatSpec operatorFormatSpec = null;
FunctionIdent ident = function.info().ident();
if (functions != null) {
FunctionImplementation impl = functions.getQualified(ident);
if (impl instanceof FunctionFormatSpec) {
functionFormatSpec = (FunctionFormatSpec) impl;
} else if (impl instanceof OperatorFormatSpec) {
operatorFormatSpec = (OperatorFormatSpec) impl;
}
} else if (ident.name().startsWith("op_")) {
operatorFormatSpec = SIMPLE_OPERATOR_FORMAT_SPEC;
}
if (operatorFormatSpec != null) {
printOperator(function, operatorFormatSpec, context);
} else {
if (functionFormatSpec == null) {
functionFormatSpec = SIMPLE_FUNCTION_FORMAT_SPEC;
}
printFunction(function, functionFormatSpec, context);
}
}
private void printAnyOperator(Function function, SymbolPrinterContext context) {
List<Symbol> args = function.arguments();
assert args.size() == 2 : "function's number of arguments must be 2";
context.builder.append(PAREN_OPEN); // wrap operator in parens to ensure precedence
context.down();
process(args.get(0), context);
context.up();
// print operator
String operatorName = anyOperatorName(function.info().ident().name());
context.builder
.append(WS)
.append(operatorName)
.append(WS);
context.builder.append(ANY).append(PAREN_OPEN);
context.down();
process(args.get(1), context);
context.up();
context.builder.append(PAREN_CLOSE)
.append(PAREN_CLOSE);
}
private String anyOperatorName(String functionName) {
// handles NOT_LIKE -> NOT LIKE
return functionName.substring(4).replace('_', ' ').toUpperCase(Locale.ENGLISH);
}
@Override
public Void visitReference(Reference symbol, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
if (context.isFullQualified()) {
context.builder.append(symbol.ident().tableIdent().sqlFqn())
.append(DOT);
}
context.builder.append(symbol.ident().columnIdent().quotedOutputName());
return null;
}
@Override
public Void visitDynamicReference(DynamicReference symbol, SymbolPrinterContext context) {
return visitReference(symbol, context);
}
@Override
public Void visitField(Field field, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
if (context.isFullQualified()) {
context.builder.append(RelationPrinter.INSTANCE.process(field.relation(), null))
.append(DOT);
}
if (field.path() instanceof ColumnIdent) {
context.builder.append(((ColumnIdent) field.path()).quotedOutputName());
} else {
context.builder.append(field.path().outputName());
}
return null;
}
@Override
public Void visitInputColumn(InputColumn inputColumn, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
context.builder.append("INPUT(")
.append(inputColumn.index())
.append(")");
return null;
}
@Override
public Void visitFetchReference(FetchReference fetchReference, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
context.builder.append("FETCH(");
process(fetchReference.fetchId(), context);
context.builder.append(", ");
process(fetchReference.ref(), context);
context.builder.append(")");
return null;
}
@Override
public Void visitLiteral(Literal symbol, SymbolPrinterContext context) {
if (context.verifyMaxDepthReached()) {
return null;
}
LiteralValueFormatter.INSTANCE.format(symbol.value(), context.builder);
return null;
}
private void printOperator(Function function, OperatorFormatSpec formatter, SymbolPrinterContext context) {
int numArgs = function.arguments().size();
context.builder.append(PAREN_OPEN);
switch (numArgs) {
case 1:
context.builder.append(formatter.operator(function))
.append(" ");
context.down();
function.arguments().get(0).accept(this, context);
context.up();
break;
case 2:
context.down();
function.arguments().get(0).accept(this, context);
context.up();
context.builder.append(WS)
.append(formatter.operator(function))
.append(WS);
context.down();
function.arguments().get(1).accept(this, context);
context.up();
break;
default:
throw new UnsupportedOperationException("cannot format operators with more than 2 operands");
}
context.builder.append(PAREN_CLOSE);
}
private void printFunction(Function function, FunctionFormatSpec formatter, SymbolPrinterContext context) {
context.builder.append(formatter.beforeArgs(function));
if (formatter.formatArgs(function)) {
printArgs(function.arguments(), context);
}
context.builder.append(formatter.afterArgs(function));
}
private void printArgs(List<Symbol> args, SymbolPrinterContext context) {
context.down();
try {
for (int i = 0, size = args.size(); i < size; i++) {
args.get(i).accept(this, context);
if (i < size - 1) {
context.builder.append(COMMA).append(WS);
}
}
} finally {
context.up();
}
}
}
public static class Strings {
public static final String PAREN_OPEN = "(";
public static final String PAREN_CLOSE = ")";
public static final String COMMA = ",";
public static final String ELLIPSIS = "...";
public static final String NULL_LOWER = "null";
public static final String WS = " ";
public static final String DOT = ".";
public static final String ANY = "ANY";
}
}