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.component.mail; 018 019 import java.io.IOException; 020 import java.io.UnsupportedEncodingException; 021 import java.nio.charset.Charset; 022 import java.nio.charset.IllegalCharsetNameException; 023 import java.util.Enumeration; 024 import java.util.HashMap; 025 import java.util.Iterator; 026 import java.util.Map; 027 028 import javax.activation.DataHandler; 029 import javax.activation.DataSource; 030 import javax.mail.Address; 031 import javax.mail.BodyPart; 032 import javax.mail.Header; 033 import javax.mail.Message; 034 import javax.mail.MessagingException; 035 import javax.mail.Multipart; 036 import javax.mail.Part; 037 import javax.mail.internet.InternetAddress; 038 import javax.mail.internet.MimeBodyPart; 039 import javax.mail.internet.MimeMessage; 040 import javax.mail.internet.MimeMultipart; 041 import javax.mail.util.ByteArrayDataSource; 042 043 import org.apache.camel.Exchange; 044 import org.apache.camel.RuntimeCamelException; 045 import org.apache.camel.converter.ObjectConverter; 046 import org.apache.camel.impl.DefaultHeaderFilterStrategy; 047 import org.apache.camel.spi.HeaderFilterStrategy; 048 import org.apache.camel.util.CollectionHelper; 049 import org.apache.camel.util.ObjectHelper; 050 import org.apache.commons.logging.Log; 051 import org.apache.commons.logging.LogFactory; 052 053 /** 054 * A Strategy used to convert between a Camel {@link Exchange} and {@link Message} to and 055 * from a Mail {@link MimeMessage} 056 * 057 * @version $Revision: 925103 $ 058 */ 059 public class MailBinding { 060 061 private static final transient Log LOG = LogFactory.getLog(MailBinding.class); 062 private HeaderFilterStrategy headerFilterStrategy; 063 private ContentTypeResolver contentTypeResolver; 064 065 public MailBinding() { 066 headerFilterStrategy = new DefaultHeaderFilterStrategy(); 067 } 068 069 public MailBinding(HeaderFilterStrategy headerFilterStrategy, ContentTypeResolver contentTypeResolver) { 070 this.headerFilterStrategy = headerFilterStrategy; 071 this.contentTypeResolver = contentTypeResolver; 072 } 073 074 public void populateMailMessage(MailEndpoint endpoint, MimeMessage mimeMessage, Exchange exchange) 075 throws MessagingException, IOException { 076 077 // camel message headers takes precedence over endpoint configuration 078 if (hasRecipientHeaders(exchange)) { 079 setRecipientFromCamelMessage(mimeMessage, exchange); 080 } else { 081 // fallback to endpoint configuration 082 setRecipientFromEndpointConfiguration(mimeMessage, endpoint); 083 } 084 085 // must have at least one recipients otherwise we do not know where to send the mail 086 if (mimeMessage.getAllRecipients() == null) { 087 throw new IllegalArgumentException("The mail message does not have any recipients set."); 088 } 089 090 // set the subject if it was passed in as an option in the uri. Note: if it is in both the URI 091 // and headers the headers win. 092 String subject = endpoint.getConfiguration().getSubject(); 093 if (subject != null) { 094 mimeMessage.setHeader("Subject", subject); 095 } 096 097 // append the rest of the headers (no recipients) that could be subject, reply-to etc. 098 appendHeadersFromCamelMessage(mimeMessage, endpoint.getConfiguration(), exchange); 099 100 if (empty(mimeMessage.getFrom())) { 101 // lets default the address to the endpoint destination 102 String from = endpoint.getConfiguration().getFrom(); 103 mimeMessage.setFrom(new InternetAddress(from)); 104 } 105 106 // if there is an alternative body provided, set up a mime multipart alternative message 107 if (hasAlternativeBody(endpoint.getConfiguration(), exchange)) { 108 createMultipartAlternativeMessage(mimeMessage, endpoint.getConfiguration(), exchange); 109 } else { 110 if (exchange.getIn().hasAttachments()) { 111 appendAttachmentsFromCamel(mimeMessage, endpoint.getConfiguration(), exchange); 112 } else { 113 populateContentOnMimeMessage(mimeMessage, endpoint.getConfiguration(), exchange); 114 } 115 } 116 } 117 118 protected String determineContentType(MailConfiguration configuration, Exchange exchange) { 119 // see if we got any content type set 120 String contentType = configuration.getContentType(); 121 if (exchange.getIn().getHeader("contentType") != null) { 122 contentType = exchange.getIn().getHeader("contentType", String.class); 123 } else if (exchange.getIn().getHeader(Exchange.CONTENT_TYPE) != null) { 124 contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); 125 } 126 127 // fix content type to include a space after semi colon if missing 128 if (contentType != null && contentType.contains(";")) { 129 String before = ObjectHelper.before(contentType, ";"); 130 String charset = determineCharSet(configuration, exchange); 131 132 if (before != null && charset == null) { 133 contentType = before.trim(); 134 } else if (before != null && charset != null) { 135 contentType = before.trim() + "; charset=" + charset; 136 } 137 } 138 139 if (LOG.isTraceEnabled()) { 140 LOG.trace("Determined Content-Type: " + contentType); 141 } 142 143 return contentType; 144 } 145 146 protected String determineCharSet(MailConfiguration configuration, Exchange exchange) { 147 148 // see if we got any content type set 149 String contentType = configuration.getContentType(); 150 if (exchange.getIn().getHeader("contentType") != null) { 151 contentType = exchange.getIn().getHeader("contentType", String.class); 152 } else if (exchange.getIn().getHeader(Exchange.CONTENT_TYPE) != null) { 153 contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); 154 } 155 156 // fix content type to include a space after semi colon if missing 157 if (contentType != null && contentType.contains(";")) { 158 String after = ObjectHelper.after(contentType, ";"); 159 160 // after is the charset lets see if its given and a valid charset 161 if (after != null) { 162 String charset = ObjectHelper.after(after, "="); 163 if (charset != null) { 164 boolean supported; 165 try { 166 supported = Charset.isSupported(charset); 167 } catch (IllegalCharsetNameException e) { 168 supported = false; 169 } 170 if (supported) { 171 return charset; 172 } else if (!configuration.isIgnoreUnsupportedCharset()) { 173 return charset; 174 } else if (configuration.isIgnoreUnsupportedCharset()) { 175 LOG.warn("Charset: " + charset + " is not supported, will fallback to use platform default instead."); 176 return null; 177 } 178 } 179 } 180 } 181 return null; 182 } 183 184 protected String populateContentOnMimeMessage(MimeMessage part, MailConfiguration configuration, Exchange exchange) 185 throws MessagingException, IOException { 186 187 String contentType = determineContentType(configuration, exchange); 188 189 if (LOG.isTraceEnabled()) { 190 LOG.trace("Using Content-Type " + contentType + " for MimeMessage: " + part); 191 } 192 193 // always store content in a byte array data store to avoid various content type and charset issues 194 DataSource ds = new ByteArrayDataSource(exchange.getIn().getBody(String.class), contentType); 195 part.setDataHandler(new DataHandler(ds)); 196 197 // set the content type header afterwards 198 part.setHeader("Content-Type", contentType); 199 200 return contentType; 201 } 202 203 protected String populateContentOnBodyPart(BodyPart part, MailConfiguration configuration, Exchange exchange) 204 throws MessagingException, IOException { 205 206 String contentType = determineContentType(configuration, exchange); 207 208 if (LOG.isTraceEnabled()) { 209 LOG.trace("Using Content-Type " + contentType + " for BodyPart: " + part); 210 } 211 212 // always store content in a byte array data store to avoid various content type and charset issues 213 DataSource ds = new ByteArrayDataSource(exchange.getIn().getBody(String.class), contentType); 214 part.setDataHandler(new DataHandler(ds)); 215 216 // set the content type header afterwards 217 part.setHeader("Content-Type", contentType); 218 219 return contentType; 220 } 221 222 /** 223 * Extracts the body from the Mail message 224 */ 225 public Object extractBodyFromMail(Exchange exchange, MailMessage mailMessage) { 226 Message message = mailMessage.getMessage(); 227 try { 228 return message.getContent(); 229 } catch (Exception e) { 230 // try to fix message in case it has an unsupported encoding in the Content-Type header 231 UnsupportedEncodingException uee = ObjectHelper.getException(UnsupportedEncodingException.class, e); 232 if (uee != null) { 233 LOG.debug("Unsupported encoding detected: " + uee.getMessage()); 234 try { 235 String contentType = message.getContentType(); 236 String type = ObjectHelper.before(contentType, "charset="); 237 if (type != null) { 238 // try again with fixed content type 239 LOG.debug("Trying to extract mail message again with fixed Content-Type: " + type); 240 // Since message is read-only, we need to use a copy 241 MimeMessage messageCopy = new MimeMessage((MimeMessage)message); 242 messageCopy.setHeader("Content-Type", type); 243 Object body = messageCopy.getContent(); 244 // If we got this far, our fix worked... 245 // Replace the MailMessage's Message with the copy 246 mailMessage.setMessage(messageCopy); 247 return body; 248 } 249 } catch (Exception e2) { 250 // fall through and let original exception be thrown 251 } 252 } 253 254 throw new RuntimeCamelException("Failed to extract body due to: " + e.getMessage() 255 + ". Exchange: " + exchange + ". Message: " + message, e); 256 } 257 } 258 259 /** 260 * Parses the attachments of the given mail message and adds them to the map 261 * 262 * @param message the mail message with attachments 263 * @param map the map to add found attachments (attachmentFilename is the key) 264 */ 265 public void extractAttachmentsFromMail(Message message, Map<String, DataHandler> map) 266 throws javax.mail.MessagingException, IOException { 267 268 LOG.trace("Extracting attachments +++ start +++"); 269 270 Object content = message.getContent(); 271 if (content instanceof Multipart) { 272 extractAttachmentsFromMultipart((Multipart)content, map); 273 } else if (content != null) { 274 LOG.trace("No attachments to extract as content is not Multipart: " + content.getClass().getName()); 275 } 276 277 LOG.trace("Extracting attachments +++ done +++"); 278 } 279 280 protected void extractAttachmentsFromMultipart(Multipart mp, Map<String, DataHandler> map) 281 throws javax.mail.MessagingException, IOException { 282 283 for (int i = 0; i < mp.getCount(); i++) { 284 Part part = mp.getBodyPart(i); 285 LOG.trace("Part #" + i + ": " + part); 286 287 if (part.isMimeType("multipart/*")) { 288 LOG.trace("Part #" + i + ": is mimetype: multipart/*"); 289 extractAttachmentsFromMultipart((Multipart)part.getContent(), map); 290 } else { 291 String disposition = part.getDisposition(); 292 if (LOG.isTraceEnabled()) { 293 LOG.trace("Part #" + i + ": Disposition: " + part.getDisposition()); 294 LOG.trace("Part #" + i + ": Description: " + part.getDescription()); 295 LOG.trace("Part #" + i + ": ContentType: " + part.getContentType()); 296 LOG.trace("Part #" + i + ": FileName: " + part.getFileName()); 297 LOG.trace("Part #" + i + ": Size: " + part.getSize()); 298 LOG.trace("Part #" + i + ": LineCount: " + part.getLineCount()); 299 } 300 301 if (disposition != null && (disposition.equalsIgnoreCase(Part.ATTACHMENT) || disposition.equalsIgnoreCase(Part.INLINE))) { 302 // only add named attachments 303 String fileName = part.getFileName(); 304 if (fileName != null) { 305 LOG.debug("Mail contains file attachment: " + fileName); 306 // Parts marked with a disposition of Part.ATTACHMENT are clearly attachments 307 CollectionHelper.appendValue(map, fileName, part.getDataHandler()); 308 } 309 } 310 } 311 } 312 } 313 314 /** 315 * Appends the Mail headers from the Camel {@link MailMessage} 316 */ 317 protected void appendHeadersFromCamelMessage(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 318 throws MessagingException { 319 320 for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) { 321 String headerName = entry.getKey(); 322 Object headerValue = entry.getValue(); 323 if (headerValue != null) { 324 if (headerFilterStrategy != null 325 && !headerFilterStrategy.applyFilterToCamelHeaders(headerName, headerValue, exchange)) { 326 327 if (isRecipientHeader(headerName)) { 328 // skip any recipients as they are handled specially 329 continue; 330 } 331 332 // alternative body should also be skipped 333 if (headerName.equalsIgnoreCase(configuration.getAlternativeBodyHeader())) { 334 // skip alternative body 335 continue; 336 } 337 338 // Mail messages can repeat the same header... 339 if (ObjectConverter.isCollection(headerValue)) { 340 Iterator iter = ObjectHelper.createIterator(headerValue); 341 while (iter.hasNext()) { 342 Object value = iter.next(); 343 mimeMessage.addHeader(headerName, asString(exchange, value)); 344 } 345 } else { 346 mimeMessage.setHeader(headerName, asString(exchange, headerValue)); 347 } 348 } 349 } 350 } 351 } 352 353 private void setRecipientFromCamelMessage(MimeMessage mimeMessage, Exchange exchange) throws MessagingException { 354 for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) { 355 String headerName = entry.getKey(); 356 Object headerValue = entry.getValue(); 357 if (headerValue != null && isRecipientHeader(headerName)) { 358 // special handling of recipients 359 if (ObjectConverter.isCollection(headerValue)) { 360 Iterator iter = ObjectHelper.createIterator(headerValue); 361 while (iter.hasNext()) { 362 Object recipient = iter.next(); 363 appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, recipient)); 364 } 365 } else { 366 appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, headerValue)); 367 } 368 } 369 } 370 } 371 372 /** 373 * Appends the Mail headers from the endpoint configuration. 374 */ 375 protected void setRecipientFromEndpointConfiguration(MimeMessage mimeMessage, MailEndpoint endpoint) 376 throws MessagingException { 377 378 Map<Message.RecipientType, String> recipients = endpoint.getConfiguration().getRecipients(); 379 if (recipients.containsKey(Message.RecipientType.TO)) { 380 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.TO.toString(), recipients.get(Message.RecipientType.TO)); 381 } 382 if (recipients.containsKey(Message.RecipientType.CC)) { 383 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.CC.toString(), recipients.get(Message.RecipientType.CC)); 384 } 385 if (recipients.containsKey(Message.RecipientType.BCC)) { 386 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.BCC.toString(), recipients.get(Message.RecipientType.BCC)); 387 } 388 } 389 390 /** 391 * Appends the Mail attachments from the Camel {@link MailMessage} 392 */ 393 protected void appendAttachmentsFromCamel(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 394 throws MessagingException, IOException { 395 396 // Put parts in message 397 mimeMessage.setContent(createMixedMultipartAttachments(configuration, exchange)); 398 } 399 400 private MimeMultipart createMixedMultipartAttachments(MailConfiguration configuration, Exchange exchange) 401 throws MessagingException, IOException { 402 403 // fill the body with text 404 MimeMultipart multipart = new MimeMultipart(); 405 multipart.setSubType("mixed"); 406 addBodyToMultipart(configuration, multipart, exchange); 407 String partDisposition = configuration.isUseInlineAttachments() ? Part.INLINE : Part.ATTACHMENT; 408 if (exchange.getIn().hasAttachments()) { 409 addAttachmentsToMultipart(multipart, partDisposition, exchange); 410 } 411 return multipart; 412 } 413 414 protected void addAttachmentsToMultipart(MimeMultipart multipart, String partDisposition, Exchange exchange) throws MessagingException { 415 LOG.trace("Adding attachments +++ start +++"); 416 int i = 0; 417 for (Map.Entry<String, DataHandler> entry : exchange.getIn().getAttachments().entrySet()) { 418 String attachmentFilename = entry.getKey(); 419 DataHandler handler = entry.getValue(); 420 421 if (LOG.isTraceEnabled()) { 422 LOG.trace("Attachment #" + i + ": Disposition: " + partDisposition); 423 LOG.trace("Attachment #" + i + ": DataHandler: " + handler); 424 LOG.trace("Attachment #" + i + ": FileName: " + attachmentFilename); 425 } 426 if (handler != null) { 427 if (shouldAddAttachment(exchange, attachmentFilename, handler)) { 428 // Create another body part 429 BodyPart messageBodyPart = new MimeBodyPart(); 430 // Set the data handler to the attachment 431 messageBodyPart.setDataHandler(handler); 432 433 if (attachmentFilename.toLowerCase().startsWith("cid:")) { 434 // add a Content-ID header to the attachment 435 // must use angle brackets according to RFC: http://www.ietf.org/rfc/rfc2392.txt 436 messageBodyPart.addHeader("Content-ID", "<" + attachmentFilename.substring(4) + ">"); 437 // Set the filename without the cid 438 messageBodyPart.setFileName(attachmentFilename.substring(4)); 439 } else { 440 // Set the filename 441 messageBodyPart.setFileName(attachmentFilename); 442 } 443 444 LOG.trace("Attachment #" + i + ": ContentType: " + messageBodyPart.getContentType()); 445 446 if (contentTypeResolver != null) { 447 String contentType = contentTypeResolver.resolveContentType(attachmentFilename); 448 LOG.trace("Attachment #" + i + ": Using content type resolver: " + contentTypeResolver + " resolved content type as: " + contentType); 449 if (contentType != null) { 450 String value = contentType + "; name=" + attachmentFilename; 451 messageBodyPart.setHeader("Content-Type", value); 452 LOG.trace("Attachment #" + i + ": ContentType: " + messageBodyPart.getContentType()); 453 } 454 } 455 456 // Set Disposition 457 messageBodyPart.setDisposition(partDisposition); 458 // Add part to multipart 459 multipart.addBodyPart(messageBodyPart); 460 } else { 461 LOG.trace("shouldAddAttachment: false"); 462 } 463 } else { 464 LOG.warn("Cannot add attachment: " + attachmentFilename + " as DataHandler is null"); 465 } 466 i++; 467 } 468 LOG.trace("Adding attachments +++ done +++"); 469 } 470 471 protected void createMultipartAlternativeMessage(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 472 throws MessagingException, IOException { 473 474 MimeMultipart multipartAlternative = new MimeMultipart("alternative"); 475 mimeMessage.setContent(multipartAlternative); 476 477 MimeBodyPart plainText = new MimeBodyPart(); 478 plainText.setText(getAlternativeBody(configuration, exchange), determineCharSet(configuration, exchange)); 479 // remove the header with the alternative mail now that we got it 480 // otherwise it might end up twice in the mail reader 481 exchange.getIn().removeHeader(configuration.getAlternativeBodyHeader()); 482 multipartAlternative.addBodyPart(plainText); 483 484 // if there are no attachments, add the body to the same mulitpart message 485 if (!exchange.getIn().hasAttachments()) { 486 addBodyToMultipart(configuration, multipartAlternative, exchange); 487 } else { 488 // if there are attachments, but they aren't set to be inline, add them to 489 // treat them as normal. It will append a multipart-mixed with the attachments and the body text 490 if (!configuration.isUseInlineAttachments()) { 491 BodyPart mixedAttachments = new MimeBodyPart(); 492 mixedAttachments.setContent(createMixedMultipartAttachments(configuration, exchange)); 493 multipartAlternative.addBodyPart(mixedAttachments); 494 } else { 495 // if the attachments are set to be inline, attach them as inline attachments 496 MimeMultipart multipartRelated = new MimeMultipart("related"); 497 BodyPart related = new MimeBodyPart(); 498 499 related.setContent(multipartRelated); 500 multipartAlternative.addBodyPart(related); 501 502 addBodyToMultipart(configuration, multipartRelated, exchange); 503 504 addAttachmentsToMultipart(multipartRelated, Part.INLINE, exchange); 505 } 506 } 507 } 508 509 protected void addBodyToMultipart(MailConfiguration configuration, MimeMultipart activeMultipart, Exchange exchange) 510 throws MessagingException, IOException { 511 512 BodyPart bodyMessage = new MimeBodyPart(); 513 populateContentOnBodyPart(bodyMessage, configuration, exchange); 514 activeMultipart.addBodyPart(bodyMessage); 515 } 516 517 /** 518 * Strategy to allow filtering of attachments which are added on the Mail message 519 */ 520 protected boolean shouldAddAttachment(Exchange exchange, String attachmentFilename, DataHandler handler) { 521 return true; 522 } 523 524 protected Map<String, Object> extractHeadersFromMail(Message mailMessage, Exchange exchange) throws MessagingException { 525 Map<String, Object> answer = new HashMap<String, Object>(); 526 Enumeration names = mailMessage.getAllHeaders(); 527 528 while (names.hasMoreElements()) { 529 Header header = (Header) names.nextElement(); 530 String value = header.getValue(); 531 if (headerFilterStrategy != null && !headerFilterStrategy.applyFilterToExternalHeaders(header.getName(), value, exchange)) { 532 CollectionHelper.appendValue(answer, header.getName(), value); 533 } 534 } 535 536 return answer; 537 } 538 539 private static void appendRecipientToMimeMessage(MimeMessage mimeMessage, String type, String recipient) 540 throws MessagingException { 541 542 // we support that multi recipient can be given as a string separated by comma or semicolon 543 String[] lines = recipient.split("[,;]"); 544 for (String line : lines) { 545 line = line.trim(); 546 mimeMessage.addRecipients(asRecipientType(type), line); 547 } 548 } 549 550 /** 551 * Does the given camel message contain any To, CC or BCC header names? 552 */ 553 private static boolean hasRecipientHeaders(Exchange exchange) { 554 for (String key : exchange.getIn().getHeaders().keySet()) { 555 if (isRecipientHeader(key)) { 556 return true; 557 } 558 } 559 return false; 560 } 561 562 protected static boolean hasAlternativeBody(MailConfiguration configuration, Exchange exchange) { 563 return getAlternativeBody(configuration, exchange) != null; 564 } 565 566 protected static String getAlternativeBody(MailConfiguration configuration, Exchange exchange) { 567 String alternativeBodyHeader = configuration.getAlternativeBodyHeader(); 568 return exchange.getIn().getHeader(alternativeBodyHeader, java.lang.String.class); 569 } 570 571 /** 572 * Is the given key a mime message recipient header (To, CC or BCC) 573 */ 574 private static boolean isRecipientHeader(String key) { 575 if (Message.RecipientType.TO.toString().equalsIgnoreCase(key)) { 576 return true; 577 } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(key)) { 578 return true; 579 } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(key)) { 580 return true; 581 } 582 return false; 583 } 584 585 /** 586 * Returns the RecipientType object. 587 */ 588 private static Message.RecipientType asRecipientType(String type) { 589 if (Message.RecipientType.TO.toString().equalsIgnoreCase(type)) { 590 return Message.RecipientType.TO; 591 } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(type)) { 592 return Message.RecipientType.CC; 593 } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(type)) { 594 return Message.RecipientType.BCC; 595 } 596 throw new IllegalArgumentException("Unknown recipient type: " + type); 597 } 598 599 600 private static boolean empty(Address[] addresses) { 601 return addresses == null || addresses.length == 0; 602 } 603 604 private static String asString(Exchange exchange, Object value) { 605 return exchange.getContext().getTypeConverter().convertTo(String.class, exchange, value); 606 } 607 608 }