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.model.InputTypeDefinition;
024import org.apache.camel.model.OutputTypeDefinition;
025import org.apache.camel.spi.Contract;
026import org.apache.camel.spi.DataType;
027import org.apache.camel.spi.DataTypeAware;
028import org.apache.camel.spi.Transformer;
029import org.apache.camel.spi.Validator;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * A {@link CamelInternalProcessorAdvice} which applies {@link Transformer} and {@link Validator}
035 * according to the data type Contract.
036 * <p/>
037 * The default camel {@link Message} implements {@link DataTypeAware} which
038 * holds a {@link DataType} to indicate current message type. If the input type
039 * declared by {@link InputTypeDefinition} is different from current IN message type,
040 * camel internal processor look for a Transformer which transforms from the current
041 * message type to the expected message type before routing.
042 * After routing, if the output type declared by {@link OutputTypeDefinition} is different
043 * from current OUT message (or IN message if no OUT), camel look for a Transformer and apply.
044 * 
045 * @see Transformer
046 * @see Validator
047 * @see InputTypeDefinition
048 * @see OutputTypeDefinition
049 */
050public class ContractAdvice implements CamelInternalProcessorAdvice {
051    private static final Logger LOG = LoggerFactory.getLogger(ContractAdvice.class);
052
053    private Contract contract;
054    
055    public ContractAdvice(Contract contract) {
056        this.contract = contract;
057    }
058    
059    @Override
060    public Object before(Exchange exchange) throws Exception {
061        if (!(exchange.getIn() instanceof DataTypeAware)) {
062            return null;
063        }
064        DataType to = contract.getInputType();
065        if (to != null) {
066            DataTypeAware target = (DataTypeAware)exchange.getIn();
067            DataType from = target.getDataType();
068            if (!to.equals(from)) {
069                LOG.debug("Looking for transformer for INPUT: from='{}', to='{}'", from, to);
070                doTransform(exchange.getIn(), from, to);
071                target.setDataType(to);
072            }
073            if (contract.isValidateInput()) {
074                doValidate(exchange.getIn(), to);
075            }
076        }
077        return null;
078    }
079    
080    @Override
081    public void after(Exchange exchange, Object data) throws Exception {
082        if (exchange.isFailed()) {
083            // TODO can we add FAULT_TYPE processing?
084            return;
085        }
086
087        Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
088        if (!(target instanceof DataTypeAware)) {
089            return;
090        }
091        DataType to = contract.getOutputType();
092        if (to != null) {
093            DataTypeAware typeAwareTarget = (DataTypeAware)target;
094            DataType from = typeAwareTarget.getDataType();
095            if (!to.equals(from)) {
096                LOG.debug("Looking for transformer for OUTPUT: from='{}', to='{}'", from, to);
097                doTransform(target, from, to);
098                typeAwareTarget.setDataType(to);
099            }
100            if (contract.isValidateOutput()) {
101                doValidate(target, to);
102            }
103        }
104    }
105    
106    private void doTransform(Message message, DataType from, DataType to) throws Exception {
107        if (from == null) {
108            // If 'from' is null, only Java-Java convertion is performed.
109            // It means if 'to' is other than Java, it's assumed to be already in expected type.
110            convertIfRequired(message, to);
111            return;
112        }
113        
114        // transform into 'from' type before performing declared transformation
115        convertIfRequired(message, from);
116        
117        if (applyMatchedTransformer(message, from, to)) {
118            // Found matched transformer. Java-Java transformer is also allowed.
119            return;
120        } else if (from.isJavaType()) {
121            // Try TypeConverter as a fallback for Java->Java transformation
122            convertIfRequired(message, to);
123            // If Java->Other transformation required but no transformer matched,
124            // then assume it's already in expected type, i.e. do nothing.
125            return;
126        } else if (applyTransformerChain(message, from, to)) {
127            // Other->Other transformation - found a transformer chain
128            return;
129        }
130        
131        throw new IllegalArgumentException("No Transformer found for [from='" + from + "', to='" + to + "']");
132    }
133    
134    private boolean convertIfRequired(Message message, DataType type) throws Exception {
135        // TODO for better performance it may be better to add TypeConverterTransformer
136        // into transformer registry automatically to avoid unnecessary scan in transformer registry
137        if (type != null && type.isJavaType() && type.getName() != null) {
138            CamelContext context = message.getExchange().getContext();
139            Class<?> typeJava = getClazz(type.getName(), context);
140            if (!typeJava.isAssignableFrom(message.getBody().getClass())) {
141                LOG.debug("Converting to '{}'", typeJava.getName());
142                message.setBody(message.getMandatoryBody(typeJava));
143                return true;
144            }
145        }
146        return false;
147    }
148    
149    private boolean applyTransformer(Transformer transformer, Message message, DataType from, DataType to) throws Exception {
150        if (transformer != null) {
151            LOG.debug("Applying transformer: from='{}', to='{}', transformer='{}'", from, to, transformer);
152            transformer.transform(message, from, to);
153            return true;
154        }
155        return false;
156    }
157    private boolean applyMatchedTransformer(Message message, DataType from, DataType to) throws Exception {
158        Transformer transformer = message.getExchange().getContext().resolveTransformer(from, to);
159        return applyTransformer(transformer, message, from, to);
160    }
161    
162    private boolean applyTransformerChain(Message message, DataType from, DataType to) throws Exception {
163        CamelContext context = message.getExchange().getContext();
164        Transformer fromTransformer = context.resolveTransformer(from.getModel());
165        Transformer toTransformer = context.resolveTransformer(to.getModel());
166        if (fromTransformer != null && toTransformer != null) {
167            LOG.debug("Applying transformer 1/2: from='{}', to='{}', transformer='{}'", from, to, fromTransformer);
168            fromTransformer.transform(message, from, new DataType(Object.class));
169            LOG.debug("Applying transformer 2/2: from='{}', to='{}', transformer='{}'", from, to, toTransformer);
170            toTransformer.transform(message, new DataType(Object.class), to);
171            return true;
172        }
173        return false;
174    }
175
176    private Class<?> getClazz(String type, CamelContext context) throws Exception {
177        return context.getClassResolver().resolveMandatoryClass(type);
178    }
179
180    private void doValidate(Message message, DataType type) throws ValidationException {
181        Validator validator = message.getExchange().getContext().resolveValidator(type);
182        if (validator != null) {
183            LOG.debug("Applying validator: type='{}', validator='{}'", type, validator);
184            validator.validate(message, type);
185        } else {
186            throw new ValidationException(message.getExchange(), String.format("No Validator found for '%s'", type));
187        }
188    }
189}