All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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