001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.processor;
018
019import org.apache.camel.CamelContext;
020import org.apache.camel.Exchange;
021import org.apache.camel.Message;
022import org.apache.camel.ValidationException;
023import org.apache.camel.spi.Contract;
024import org.apache.camel.spi.DataType;
025import org.apache.camel.spi.DataTypeAware;
026import org.apache.camel.spi.Transformer;
027import org.apache.camel.spi.Validator;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * A {@link CamelInternalProcessorAdvice} which applies {@link Transformer} and {@link Validator}
033 * according to the data type Contract.
034 * The default camel {@link Message} implements {@link DataTypeAware} which
035 * holds a {@link DataType} to indicate current message type. If the input type
036 * declared by {@link InputTypeDefinition} is diffrent from current IN message type,
037 * camel internal processor look for a Transformer which transforms from the current
038 * message type to the expected message type before routing.
039 * After routing, if the output type declared by {@link OutputTypeDefinition} is different
040 * from current OUT message (or IN message if no OUT), camel look for a Transformer and apply.
041 * 
042 * @see {@link Transformer} {@link Validator}
043 * {@link InputTypeDefinition} {@link OutputTypeDefinition}
044 */
045public class ContractAdvice implements CamelInternalProcessorAdvice {
046    private static final Logger LOG = LoggerFactory.getLogger(CamelInternalProcessor.class);
047
048    private Contract contract;
049    
050    public ContractAdvice(Contract contract) {
051        this.contract = contract;
052    }
053    
054    @Override
055    public Object before(Exchange exchange) throws Exception {
056        if (!(exchange.getIn() instanceof DataTypeAware)) {
057            return null;
058        }
059        DataTypeAware target = (DataTypeAware)exchange.getIn();
060        DataType from = target.getDataType();
061        DataType to = contract.getInputType();
062        if (to != null) {
063            if (!to.equals(from)) {
064                LOG.debug("Looking for transformer for INPUT: from='{}', to='{}'", from, to);
065                doTransform(exchange.getIn(), from, to);
066                target.setDataType(to);
067            }
068            if (contract.isValidateInput()) {
069                doValidate(exchange.getIn(), to);
070            }
071        }
072        return null;
073    }
074    
075    @Override
076    public void after(Exchange exchange, Object data) throws Exception {
077        if (exchange.isFailed()) {
078            // TODO can we add FAULT_TYPE processing?
079            return;
080        }
081
082        Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
083        if (!(target instanceof DataTypeAware)) {
084            return;
085        }
086        DataTypeAware typeAwareTarget = (DataTypeAware)target;
087        DataType from = typeAwareTarget.getDataType();
088        DataType to = contract.getOutputType();
089        if (to != null) {
090            if (!to.equals(from)) {
091                LOG.debug("Looking for transformer for OUTPUT: from='{}', to='{}'", from, to);
092                doTransform(target, from, to);
093                typeAwareTarget.setDataType(to);
094            }
095            if (contract.isValidateOutput()) {
096                doValidate(target, to);
097            }
098        }
099    }
100    
101    private void doTransform(Message message, DataType from, DataType to) throws Exception {
102        if (from == null) {
103            // If 'from' is null, only Java-Java convertion is performed.
104            // It means if 'to' is other than Java, it's assumed to be already in expected type.
105            convertIfRequired(message, to);
106            return;
107        }
108        
109        // transform into 'from' type before performing declared transformation
110        convertIfRequired(message, from);
111        
112        if (applyMatchedTransformer(message, from, to)) {
113            // Found matched transformer. Java-Java transformer is also allowed.
114            return;
115        } else if (from.isJavaType()) {
116            // Try TypeConverter as a fallback for Java->Java transformation
117            convertIfRequired(message, to);
118            // If Java->Other transformation required but no transformer matched,
119            // then assume it's already in expected type, i.e. do nothing.
120            return;
121        } else if (applyTransformerChain(message, from, to)) {
122            // Other->Other transformation - found a transformer chain
123            return;
124        }
125        
126        throw new IllegalArgumentException("No Transformer found for [from='" + from + "', to='" + to + "']");
127    }
128    
129    private boolean convertIfRequired(Message message, DataType type) throws Exception {
130        // TODO for better performance it may be better to add TypeConveterTransformer
131        // into transformer registry automatically to avoid unnecessary scan in transformer registry
132        if (type != null && type.isJavaType() && type.getName() != null) {
133            CamelContext context = message.getExchange().getContext();
134            Class<?> typeJava = getClazz(type.getName(), context);
135            if (!typeJava.isAssignableFrom(message.getBody().getClass())) {
136                LOG.debug("Converting to '{}'", typeJava.getName());
137                message.setBody(message.getMandatoryBody(typeJava));
138                return true;
139            }
140        }
141        return false;
142    }
143    
144    private boolean applyTransformer(Transformer transformer, Message message, DataType from, DataType to) throws Exception {
145        if (transformer != null) {
146            LOG.debug("Applying transformer: from='{}', to='{}', transformer='{}'", from, to, transformer);
147            transformer.transform(message, from, to);
148            return true;
149        }
150        return false;
151    }
152    private boolean applyMatchedTransformer(Message message, DataType from, DataType to) throws Exception {
153        Transformer transformer = message.getExchange().getContext().resolveTransformer(from, to);
154        return applyTransformer(transformer, message, from, to);
155    }
156    
157    private boolean applyTransformerChain(Message message, DataType from, DataType to) throws Exception {
158        CamelContext context = message.getExchange().getContext();
159        Transformer fromTransformer = context.resolveTransformer(from.getModel());
160        Transformer toTransformer = context.resolveTransformer(to.getModel());
161        if (fromTransformer != null && toTransformer != null) {
162            LOG.debug("Applying transformer 1/2: from='{}', to='{}', transformer='{}'", from, to, fromTransformer);
163            fromTransformer.transform(message, from, new DataType(Object.class));
164            LOG.debug("Applying transformer 2/2: from='{}', to='{}', transformer='{}'", from, to, toTransformer);
165            toTransformer.transform(message, new DataType(Object.class), to);
166            return true;
167        }
168        return false;
169    }
170
171    private Class<?> getClazz(String type, CamelContext context) throws Exception {
172        return context.getClassResolver().resolveMandatoryClass(type);
173    }
174
175    private void doValidate(Message message, DataType type) throws ValidationException {
176        Validator validator = message.getExchange().getContext().resolveValidator(type);
177        if (validator != null) {
178            LOG.debug("Applying validator: type='{}', validator='{}'", type, validator);
179            validator.validate(message, type);
180        } else {
181            throw new ValidationException(message.getExchange(), String.format("No Validator found for '%s'", type));
182        }
183    }
184}