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     */
017    package org.apache.camel.dataformat.soap;
018    
019    import java.io.IOException;
020    import java.io.InputStream;
021    import java.io.OutputStream;
022    import java.lang.annotation.Annotation;
023    import java.lang.reflect.Constructor;
024    import java.lang.reflect.Method;
025    import java.util.ArrayList;
026    import java.util.List;
027    
028    import javax.jws.WebMethod;
029    import javax.jws.WebParam;
030    import javax.xml.bind.JAXBContext;
031    import javax.xml.bind.JAXBElement;
032    import javax.xml.bind.JAXBException;
033    import javax.xml.bind.JAXBIntrospector;
034    import javax.xml.namespace.QName;
035    import javax.xml.ws.WebFault;
036    
037    import org.apache.camel.Exchange;
038    import org.apache.camel.Message;
039    import org.apache.camel.RuntimeCamelException;
040    import org.apache.camel.component.bean.BeanInvocation;
041    import org.apache.camel.converter.jaxb.JaxbDataFormat;
042    import org.apache.camel.dataformat.soap.name.ElementNameStrategy;
043    import org.apache.camel.dataformat.soap.name.ServiceInterfaceStrategy;
044    import org.apache.camel.dataformat.soap.name.TypeNameStrategy;
045    import org.slf4j.Logger;
046    import org.slf4j.LoggerFactory;
047    
048    import org.xmlsoap.schemas.soap.envelope.Body;
049    import org.xmlsoap.schemas.soap.envelope.Detail;
050    import org.xmlsoap.schemas.soap.envelope.Envelope;
051    import org.xmlsoap.schemas.soap.envelope.Fault;
052    import org.xmlsoap.schemas.soap.envelope.Header;
053    import org.xmlsoap.schemas.soap.envelope.ObjectFactory;
054    
055    /**
056     * Marshaling from Objects to SOAP and back by using JAXB. The classes to be
057     * processed need to have JAXB annotations. For marshaling a ElementNameStrategy
058     * is used to determine how the top level elements in SOAP are named as this can
059     * not be extracted from JAXB.
060     */
061    public class SoapJaxbDataFormat extends JaxbDataFormat {
062    
063        public static final String SOAP_UNMARSHALLED_HEADER_LIST = "org.apache.camel.dataformat.soap.UNMARSHALLED_HEADER_LIST";
064        
065        private static final String SOAP_PACKAGE_NAME = Envelope.class.getPackage().getName();
066    
067        private static final QName FAULT_CODE_SERVER = new QName("http://www.w3.org/2003/05/soap-envelope", "Receiver");
068        
069        protected final transient Logger log = LoggerFactory.getLogger(getClass());
070        
071        private ElementNameStrategy elementNameStrategy;
072    
073        private String elementNameStrategyRef;
074        
075        private boolean ignoreUnmarshalledHeaders;
076    
077        /**
078         * Remember to set the context path when using this constructor
079         */
080        public SoapJaxbDataFormat() {
081            super();
082        }
083    
084        /**
085         * Initialize with JAXB context path
086         * 
087         * @param contexPath
088         */
089        public SoapJaxbDataFormat(String contextPath) {
090            super(contextPath);
091        }
092    
093        /**
094         * Initialize the data format. The serviceInterface is necessary to
095         * determine the element name and namespace of the element inside the soap
096         * body when marshalling
097         * 
098         * @param contextPath
099         *            package for JAXB context
100         * @param serviceInterface
101         *            webservice interface
102         */
103        public SoapJaxbDataFormat(String contextPath, ElementNameStrategy elementNameStrategy) {
104            this(contextPath);
105            this.elementNameStrategy = elementNameStrategy;
106        }
107        
108        /**
109         * Initialize the data format. The serviceInterface is necessary to
110         * determine the element name and namespace of the element inside the soap
111         * body when marshalling
112         * 
113         * @param contextPath
114         *            package for JAXB context
115         * @param elementNameStrategyRef
116         *            webservice interface referenced bean name
117         */
118        public SoapJaxbDataFormat(String contextPath, String elementNameStrategyRef) {
119            this(contextPath);
120            this.elementNameStrategyRef = elementNameStrategyRef;
121        }
122    
123        public void setElementNameStrategy(Object nameStrategy) {
124            if (nameStrategy instanceof ElementNameStrategy) {
125                this.elementNameStrategy = (ElementNameStrategy) nameStrategy;
126            } else {
127                throw new IllegalArgumentException("The argument for setElementNameStrategy should be subClass of "
128                        + ElementNameStrategy.class.getName());
129            }
130        }
131        
132        public void setIgnoreUnmarshalledHeaders(boolean ignoreHeaders) {
133            this.ignoreUnmarshalledHeaders = ignoreHeaders;
134        }
135        
136        /**
137         * Indicates whether header content that has been unmarshalled should be placed into a message
138         * header on the exchange
139         */
140        private boolean isIgnoreUnmarshalledHeaders() {
141            return ignoreUnmarshalledHeaders;
142        }
143    
144        protected void checkElementNameStrategy(Exchange exchange) {
145            if (elementNameStrategy == null) {
146                synchronized (this) {
147                    if (elementNameStrategy != null) {
148                        return;
149                    } else {
150                        if (elementNameStrategyRef != null) {
151                            elementNameStrategy = exchange.getContext().getRegistry().lookup(elementNameStrategyRef,
152                                    ElementNameStrategy.class);
153                        } else {
154                            elementNameStrategy = new TypeNameStrategy();
155                        }
156                    }
157                }
158            }
159        }
160    
161        /**
162         * Marshal inputObjects to SOAP xml. If the exchange or message has an
163         * EXCEPTION_CAUGTH property or header then instead of the object the
164         * exception is marshaled.
165         * 
166         * To determine the name of the top level xml elements the elementNameStrategy
167         * is used.
168         */
169        public void marshal(Exchange exchange, final Object inputObject, OutputStream stream) throws IOException {
170            checkElementNameStrategy(exchange);
171    
172            String soapAction = getSoapActionFromExchange(exchange);
173            if (soapAction == null && inputObject instanceof BeanInvocation) {
174                BeanInvocation beanInvocation = (BeanInvocation) inputObject;
175                WebMethod webMethod = beanInvocation.getMethod().getAnnotation(WebMethod.class);
176                if (webMethod != null && webMethod.action() != null) {
177                    soapAction = webMethod.action();
178                }
179            }
180                    
181            Body body = new Body();
182            Header header = new Header();
183    
184            Throwable exception = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
185            if (exception == null) {
186                exception = exchange.getIn().getHeader(Exchange.EXCEPTION_CAUGHT, Throwable.class);
187            }
188            
189            final List<JAXBElement<?>> bodyContent;
190            List<JAXBElement<?>> headerContent = new ArrayList<JAXBElement<?>>();
191            if (exception != null) {
192                bodyContent = new ArrayList<JAXBElement<?>>();
193                bodyContent.add(createFaultFromException(exception));
194            } else {
195                bodyContent = createContentFromObject(inputObject, soapAction, headerContent);
196            }
197           
198            for (JAXBElement<?> elem : bodyContent) {
199                body.getAny().add(elem);
200            }
201            for (JAXBElement<?> elem : headerContent) {
202                header.getAny().add(elem);
203            }
204            Envelope envelope = new Envelope();
205            if (headerContent.size() > 0) {
206                envelope.setHeader(header);
207            }
208            envelope.setBody(body);
209            JAXBElement<Envelope> envelopeEl = new ObjectFactory().createEnvelope(envelope);
210            super.marshal(exchange, envelopeEl, stream);
211        }
212    
213        /**
214         * Create body content from a non Exception object. If the inputObject is a
215         * BeanInvocation the following should be considered: The first parameter
216         * will be used for the SOAP body. BeanInvocations with more than one
217         * parameter are not supported. So the interface should be in doc lit bare
218         * style.
219         * 
220         * @param inputObject
221         *            object to be put into the SOAP body
222         * @param soapAction
223         *            for name resolution
224         * @param classResolver
225         *            for name resolution
226         * @param headerElements
227         *            in/out parameter used to capture header content if present
228         *            
229         * @return JAXBElement for the body content
230         */
231        private List<JAXBElement<?>> createContentFromObject(final Object inputObject, String soapAction,
232                                                             List<JAXBElement<?>> headerElements) {
233            List<Object> bodyParts = new ArrayList<Object>();
234            List<Object> headerParts = new ArrayList<Object>();
235            if (inputObject instanceof BeanInvocation) {
236                BeanInvocation bi = (BeanInvocation)inputObject;
237                Annotation[][] annotations = bi.getMethod().getParameterAnnotations();
238    
239                List<WebParam> webParams = new ArrayList<WebParam>();
240                for (int i = 0; i < annotations.length; i++) {
241                    Annotation[] singleParameterAnnotations = annotations[i];
242                    for (int j = 0; j < singleParameterAnnotations.length; j++) {
243                        Annotation annotation = singleParameterAnnotations[j];
244                        if (annotation instanceof WebParam) {
245                            webParams.add((WebParam)annotation);
246                        }
247                    }
248                }
249    
250                if (webParams.size() > 0) {
251                    if (webParams.size() == bi.getArgs().length) {
252                        int index = -1;
253                        for (Object o : bi.getArgs()) {
254                            if (webParams.get(++index).header()) {
255                                headerParts.add(o);
256                            } else {
257                                bodyParts.add(o);
258                            }
259                        }
260                    } else {
261                        throw new RuntimeCamelException(
262                                                        "The number of bean invocation parameters does not "
263                                                            + "match the number of parameters annotated with @WebParam for the method [ "
264                                                            + bi.getMethod().getName() + "].");
265                    }
266                } else {
267                    // try to map all objects for the body
268                    for (Object o : bi.getArgs()) {
269                        bodyParts.add(o);
270                    }
271                }
272    
273            } else {
274                bodyParts.add(inputObject);
275            }
276    
277            List<JAXBElement<?>> bodyElements = new ArrayList<JAXBElement<?>>();
278            for (Object bodyObj : bodyParts) {
279                QName name = elementNameStrategy.findQNameForSoapActionOrType(soapAction, bodyObj.getClass());
280                if (name == null) {
281                    log.warn("Could not find QName for class " + bodyObj.getClass().getName());
282                    continue;
283                } else {
284                    bodyElements.add(getElement(bodyObj, name));
285                }
286            }
287    
288            for (Object headerObj : headerParts) {
289                QName name = elementNameStrategy.findQNameForSoapActionOrType(soapAction, headerObj.getClass());
290                if (name == null) {
291                    log.warn("Could not find QName for class " + headerObj.getClass().getName());
292                    continue;
293                } else {
294                    JAXBElement<?> headerElem = getElement(headerObj, name);
295                    if (null != headerElem) {
296                        headerElements.add(headerElem);
297                    }
298                }
299            }
300    
301            return bodyElements;
302        }
303        
304        @SuppressWarnings({ "rawtypes", "unchecked" })
305        private JAXBElement<?> getElement(Object fromObj, QName name) {
306           
307            Object value = null;
308            
309            // In the case of a parameter, the class of the value of the holder class
310            // is used for the mapping rather than the holder class itself.
311            
312            if (fromObj instanceof javax.xml.ws.Holder) {
313                javax.xml.ws.Holder holder = (javax.xml.ws.Holder) fromObj;
314                value = holder.value;
315                if (null == value) {
316                    return null;
317                }
318            } else {
319                value = fromObj;
320            }
321            
322            return new JAXBElement(name, value.getClass(), value);
323        }
324        
325        
326        /**
327         * Creates a SOAP fault from the exception and populates the message as well
328         * as the detail. The detail object is read from the method getFaultInfo of
329         * the throwable if present
330         * 
331         * @param exception
332         * @return SOAP fault from given Throwable
333         */
334        @SuppressWarnings("unchecked")
335        private JAXBElement<Fault> createFaultFromException(final Throwable exception) {
336            WebFault webFault = exception.getClass().getAnnotation(WebFault.class);
337            if (webFault == null || webFault.targetNamespace() == null) {
338                throw new RuntimeException("The exception " + exception.getClass().getName()
339                        + " needs to have an WebFault annotation with name and targetNamespace", exception);
340            }
341            QName name = new QName(webFault.targetNamespace(), webFault.name());
342            Object faultObject = null;
343            try {
344                Method method = exception.getClass().getMethod("getFaultInfo");
345                faultObject = method.invoke(exception);
346            } catch (Exception e) {
347                throw new RuntimeCamelException("Exception while trying to get fault details", e);
348            }
349            Fault fault = new Fault();
350            fault.setFaultcode(FAULT_CODE_SERVER);
351            fault.setFaultstring(exception.getMessage());
352            Detail detailEl = new ObjectFactory().createDetail();
353            @SuppressWarnings("rawtypes")
354            JAXBElement<?> faultDetailContent = new JAXBElement(name, faultObject.getClass(), faultObject);
355            detailEl.getAny().add(faultDetailContent);
356            fault.setDetail(detailEl);
357            return new ObjectFactory().createFault(fault);
358        }
359    
360        /**
361         * Unmarshal a given SOAP xml stream and return the content of the SOAP body
362         */
363        public Object unmarshal(Exchange exchange, InputStream stream) throws IOException {
364            checkElementNameStrategy(exchange);
365            
366            String soapAction = getSoapActionFromExchange(exchange);
367            
368            // Determine the method name for an eventual BeanProcessor in the route
369            if (soapAction != null && elementNameStrategy instanceof ServiceInterfaceStrategy) {
370                ServiceInterfaceStrategy strategy = (ServiceInterfaceStrategy) elementNameStrategy;
371                String methodName = strategy.getMethodForSoapAction(soapAction);
372                exchange.getOut().setHeader(Exchange.BEAN_METHOD_NAME, methodName);
373            }
374            
375            // Store soap action for an eventual later marshal step.
376            // This is necessary as the soap action in the message may get lost on the way
377            if (soapAction != null) {
378                exchange.setProperty(Exchange.SOAP_ACTION, soapAction);
379            }
380            
381            Object unmarshalledObject = super.unmarshal(exchange, stream);
382            Object rootObject = JAXBIntrospector.getValue(unmarshalledObject);
383            if (rootObject.getClass() != Envelope.class) {
384                throw new RuntimeCamelException("Expected Soap Envelope but got " + rootObject.getClass());
385            }
386            Envelope envelope = (Envelope) rootObject;
387            
388            Header header = envelope.getHeader();
389            if (header != null) {
390                List<Object> returnHeaders;
391                List<Object> anyHeaderElements = envelope.getHeader().getAny();
392                if (null != anyHeaderElements && !(isIgnoreUnmarshalledHeaders())) {
393                    if (isIgnoreJAXBElement()) {
394                        returnHeaders = new ArrayList<Object>();
395                        for (Object headerEl : anyHeaderElements) {
396                            returnHeaders.add(JAXBIntrospector.getValue(headerEl));
397                        }  
398                    } else {
399                        returnHeaders = anyHeaderElements;
400                    }
401                    exchange.getOut().setHeader(SoapJaxbDataFormat.SOAP_UNMARSHALLED_HEADER_LIST, returnHeaders);
402                }
403            }
404            
405            List<Object> anyElement = envelope.getBody().getAny();
406            if (anyElement.size() == 0) {
407                // No parameter so return null
408                return null;
409    
410            }
411            Object payloadEl = anyElement.get(0);
412            Object payload = JAXBIntrospector.getValue(payloadEl);
413            if (payload instanceof Fault) {
414                Exception exception = createExceptionFromFault((Fault) payload);
415                exchange.setException(exception);
416                return null;
417            } else {
418                return isIgnoreJAXBElement() ? payload : payloadEl;
419            }
420        }
421    
422        private String getSoapActionFromExchange(Exchange exchange) {
423            Message inMessage = exchange.getIn();
424            String soapAction = inMessage .getHeader(Exchange.SOAP_ACTION, String.class);
425            if (soapAction == null) {
426                soapAction = inMessage.getHeader("SOAPAction", String.class);
427                if (soapAction != null && soapAction.startsWith("\"")) {
428                    soapAction = soapAction.substring(1, soapAction.length() - 1);
429                }
430            }
431            if (soapAction == null) {
432                soapAction = exchange.getProperty(Exchange.SOAP_ACTION, String.class);
433            }
434            return soapAction;
435        }
436    
437        /**
438         * Creates an exception and eventually an embedded bean that contains the
439         * fault detail. The exception class is determined by using the
440         * elementNameStrategy. The qName of the fault detail should match the
441         * WebFault annotation of the Exception class. If no fault detail is set the
442         * a RuntimeCamelException is created.
443         * 
444         * @param fault
445         *            Soap fault
446         * @return created Exception
447         */
448        private Exception createExceptionFromFault(Fault fault) {
449            List<Object> detailList = fault.getDetail().getAny();
450            String message = fault.getFaultstring();
451    
452            if (detailList.size() == 0) {
453                return new RuntimeCamelException(message);
454            }
455            JAXBElement<?> detailEl = (JAXBElement<?>) detailList.get(0);
456            Class<? extends Exception> exceptionClass = elementNameStrategy.findExceptionForFaultName(detailEl.getName());
457            Constructor<? extends Exception> messageContructor;
458            Constructor<? extends Exception> constructor;
459    
460            try {
461                messageContructor = exceptionClass.getConstructor(String.class);
462                Object detail = JAXBIntrospector.getValue(detailEl);
463                try {
464                    constructor = exceptionClass.getConstructor(String.class, detail.getClass());
465                    return constructor.newInstance(message, detail);
466                } catch (NoSuchMethodException e) {
467                    return messageContructor.newInstance(message);
468                }
469            } catch (Exception e) {
470                throw new RuntimeCamelException(e);
471            }
472        }
473    
474        /**
475         * Added the generated SOAP package to the JAXB context so Soap datatypes
476         * are available
477         */
478        @Override
479        protected JAXBContext createContext() throws JAXBException {
480            if (getContextPath() != null) {
481                return JAXBContext.newInstance(SOAP_PACKAGE_NAME + ":" + getContextPath());
482            } else {
483                return JAXBContext.newInstance();
484            }
485        }
486    
487        public void setElementNameStrategy(ElementNameStrategy elementNameStrategy) {
488            this.elementNameStrategy = elementNameStrategy;
489        }
490        
491        public void setElementNameStrategyRef(String nameStrategyRef) {
492            this.elementNameStrategyRef = nameStrategyRef;
493        }
494    
495    }
496