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

org.apache.camel.component.jslt.JsltEndpoint Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.
 */
package org.apache.camel.component.jslt;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.schibsted.spt.data.jslt.Expression;
import com.schibsted.spt.data.jslt.Function;
import com.schibsted.spt.data.jslt.JsltException;
import com.schibsted.spt.data.jslt.Parser;
import com.schibsted.spt.data.jslt.filters.DefaultJsonFilter;
import com.schibsted.spt.data.jslt.filters.JsonFilter;
import org.apache.camel.Category;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Message;
import org.apache.camel.ValidationException;
import org.apache.camel.WrappedFile;
import org.apache.camel.component.ResourceEndpoint;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;

/**
 * Query or transform JSON payloads using JSLT.
 */
@UriEndpoint(firstVersion = "3.1.0", scheme = "jslt", title = "JSLT", syntax = "jslt:resourceUri", producerOnly = true,
             remote = false, category = { Category.TRANSFORMATION }, headersClass = JsltConstants.class)
public class JsltEndpoint extends ResourceEndpoint {

    private static final ObjectMapper OBJECT_MAPPER;
    private static final JsonFilter DEFAULT_JSON_FILTER = new DefaultJsonFilter();

    static {
        OBJECT_MAPPER = new ObjectMapper();
        OBJECT_MAPPER.setSerializerFactory(OBJECT_MAPPER.getSerializerFactory().withSerializerModifier(
                new SafeTypesOnlySerializerModifier()));
    }

    private Expression transform;

    @UriParam(defaultValue = "false")
    private boolean allowTemplateFromHeader;
    @UriParam(defaultValue = "false", label = "common")
    private boolean prettyPrint;
    @UriParam(defaultValue = "false")
    private boolean mapBigDecimalAsFloats;
    @UriParam
    private ObjectMapper objectMapper;

    public JsltEndpoint() {
    }

    public JsltEndpoint(String uri, JsltComponent component, String resourceUri) {
        super(uri, component, resourceUri);
    }

    @Override
    public boolean isRemote() {
        return false;
    }

    @Override
    public ExchangePattern getExchangePattern() {
        return ExchangePattern.InOut;
    }

    @Override
    protected String createEndpointUri() {
        return "jslt:" + getResourceUri();
    }

    private Expression getTransform(Message msg) throws Exception {
        getInternalLock().lock();
        try {

            final String jsltStringFromHeader
                    = allowTemplateFromHeader ? msg.getHeader(JsltConstants.HEADER_JSLT_STRING, String.class) : null;

            final boolean useTemplateFromUri = jsltStringFromHeader == null;

            if (useTemplateFromUri && transform != null) {
                return transform;
            }

            final Collection functions = Objects.requireNonNullElse(
                    ((JsltComponent) getComponent()).getFunctions(),
                    Collections.emptyList());

            final JsonFilter objectFilter = Objects.requireNonNullElse(
                    ((JsltComponent) getComponent()).getObjectFilter(),
                    DEFAULT_JSON_FILTER);

            final String transformSource;
            final InputStream stream;

            if (useTemplateFromUri) {
                transformSource = getResourceUri();

                if (log.isDebugEnabled()) {
                    log.debug("Jslt content read from resource {} with resourceUri: {} for endpoint {}",
                            transformSource,
                            transformSource,
                            getEndpointUri());
                }

                stream = ResourceHelper.resolveMandatoryResourceAsInputStream(getCamelContext(), transformSource);
                if (stream == null) {
                    throw new JsltException("Cannot load resource '" + transformSource + "': not found");
                }
            } else { // use template from header
                stream = new ByteArrayInputStream(jsltStringFromHeader.getBytes(StandardCharsets.UTF_8));
                transformSource = "";
            }

            final Expression transform;
            try {
                transform = new Parser(new InputStreamReader(stream))
                        .withFunctions(functions)
                        .withObjectFilter(objectFilter)
                        .withSource(transformSource)
                        .compile();
            } finally {
                // the stream is consumed only on .compile(), cannot be closed before
                IOHelper.close(stream);
            }

            if (useTemplateFromUri) {
                this.transform = transform;
            }
            return transform;
        } finally {
            getInternalLock().unlock();
        }
    }

    public JsltEndpoint findOrCreateEndpoint(String uri, String newResourceUri) {
        String newUri = uri.replace(getResourceUri(), newResourceUri);
        log.debug("Getting endpoint with URI: {}", newUri);
        return getCamelContext().getEndpoint(newUri, JsltEndpoint.class);
    }

