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