
io.syndesis.integration.runtime.handlers.TemplateStepHandler 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.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.component.mustache.MustacheConstants;
import org.apache.camel.model.ProcessorDefinition;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.syndesis.common.model.DataShape;
import io.syndesis.common.model.DataShapeKinds;
import io.syndesis.common.model.action.Action;
import io.syndesis.common.model.integration.Step;
import io.syndesis.common.model.integration.StepKind;
import io.syndesis.common.util.StringConstants;
import io.syndesis.integration.runtime.IntegrationRouteBuilder;
import io.syndesis.integration.runtime.IntegrationStepHandler;
/**
* Handler for processing JSON messages and using their properties to populate a
* template written in recognised template languages.
*/
public class TemplateStepHandler implements IntegrationStepHandler, StringConstants {
private static final String TEMPLATE_PROPERTY = "template";
private static final String DOUBLE_OPEN_BRACE_PATTERN = "\\{\\{";
private static final String DOUBLE_CLOSE_BRACE_PATTERN = "\\}\\}";
private static final String MUSTACHE_OPEN_DELIMITER = "[[";
private static final String MUSTACHE_CLOSE_DELIMITER = "]]";
private final JsonToMapProcessor jsonToMapProcessor = new JsonToMapProcessor();
private final TextToJsonProcessor textToJsonProcessor = new TextToJsonProcessor();
@Override
public boolean canHandle(Step step) {
if (StepKind.template != step.getStepKind()) {
return false;
}
Action action = step.getAction().orElse(null);
if (action == null) {
return false;
}
DataShape inputDataShape = action.getInputDataShape().orElse(null);
if (inputDataShape == null) {
return false;
}
return DataShapeKinds.JSON_SCHEMA == inputDataShape.getKind();
}
@SuppressWarnings("PMD.AvoidReassigningParameters")
@Override
public Optional> handle(Step step, ProcessorDefinition> route, IntegrationRouteBuilder builder, String flowIndex, String stepIndex) {
ObjectHelper.notNull(route, "route");
Map properties = step.getConfiguredProperties();
String template = properties.get(TEMPLATE_PROPERTY);
/*
* To avoid creating a temporary file, we use the mustache template header
* to override the camel-mustache resource uri. This is more efficient but has
* a downside.
*
* Once returned, the route (ProcessorDefinition) is resolved. This resolution
* looks for any properties delimited by {{ and }}, which is the default syntax
* for mustache properties. Thus, resolution of the mustache properties is
* incorrectly attempted and unsurprisingly fails since these properties are
* meant for mustache and not camel.
*
* Changing the mustache delimiter patterns avoids this problem and allows the
* properties to be resolved later by mustache.
*/
template = template.replaceAll(DOUBLE_OPEN_BRACE_PATTERN, MUSTACHE_OPEN_DELIMITER);
template = template.replaceAll(DOUBLE_CLOSE_BRACE_PATTERN, MUSTACHE_CLOSE_DELIMITER);
//
// Convert the exchange's in message from JSON
// to a HashMap since this is required by camel modules
//
route = route.process(jsonToMapProcessor);
//
// Apply the template to the mustache header property
// Then add to the route path
//
route.setHeader(MustacheConstants.MUSTACHE_TEMPLATE).constant(template);
//
// Encode the delimiters since they are applied as URI query parameters
//
try {
String id = flowIndex + HYPHEN + stepIndex;
String uri = "mustache" + COLON + id;
Map params = new HashMap<>();
params.put("startDelimiter", MUSTACHE_OPEN_DELIMITER);
params.put("endDelimiter", MUSTACHE_CLOSE_DELIMITER);
uri = URISupport.appendParametersToURI(uri, params);
route = route.to(uri);
//
// Post-process the output exchange into JSON
// so it will be available as part of a JSON object
//
route = route.process(textToJsonProcessor);
return Optional.ofNullable(route);
} catch (UnsupportedEncodingException | URISyntaxException e) {
throw new IllegalStateException(e);
}
}
/**
* Processor that is designed to jump in front of the template routing.
* The handler accepts messages in JSON but the mustache route requires
* all properties to be available in a {@link Map}. Thus, this parses the JSON
* and converts it to the appropriate {@link Map}.
*/
public static class JsonToMapProcessor implements Processor {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonToMapProcessor.class);
private static final String BODY_PREFIX = "body.";
private static final ObjectMapper MAPPER = new ObjectMapper();
/*
* If the key starts with the word "body" which it will if data-mapped
* via the template then remove it since the map will be stored AS the
* body of the exchange which is then inserted into a mustache map
* keyed with "body".
*/
private void refactorKeys(Map map) {
ArrayList keys = new ArrayList(map.keySet());
keys.stream().forEach(key -> {
Object value = map.remove(key);
if (key.startsWith(BODY_PREFIX)) {
key = key.substring(BODY_PREFIX.length());
LOGGER.debug("Refactored Key: " + key);
}
map.put(key, value);
});
}
@SuppressWarnings( "unchecked" )
@Override
public void process(Exchange exchange) throws Exception {
Object body = exchange.getIn().getBody();
LOGGER.debug("Exchange In Body: " + body.toString());
try {
// Map the json body to a Map
Map map = (Map) MAPPER.readValue(body.toString(), HashMap.class);
// Refactor the keys of the map to remove any "body." notation since this
// is no longer required during actual exchange processing
refactorKeys(map);
exchange.getIn().setBody(map);
} catch (Exception ex) {
throw new IllegalStateException("Failed to pre-process the TemplateStep's input message (expecting JSON)", ex);
}
}
}
public static class TextToJsonProcessor implements Processor {
private static final String MESSAGE_ATTRIBUTE = "message";
private static final Logger LOGGER = LoggerFactory.getLogger(TextToJsonProcessor.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void process(Exchange exchange) throws Exception {
Object body = exchange.getIn().getBody();
LOGGER.debug("Exchange In Body: " + body.toString());
try {
ObjectNode node = MAPPER.createObjectNode();
node.put(MESSAGE_ATTRIBUTE, body.toString());
String newBody = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(node);
exchange.getIn().setBody(newBody);
LOGGER.debug("New Exchange In Body: " + newBody);
} catch (Exception ex) {
throw new IllegalStateException("Failed to post-process the TemplateStep's message into JSON", ex);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy