/*
* Licensed to CRATE Technology GmbH ("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;
import io.crate.analyze.expressions.ExpressionToStringVisitor;
import io.crate.metadata.FulltextAnalyzerResolver;
import io.crate.sql.tree.*;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.settings.Settings;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static io.crate.analyze.CreateAnalyzerAnalyzedStatement.getSettingsKey;
class CreateAnalyzerStatementAnalyzer
extends DefaultTraversalVisitor<CreateAnalyzerAnalyzedStatement, CreateAnalyzerStatementAnalyzer.Context> {
private final FulltextAnalyzerResolver fulltextAnalyzerResolver;
CreateAnalyzerStatementAnalyzer(FulltextAnalyzerResolver fulltextAnalyzerResolver) {
this.fulltextAnalyzerResolver = fulltextAnalyzerResolver;
}
public CreateAnalyzerAnalyzedStatement analyze(Node node, Analysis analysis) {
return super.process(node, new Context(analysis));
}
static class Context {
Analysis analysis;
CreateAnalyzerAnalyzedStatement statement;
public Context(Analysis analysis) {
this.analysis = analysis;
}
}
@Override
public CreateAnalyzerAnalyzedStatement visitCreateAnalyzer(CreateAnalyzer node, Context context) {
context.statement = new CreateAnalyzerAnalyzedStatement(fulltextAnalyzerResolver);
context.statement.ident(node.ident());
if (node.isExtending()) {
context.statement.extendedAnalyzer(node.extendedAnalyzer().get());
}
for (AnalyzerElement element : node.elements()) {
process(element, context);
}
return context.statement;
}
@Override
public CreateAnalyzerAnalyzedStatement visitTokenizer(Tokenizer tokenizer, Context context) {
String name = tokenizer.ident();
Optional<io.crate.sql.tree.GenericProperties> properties = tokenizer.properties();
if (!properties.isPresent()) {
// use a builtin tokenizer without parameters
// validate
if (!context.statement.analyzerService().hasBuiltInTokenizer(name)) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Non-existing tokenizer '%s'", name));
}
// build
context.statement.tokenDefinition(name, Settings.EMPTY);
} else {
// validate
if (!context.statement.analyzerService().hasBuiltInTokenizer(name)) {
// type mandatory
String evaluatedType = extractType(properties.get(), context.analysis.parameterContext());
if (!context.statement.analyzerService().hasBuiltInTokenizer(evaluatedType)) {
// only builtin tokenizers can be extended, for now
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Non-existing built-in tokenizer type '%s'", evaluatedType));
}
} else {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "tokenizer name '%s' is reserved", name));
}
// build
// transform name as tokenizer is not publicly available
name = String.format(Locale.ENGLISH, "%s_%s", context.statement.ident(), name);
Settings.Builder builder = Settings.builder();
for (Map.Entry<String, Expression> tokenizerProperty : properties.get().properties().entrySet()) {
GenericPropertiesConverter.genericPropertyToSetting(builder,
getSettingsKey("index.analysis.tokenizer.%s.%s", name, tokenizerProperty.getKey()),
tokenizerProperty.getValue(),
context.analysis.parameterContext().parameters());
}
context.statement.tokenDefinition(name, builder.build());
}
return null;
}
@Override
public CreateAnalyzerAnalyzedStatement visitGenericProperty(GenericProperty property, Context context) {
GenericPropertiesConverter.genericPropertyToSetting(context.statement.genericAnalyzerSettingsBuilder(),
getSettingsKey("index.analysis.analyzer.%s.%s", context.statement.ident(), property.key()),
property.value(),
context.analysis.parameterContext().parameters()
);
return null;
}
@Override
public CreateAnalyzerAnalyzedStatement visitTokenFilters(TokenFilters tokenFilters, Context context) {
for (NamedProperties tokenFilterNode : tokenFilters.tokenFilters()) {
String name = tokenFilterNode.ident();
Optional<GenericProperties> properties = tokenFilterNode.properties();
// use a builtin token-filter without parameters
if (!properties.isPresent()) {
// validate
if (!context.statement.analyzerService().hasBuiltInTokenFilter(name)) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Non-existing built-in token-filter '%s'", name));
}
// build
context.statement.addTokenFilter(name, Settings.EMPTY);
} else {
// validate
GenericProperties filterProperties = properties.get();
if (!context.statement.analyzerService().hasBuiltInTokenFilter(name)) {
// type mandatory when name is not a builtin filter
String evaluatedType = extractType(filterProperties, context.analysis.parameterContext());
if (!context.statement.analyzerService().hasBuiltInTokenFilter(evaluatedType)) {
// only builtin token-filters can be extended, for now
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Non-existing built-in token-filter type '%s'", evaluatedType));
}
} else {
if (filterProperties.get("type") != null) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"token-filter name '%s' is reserved, 'type' property forbidden here", name));
}
filterProperties.add(new GenericProperty("type", new StringLiteral(name)));
}
// build
// transform name as token-filter is not publicly available
name = String.format(Locale.ENGLISH, "%s_%s", context.statement.ident(), name);
Settings.Builder builder = Settings.builder();
for (Map.Entry<String, Expression> tokenFilterProperty : filterProperties.properties().entrySet()) {
GenericPropertiesConverter.genericPropertyToSetting(builder,
getSettingsKey("index.analysis.filter.%s.%s", name, tokenFilterProperty.getKey()),
tokenFilterProperty.getValue(),
context.analysis.parameterContext().parameters());
}
context.statement.addTokenFilter(name, builder.build());
}
}
return null;
}
@Override
public CreateAnalyzerAnalyzedStatement visitCharFilters(CharFilters charFilters, Context context) {
for (NamedProperties charFilterNode : charFilters.charFilters()) {
String name = charFilterNode.ident();
Optional<GenericProperties> properties = charFilterNode.properties();
// use a builtin char-filter without parameters
if (!properties.isPresent()) {
// validate
if (!context.statement.analyzerService().hasBuiltInCharFilter(name)) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Non-existing built-in char-filter '%s'", name));
}
validateCharFilterProperties(name, properties.orElse(null));
// build
context.statement.addCharFilter(name, Settings.EMPTY);
} else {
String type = extractType(properties.get(), context.analysis.parameterContext());
if (!context.statement.analyzerService().hasBuiltInCharFilter(type)) {
// only builtin char-filters can be extended, for now
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Non-existing built-in char-filter" +
" type '%s'", type));
}
validateCharFilterProperties(type, properties.get());
// build
// transform name as char-filter is not publicly available
name = String.format(Locale.ENGLISH, "%s_%s", context.statement.ident(), name);
Settings.Builder builder = Settings.builder();
for (Map.Entry<String, Expression> charFilterProperty : properties.get().properties().entrySet()) {
GenericPropertiesConverter.genericPropertyToSetting(builder,
getSettingsKey("index.analysis.char_filter.%s.%s", name, charFilterProperty.getKey()),
charFilterProperty.getValue(),
context.analysis.parameterContext().parameters());
}
context.statement.addCharFilter(name, builder.build());
}
}
return null;
}
/**
* Validate and process `type` property
*/
private String extractType(GenericProperties properties, ParameterContext parameterContext) {
Expression expression = properties.get("type");
if (expression == null) {
throw new IllegalArgumentException("'type' property missing");
}
if (expression instanceof ArrayLiteral) {
throw new IllegalArgumentException("'type' property is invalid");
}
return ExpressionToStringVisitor.convert(expression, parameterContext.parameters());
}
private static void validateCharFilterProperties(String type, @Nullable GenericProperties properties) {
if (properties == null && !type.equals("html_strip")) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"CHAR_FILTER of type '%s' needs additional parameters", type));
}
}
}