io.syndesis.integration.runtime.handlers.SplitStepHandler Maven / Gradle / Ivy
/*
* Copyright (C) 2016 Red Hat, Inc.
*
* Licensed 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 io.syndesis.integration.runtime.handlers;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Optional;
import com.fasterxml.jackson.databind.JsonNode;
import io.syndesis.common.model.integration.Step;
import io.syndesis.common.model.integration.StepKind;
import io.syndesis.common.util.SyndesisServerException;
import io.syndesis.common.util.json.JsonUtils;
import io.syndesis.integration.runtime.IntegrationRouteBuilder;
import io.syndesis.integration.runtime.IntegrationStepHandler;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.TypeConverter;
import org.apache.camel.builder.Builder;
import org.apache.camel.model.ProcessorDefinition;
import org.apache.camel.spi.Language;
import org.apache.camel.support.ExpressionAdapter;
import org.apache.camel.util.ObjectHelper;
public class SplitStepHandler implements IntegrationStepHandler {
/* Types that need conversion to String in order to perform a split operation */
private enum AutoConvertTypes {
INPUT_STREAM(InputStream.class),
REMOTE_FILE("org.apache.camel.component.file.remote.RemoteFile") // ftp connector
;
private final Class> type;
private final String typeName;
AutoConvertTypes(Class> type) {
this.type = type;
this.typeName = type.getName();
}
AutoConvertTypes(String typeName) {
this.type = null;
this.typeName = typeName;
}
boolean isInstance(Object value) {
if (type != null) {
return type.isInstance(value);
}
return typeName.equals(value.getClass().getName());
}
}
@Override
public boolean canHandle(Step step) {
return StepKind.split == step.getStepKind();
}
@SuppressWarnings({"PMD.AvoidReassigningParameters", "PMD.AvoidDeeplyNestedIfStmts"})
@Override
public Optional> handle(Step step, ProcessorDefinition> route, IntegrationRouteBuilder builder, String flowIndex, String stepIndex) {
ObjectHelper.notNull(route, "route");
SplitExpression splitExpression;
String languageName = step.getConfiguredProperties().get("language");
String expressionDefinition = step.getConfiguredProperties().get("expression");
if (step.hasUnifiedJsonSchemaOutputShape()) {
// we have to split the nested unified body property by default.
splitExpression = new SplitExpression(new UnifiedJsonBodyExpression(Builder.body()));
} else if (ObjectHelper.isNotEmpty(expressionDefinition)) {
if (ObjectHelper.isEmpty(languageName)) {
languageName = "simple";
}
// A small hack until https://issues.apache.org/jira/browse/CAMEL-12079
// gets fixed so we can support the 'bean::method' annotation as done by
// Function step definition
if ("bean".equals(languageName) && expressionDefinition.contains("::")) {
expressionDefinition = expressionDefinition.replace("::", "?method=");
}
final Language language = builder.getContext().resolveLanguage(languageName);
splitExpression = new SplitExpression(language.createExpression(expressionDefinition));
} else {
splitExpression = new SplitExpression(Builder.body());
}
AggregateStepHandler.AggregationOption aggregation = Optional.ofNullable(step.getConfiguredProperties().get("aggregationStrategy"))
.map(AggregateStepHandler.AggregationOption::valueOf)
.orElse(AggregateStepHandler.AggregationOption.body);
route = route.split(splitExpression).aggregationStrategy(aggregation.getStrategy(step.getConfiguredProperties()));
return Optional.of(route);
}
/**
* Split expression that is aware of special list of Json beans representation provided by some connectors such as
* SQL. Also handles Json array String representation and splits its array elements accordingly.
*
* Expression receives a delegate expression that usually evaluates the part of the body or header that should be split. By
* default this is a simple body expression.
*
* When delegate expression evaluates to something else that a Json array or list of Json beans nothing is performed on top of
* the delegate expression.
*/
private static class SplitExpression extends ExpressionAdapter {
private final Expression delegate;
SplitExpression(Expression delegate) {
this.delegate = delegate;
}
@Override
public Object evaluate(Exchange exchange) {
try {
Object value = convert(delegate.evaluate(exchange, Object.class), exchange);
if (value instanceof String && JsonUtils.isJson(value.toString())) {
JsonNode json = JsonUtils.reader().readTree(value.toString());
if (json.isArray()) {
return JsonUtils.arrayToJsonBeans(json);
}
}
return value;
} catch (IOException e) {
throw SyndesisServerException.launderThrowable(e);
}
}
}
/**
* Expression extracts body property from unified Json schema typed input. The unified Json holds the actual body in
* a nested property. This property is extracted and set as expression result so follow up expressions can operate on the body.
*
* Expression receives a delegate expression that usually evaluates the part of the body or header that should be
* treated as unified Json. By default this is a simple body expression.
*/
private static class UnifiedJsonBodyExpression extends ExpressionAdapter {
private final Expression delegate;
UnifiedJsonBodyExpression(Expression delegate) {
this.delegate = delegate;
}
@Override
public Object evaluate(Exchange exchange) {
try {
Object value = convert(delegate.evaluate(exchange, Object.class), exchange);
if (value instanceof String && JsonUtils.isJson(value.toString())) {
JsonNode json = JsonUtils.reader().readTree(value.toString());
JsonNode body = json.get("body");
if (body != null) {
return JsonUtils.writer().writeValueAsString(body);
}
}
return value;
} catch (IOException e) {
throw SyndesisServerException.launderThrowable(e);
}
}
}
/**
* Helper tries to convert given value to a String representation that can be split. For instance a remote file object
* with Json array as content is converted to String and then split using the elements in that Json array.
*
* Some types are already supported by Camel's splitter such as List or Scanner. Do not touch these types and skip conversion.
* @param value the value to convert.
* @param exchange the current exchange.
* @return converted value or original value itself if conversion failed or is not applicable.
*/
private static Object convert(Object value, Exchange exchange) {
if (Arrays.stream(AutoConvertTypes.values()).noneMatch(type -> type.isInstance(value))) {
return value;
}
TypeConverter converter = exchange.getContext().getTypeConverter();
String answer = converter.convertTo(String.class, exchange, value);
if (answer != null) {
return answer;
}
answer = converter.tryConvertTo(String.class, exchange, value);
if (answer != null) {
return answer;
}
return value;
}
}