    @Override
    protected void onExchange(Exchange exchange) throws Exception {
        String path = getResourceUri();
        ObjectHelper.notNull(path, "resourceUri");

        String newResourceUri = null;
        if (allowTemplateFromHeader) {
            newResourceUri = exchange.getIn().getHeader(JsltConstants.HEADER_JSLT_RESOURCE_URI, String.class);
        }
        if (newResourceUri != null) {
            exchange.getIn().removeHeader(JsltConstants.HEADER_JSLT_RESOURCE_URI);

            log.debug("{} set to {} creating new endpoint to handle exchange", JsltConstants.HEADER_JSLT_RESOURCE_URI,
                    newResourceUri);
            JsltEndpoint newEndpoint = findOrCreateEndpoint(getEndpointUri(), newResourceUri);
            newEndpoint.onExchange(exchange);
            return;
        }

        JsonNode input;

        ObjectMapper objectMapper;
        if (ObjectHelper.isEmpty(getObjectMapper())) {
            objectMapper = new ObjectMapper();
        } else {
            objectMapper = getObjectMapper();
        }
        if (isMapBigDecimalAsFloats()) {
            objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
        }

        Object body = exchange.getIn().getBody();
        if (body instanceof WrappedFile) {
            body = ((WrappedFile) body).getFile();
        }
        if (body instanceof String) {
            input = objectMapper.readTree((String) body);
        } else if (body instanceof Reader) {
            input = objectMapper.readTree((Reader) body);
        } else if (body instanceof File) {
            input = objectMapper.readTree((File) body);
        } else if (body instanceof byte[]) {
            input = objectMapper.readTree((byte[]) body);
        } else if (body instanceof InputStream) {
            input = objectMapper.readTree((InputStream) body);
        } else {
            throw new ValidationException(exchange, "Allowed body types are String, Reader, File, byte[] or InputStream.");
        }

        Map variables = extractVariables(exchange);
        JsonNode output = getTransform(exchange.getMessage()).apply(variables, input);

        String result = isPrettyPrint() ? output.toPrettyString() : output.toString();
        ExchangeHelper.setInOutBodyPatternAware(exchange, result);
    }

    /**
     * Extract the variables from the headers in the message.
     */
    private Map extractVariables(Exchange exchange) {
        Map variableMap = ExchangeHelper.createVariableMap(exchange, isAllowContextMapAll());
        Map serializedVariableMap = new HashMap<>();
        if (variableMap.containsKey("headers")) {
            serializedVariableMap.put("headers", serializeMapToJsonNode((Map) variableMap.get("headers")));
        }
        if (variableMap.containsKey("variables")) {
            serializedVariableMap.put("variables", serializeMapToJsonNode((Map) variableMap.get("variables")));
        }
        if (variableMap.containsKey("exchange")) {
            Exchange ex = (Exchange) variableMap.get("exchange");
            ObjectNode exchangeNode = OBJECT_MAPPER.createObjectNode();
            if (ex.getProperties() != null) {
                exchangeNode.set("properties", serializeMapToJsonNode(ex.getProperties()));
            }
            serializedVariableMap.put("exchange", exchangeNode);
        }
        return serializedVariableMap;
    }

    private ObjectNode serializeMapToJsonNode(Map map) {
        ObjectNode mapNode = OBJECT_MAPPER.createObjectNode();
        for (Map.Entry entry : map.entrySet()) {
            if (entry.getValue() != null) {
                try {
                    // Use Jackson to convert value to JsonNode
                    mapNode.set(entry.getKey(), OBJECT_MAPPER.valueToTree(entry.getValue()));
                } catch (IllegalArgumentException e) {
                    //If Jackson cannot convert the value to json (e.g. infinite recursion in the value to serialize)
                    log.debug("Value could not be converted to JsonNode", e);
                }
            }
        }
        return mapNode;
    }

    /**
     * If true, JSON in output message is pretty printed.
     */
    public boolean isPrettyPrint() {
        return prettyPrint;
    }

    public void setPrettyPrint(boolean prettyPrint) {
        this.prettyPrint = prettyPrint;
    }

    public boolean isAllowTemplateFromHeader() {
        return allowTemplateFromHeader;
    }

    /**
     * Whether to allow to use resource template from header or not (default false).
     *
     * Enabling this allows to specify dynamic templates via message header. However this can be seen as a potential
     * security vulnerability if the header is coming from a malicious user, so use this with care.
     */
    public void setAllowTemplateFromHeader(boolean allowTemplateFromHeader) {
        this.allowTemplateFromHeader = allowTemplateFromHeader;
    }

    public boolean isMapBigDecimalAsFloats() {
        return mapBigDecimalAsFloats;
    }

    /**
     * If true, the mapper will use the USE_BIG_DECIMAL_FOR_FLOATS in serialization features
     */
    public void setMapBigDecimalAsFloats(boolean mapBigDecimalAsFloats) {
        this.mapBigDecimalAsFloats = mapBigDecimalAsFloats;
    }

    public ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    /**
     * Setting a custom JSON Object Mapper to be used
     */
    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    private static class SafeTypesOnlySerializerModifier extends BeanSerializerModifier {
        // Serialize only safe types: primitives, records, serializable objects and
        // collections/maps/arrays of them. To avoid serializing something like Response object.
        // Types that are not safe are serialized as their toString() value.
        @Override
        public JsonSerializer modifySerializer(
                SerializationConfig config, BeanDescription beanDesc,
                JsonSerializer serializer) {
            final Class beanClass = beanDesc.getBeanClass();

            if (Collection.class.isAssignableFrom(beanClass)
                    || Map.class.isAssignableFrom(beanClass)
                    || beanClass.isArray()
                    || beanClass.isPrimitive()
                    || isRecord(beanClass)
                    || Serializable.class.isAssignableFrom(beanClass)) {
                return serializer;
            }

            return ToStringSerializer.instance;
        }

        private static boolean isRecord(Class clazz) {
            final Class parent = clazz.getSuperclass();
            return parent != null && parent.getName().equals("java.lang.Record");
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy