/* * Copyright 2016 Red Hat, Inc. * <p> * Red Hat 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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.fabric8.funktion.runtime; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.funktion.model.Flow; import io.fabric8.funktion.model.Funktion; import io.fabric8.funktion.model.Funktions; import io.fabric8.funktion.model.steps.*; import io.fabric8.funktion.runtime.designer.SingleMessageRoutePolicyFactory; import io.fabric8.funktion.support.Strings; import org.apache.camel.Expression; import org.apache.camel.LoggingLevel; import org.apache.camel.Predicate; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.http4.HttpEndpoint; import org.apache.camel.component.servlet.CamelHttpTransportServlet; import org.apache.camel.model.ChoiceDefinition; import org.apache.camel.model.FilterDefinition; import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.SplitDefinition; import org.apache.camel.model.ThrottleDefinition; import org.apache.camel.spi.Language; import org.apache.camel.spring.boot.CamelSpringBootApplicationController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import static io.fabric8.funktion.model.Funktions.createObjectMapper; import static io.fabric8.funktion.support.Lists.notNullList; /** * A Camel {@link RouteBuilder} which maps the Funktion rules to Camel routes */ @Component public class FunktionRouteBuilder extends RouteBuilder { private static final transient Logger LOG = LoggerFactory.getLogger(FunktionRouteBuilder.class); // use servlet to map from http trigger to use Spring Boot servlet engine private static final String DEFAULT_TRIGGER_URL = "http://0.0.0.0:8080/"; private static final String DEFAULT_HTTP_ENDPOINT_PREFIX = "servlet:funktion"; private final Set<String> localHosts = new HashSet<>(Arrays.asList("localhost", "0.0.0.0", "127.0.0.1")); private FunktionConfigurationProperties config = new FunktionConfigurationProperties(); public FunktionConfigurationProperties getConfig() { return config; } public void setConfig(FunktionConfigurationProperties config) { this.config = config; } // must have a main method spring-boot can run public static void main(String[] args) { ApplicationContext applicationContext = new SpringApplication(FunktionRouteBuilder.class).run(args); CamelSpringBootApplicationController ctx = applicationContext.getBean(CamelSpringBootApplicationController.class); ctx.run(); } private static String replacePrefix(String text, String prefix, String replacement) { if (text.startsWith(prefix)) { return replacement + text.substring(prefix.length()); } return text; } @Bean ServletRegistrationBean camelServlet() { // use a @Bean to register the Camel servlet which we need to do // because we want to use the camel-servlet component for the Camel REST service ServletRegistrationBean mapping = new ServletRegistrationBean(); mapping.setName("CamelServlet"); mapping.setLoadOnStartup(1); mapping.setServlet(new CamelHttpTransportServlet()); mapping.addUrlMappings("/camel/*"); return mapping; } @Override public void configure() throws Exception { // inject auto configured properties if there is any Set<FunktionConfigurationProperties> props = getContext().getRegistry().findByType(FunktionConfigurationProperties.class); if (props != null && props.size() == 1) { config = props.iterator().next(); } Funktion funktion = loadFunktion(); int idx = 0; List<Flow> rules = funktion.getFlows(); for (Flow rule : rules) { configureRule(rule, idx++); } LOG.info("Configured {} flow(s)", idx); try { if ("yaml".equals(config.getDumpFlowModel())) { StringWriter sw = new StringWriter(); createObjectMapper().writeValue(sw, rules); LOG.info("============================================================"); LOG.info("\n" + sw.toString()); LOG.info("============================================================"); } else if ("json".equals(config.getDumpFlowModel())) { StringWriter sw = new StringWriter(); ObjectMapper mapper = new ObjectMapper(); mapper.writerWithDefaultPrettyPrinter().writeValue(sw, rules); LOG.info("============================================================"); LOG.info("\n" + sw.toString()); LOG.info("============================================================"); } } catch (Exception e) { LOG.warn("Cannot dump flow model due " + e.getMessage() + ". This exception is ignored."); } } protected Funktion loadFunktion() throws IOException { return Funktions.load(); } protected void configureRule(Flow flow, int funktionIndex) throws MalformedURLException { if (flow.isTraceEnabled() || (config.getTrace() != null && config.getTrace())) { getContext().setTracing(true); } StringBuilder message = new StringBuilder("FLOW "); String name = flow.getName(); if (Strings.isEmpty(name)) { name = "flow" + (funktionIndex + 1); flow.setName(name); } RouteDefinition route = null; List<Step> steps = flow.getSteps(); int validSteps = 0; if (steps != null) { for (Step item : steps) { if (item instanceof Function) { Function function = (Function) item; String functionName = function.getName(); if (!Strings.isEmpty(functionName)) { if (route != null) { route.to("json:marshal"); } String method = null; int idx = functionName.indexOf("::"); if (idx > 0) { method = functionName.substring(idx + 2); functionName = functionName.substring(0, idx); } String uri = "class:" + functionName; if (method != null) { uri += "?method=" + method; } route = fromOrTo(route, name, uri, message); message.append(functionName); if (method != null) { message.append("." + method + "()"); } else { message.append(".main()"); } validSteps++; } } else if (item instanceof Endpoint) { Endpoint invokeEndpoint = (Endpoint) item; String uri = invokeEndpoint.getUri(); if (!Strings.isEmpty(uri)) { if (route != null) { route.to("json:marshal"); } route = fromOrTo(route, name, uri, message); message.append(uri); validSteps++; } } else { addStep(route, item); validSteps++; } } } if (route == null || validSteps == 0) { throw new IllegalStateException("No valid steps! Invalid flow " + flow); } if (flow.isLogResultEnabled() || (config.getLogResult() != null && config.getLogResult())) { String chain = "log:" + name + "?showStreams=true"; route.to(chain); message.append(" => "); message.append(chain); validSteps++; } LOG.info(message.toString()); // TODO: Camel 2.19 has functionality OOTB // https://github.com/apache/camel/blob/master/components/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelConfigurationProperties.java#L146 if (flow.isSingleMessageModeEnabled() || (config.getSingleMessageMode() != null && config.getSingleMessageMode())) { LOG.info("Enabling single message mode so that only one message is consumed for Design Mode"); getContext().addRoutePolicyFactory(new SingleMessageRoutePolicyFactory()); } } protected void addSteps(ProcessorDefinition route, Iterable<Step> steps) { if (route != null && steps != null) { for (Step item : steps) { route = addStep(route, item); } } } private ProcessorDefinition addStep(ProcessorDefinition route, Step item) { assertRouteNotNull(route, item); if (item instanceof Function) { Function function = (Function) item; String functionName = function.getName(); if (!Strings.isEmpty(functionName)) { route.to("json:marshal"); String method = null; int idx = functionName.indexOf("::"); if (idx > 0) { method = functionName.substring(idx + 2); functionName = functionName.substring(0, idx); } String uri = "class:" + functionName; if (method != null) { uri += "?method=" + method; } uri = convertEndpointURI(uri); route = route.to(uri); } } else if (item instanceof Endpoint) { Endpoint invokeEndpoint = (Endpoint) item; String uri = invokeEndpoint.getUri(); if (!Strings.isEmpty(uri)) { uri = convertEndpointURI(uri); route = route.to("json:marshal"); route = route.to(uri); } } else if (item instanceof SetBody) { SetBody step = (SetBody) item; route.setBody(constant(step.getBody())); } else if (item instanceof Throttle) { Throttle step = (Throttle) item; ThrottleDefinition throttle = route.throttle(step.getMaximumRequests()); Long period = step.getPeriodMillis(); if (period != null) { throttle.timePeriodMillis(period); } addSteps(throttle, step.getSteps()); } else if (item instanceof SetHeaders) { SetHeaders step = (SetHeaders) item; Map<String, Object> headers = step.getHeaders(); if (headers != null) { Set<Map.Entry<String, Object>> entries = headers.entrySet(); for (Map.Entry<String, Object> entry : entries) { String key = entry.getKey(); Object value = entry.getValue(); route.setHeader(key, constant(value)); } } } else if (item instanceof Filter) { Filter step = (Filter) item; Predicate predicate = getMandatoryPredicate(step, step.getExpression(), step.getLanguage()); FilterDefinition filter = route.filter(predicate); addSteps(filter, step.getSteps()); } else if (item instanceof Split) { Split step = (Split) item; Expression expression = getMandatoryExpression(step, step.getExpression(), step.getLanguage()); SplitDefinition split = route.split(expression); addSteps(split, step.getSteps()); } else if (item instanceof Choice) { Choice step = (Choice) item; ChoiceDefinition choice = route.choice(); List<Filter> filters = notNullList(step.getFilters()); for (Filter filter : filters) { Predicate predicate = getMandatoryPredicate(filter, filter.getExpression(), filter.getLanguage()); ChoiceDefinition when = choice.when(predicate); addSteps(when, filter.getSteps()); } Otherwise otherwiseStep = step.getOtherwise(); if (otherwiseStep != null) { List<Step> otherwiseSteps = notNullList(otherwiseStep.getSteps()); if (!otherwiseSteps.isEmpty()) { ChoiceDefinition otherwise = choice.otherwise(); addSteps(otherwise, otherwiseSteps); } } } else if (item instanceof Log) { Log step = (Log) item; LoggingLevel loggingLevel = LoggingLevel.INFO; if (step.getLoggingLevel() != null) { loggingLevel = LoggingLevel.valueOf(step.getLoggingLevel()); } route.log(loggingLevel, step.getLogger(), step.getMarker(), step.getMessage()); } else { throw new IllegalStateException("Unknown step kind: " + item + " of class: " + item.getClass().getName()); } return route; } protected Predicate getMandatoryPredicate(Step step, String expression, String language) { Objects.requireNonNull(expression, "No expression specified for step " + step); Language jsonpath = getLanguage(language); Predicate answer = jsonpath.createPredicate(expression); Objects.requireNonNull(answer, "No predicate created from: " + expression); return answer; } protected Expression getMandatoryExpression(Step step, String expression, String language) { Objects.requireNonNull(expression, "No expression specified for step " + step); Language jsonpath = getLanguage(language); Expression answer = jsonpath.createExpression(expression); Objects.requireNonNull(answer, "No expression created from: " + expression); return answer; } protected Language getLanguage(String language) { // use jsonpath as default String languageName = language != null && !language.isEmpty() ? language : "jsonpath"; Language answer = getContext().resolveLanguage(languageName); Objects.requireNonNull(answer, "The language `" + languageName + "` cound not be resolved!"); return answer; } protected void assertRouteNotNull(ProcessorDefinition route, Step item) { if (route == null) { throw new IllegalArgumentException("You cannot use a " + item.getKind() + " step before you have started a flow with an endpoint or function!"); } } protected RouteDefinition fromOrTo(RouteDefinition route, String name, String uri, StringBuilder message) { if (route == null) { String trigger = uri; if (Strings.isEmpty(trigger)) { trigger = DEFAULT_TRIGGER_URL; } message.append(name); message.append("() "); if (trigger.equals("http")) { trigger = DEFAULT_HTTP_ENDPOINT_PREFIX; } else if (trigger.startsWith("http:") || trigger.startsWith("https:") || trigger.startsWith("http://") || trigger.startsWith("https://")) { String host = getURIHost(trigger); if (localHosts.contains(host)) { trigger = DEFAULT_HTTP_ENDPOINT_PREFIX; } else { // lets add the HTTP endpoint prefix // is there any context-path String path = trigger.startsWith("https:") ? trigger.substring(6) : null; if (path == null) { path = trigger.startsWith("http:") ? trigger.substring(5) : null; } if (path == null) { path = trigger.startsWith("https://") ? trigger.substring(8) : null; } if (path == null) { path = trigger.startsWith("http://") ? trigger.substring(7) : null; } if (path != null) { // keep only context path if (path.contains("/")) { path = path.substring(path.indexOf('/')); } } if (path != null) { trigger = path; } trigger = DEFAULT_HTTP_ENDPOINT_PREFIX + "/" + trigger; } } route = from(trigger); route.id(name); } else { uri = convertEndpointURI(uri); message.append(" => "); route.to(uri); } return route; } private String convertEndpointURI(String uri) { if (uri.startsWith("http:") || uri.startsWith("https:")) { // lets use http4 for all http transports uri = replacePrefix(uri, "http:", "http4:"); uri = replacePrefix(uri, "https:", "https4:"); HttpEndpoint endpoint = endpoint(uri, HttpEndpoint.class); if (endpoint != null) { // lets bridge them as a proxy endpoint.setBridgeEndpoint(true); endpoint.setThrowExceptionOnFailure(false); } } return uri; } private String getURIHost(String uri) { try { return new URI(uri).getHost(); } catch (URISyntaxException e) { return null; } } }