1 /* 2 * Copyright 2002-2018 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 module hunt.stomp.support.MessageHeaderAccessor; 18 19 import hunt.stomp.IdGenerator; 20 import hunt.stomp.Message; 21 import hunt.stomp.MessageChannel; 22 import hunt.stomp.MessageHeaders; 23 24 import hunt.collection; 25 import hunt.Exceptions; 26 import hunt.Nullable; 27 import hunt.Long; 28 import hunt.Object; 29 import hunt.text.Charset; 30 import hunt.text.PatternMatchUtils; 31 import hunt.util.DateTime; 32 import hunt.util.MimeType; 33 import hunt.util.ObjectUtils; 34 35 import std.algorithm; 36 import std.conv; 37 import std.range.primitives; 38 import std.string; 39 import std.uuid; 40 41 42 /** 43 * Callback interface for initializing a {@link MessageHeaderAccessor}. 44 * 45 * @author Rossen Stoyanchev 46 * @since 4.1 47 */ 48 interface MessageHeaderInitializer { 49 50 /** 51 * Initialize the given {@code MessageHeaderAccessor}. 52 * @param headerAccessor the MessageHeaderAccessor to initialize 53 */ 54 void initHeaders(MessageHeaderAccessor headerAccessor); 55 56 } 57 58 alias Charset = string; 59 60 /** 61 * A base for classes providing strongly typed getters and setters as well as 62 * behavior around specific categories of headers (e.g. STOMP headers). 63 * Supports creating new headers, modifying existing headers (when still mutable), 64 * or copying and modifying existing headers. 65 * 66 * <p>The method {@link #getMessageHeaders()} provides access to the underlying, 67 * fully-prepared {@link MessageHeaders} that can then be used as-is (i.e. 68 * without copying) to create a single message as follows: 69 * 70 * <pre class="code"> 71 * MessageHeaderAccessor accessor = new MessageHeaderAccessor(); 72 * accessor.setHeader("foo", "bar"); 73 * Message message = MessageHelper.createMessage("payload", accessor.getMessageHeaders()); 74 * </pre> 75 * 76 * <p>After the above, by default the {@code MessageHeaderAccessor} becomes 77 * immutable. However it is possible to leave it mutable for further initialization 78 * in the same thread, for example: 79 * 80 * <pre class="code"> 81 * MessageHeaderAccessor accessor = new MessageHeaderAccessor(); 82 * accessor.setHeader("foo", "bar"); 83 * accessor.setLeaveMutable(true); 84 * Message message = MessageHelper.createMessage("payload", accessor.getMessageHeaders()); 85 * 86 * // later on in the same thread... 87 * 88 * MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message); 89 * accessor.setHeader("bar", "baz"); 90 * accessor.setImmutable(); 91 * </pre> 92 * 93 * <p>The method {@link #toMap()} returns a copy of the underlying headers. It can 94 * be used to prepare multiple messages from the same {@code MessageHeaderAccessor} 95 * instance: 96 * <pre class="code"> 97 * MessageHeaderAccessor accessor = new MessageHeaderAccessor(); 98 * MessageBuilder builder = MessageBuilder.withPayload("payload").setHeaders(accessor); 99 * 100 * accessor.setHeader("foo", "bar1"); 101 * Message message1 = builder.build(); 102 * 103 * accessor.setHeader("foo", "bar2"); 104 * Message message2 = builder.build(); 105 * 106 * accessor.setHeader("foo", "bar3"); 107 * Message message3 = builder.build(); 108 * </pre> 109 * 110 * <p>However note that with the above style, the header accessor is shared and 111 * cannot be re-obtained later on. Alternatively it is also possible to create 112 * one {@code MessageHeaderAccessor} per message: 113 * 114 * <pre class="code"> 115 * MessageHeaderAccessor accessor1 = new MessageHeaderAccessor(); 116 * accessor.set("foo", "bar1"); 117 * Message message1 = MessageHelper.createMessage("payload", accessor1.getMessageHeaders()); 118 * 119 * MessageHeaderAccessor accessor2 = new MessageHeaderAccessor(); 120 * accessor.set("foo", "bar2"); 121 * Message message2 = MessageHelper.createMessage("payload", accessor2.getMessageHeaders()); 122 * 123 * MessageHeaderAccessor accessor3 = new MessageHeaderAccessor(); 124 * accessor.set("foo", "bar3"); 125 * Message message3 = MessageHelper.createMessage("payload", accessor3.getMessageHeaders()); 126 * </pre> 127 * 128 * <p>Note that the above examples aim to demonstrate the general idea of using 129 * header accessors. The most likely usage however is through subclasses. 130 * 131 * @author Rossen Stoyanchev 132 * @author Juergen Hoeller 133 * @since 4.0 134 */ 135 class MessageHeaderAccessor { 136 137 /** 138 * The default charset used for headers. 139 */ 140 enum Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 141 142 private __gshared MimeType[] READABLE_MIME_TYPES; 143 144 private MutableMessageHeaders headers; 145 146 private bool leaveMutable = false; 147 148 private bool modified = false; 149 150 private bool enableTimestamp = false; 151 152 private IdGenerator idGenerator; 153 154 shared static this() { 155 READABLE_MIME_TYPES = [ 156 MimeType.APPLICATION_JSON, MimeType.APPLICATION_XML, 157 new MimeType("text", new MimeType("*")), 158 new MimeType("application", new MimeType("*+json")), 159 new MimeType("application", new MimeType("*+xml")) 160 ]; 161 } 162 163 164 /** 165 * A constructor to create new headers. 166 */ 167 this() { 168 this(null); 169 } 170 171 /** 172 * A constructor accepting the headers of an existing message to copy. 173 * @param message a message to copy the headers from, or {@code null} if none 174 */ 175 this(MessageBase message) { 176 this.headers = new MutableMessageHeaders(message !is null ? message.getHeaders() : null); 177 } 178 179 180 /** 181 * Build a 'nested' accessor for the given message. 182 * @param message the message to build a new accessor for 183 * @return the nested accessor (typically a specific subclass) 184 */ 185 protected MessageHeaderAccessor createAccessor(MessageBase message) { 186 return new MessageHeaderAccessor(message); 187 } 188 189 190 // Configuration properties 191 192 /** 193 * By default when {@link #getMessageHeaders()} is called, {@code "this"} 194 * {@code MessageHeaderAccessor} instance can no longer be used to modify the 195 * underlying message headers and the returned {@code MessageHeaders} is immutable. 196 * <p>However when this is set to {@code true}, the returned (underlying) 197 * {@code MessageHeaders} instance remains mutable. To make further modifications 198 * continue to use the same accessor instance or re-obtain it via:<br> 199 * {@link MessageHeaderAccessor#getAccessor(Message, Class) 200 * MessageHeaderAccessor.getAccessor(Message, Class)} 201 * <p>When modifications are complete use {@link #setImmutable()} to prevent 202 * further changes. The intended use case for this mechanism is initialization 203 * of a Message within a single thread. 204 * <p>By default this is set to {@code false}. 205 * @since 4.1 206 */ 207 void setLeaveMutable(bool leaveMutable) { 208 assert(this.headers.isMutable(), "Already immutable"); 209 this.leaveMutable = leaveMutable; 210 } 211 212 /** 213 * By default when {@link #getMessageHeaders()} is called, {@code "this"} 214 * {@code MessageHeaderAccessor} instance can no longer be used to modify the 215 * underlying message headers. However if {@link #setLeaveMutable()} 216 * is used, this method is necessary to indicate explicitly when the 217 * {@code MessageHeaders} instance should no longer be modified. 218 * @since 4.1 219 */ 220 void setImmutable() { 221 this.headers.setImmutable(); 222 } 223 224 /** 225 * Whether the underlying headers can still be modified. 226 * @since 4.1 227 */ 228 bool isMutable() { 229 return this.headers.isMutable(); 230 } 231 232 /** 233 * Mark the underlying message headers as modified. 234 * @param modified typically {@code true}, or {@code false} to reset the flag 235 * @since 4.1 236 */ 237 protected void setModified(bool modified) { 238 this.modified = modified; 239 } 240 241 /** 242 * Check whether the underlying message headers have been marked as modified. 243 * @return {@code true} if the flag has been set, {@code false} otherwise 244 */ 245 bool isModified() { 246 return this.modified; 247 } 248 249 /** 250 * A package private mechanism to enables the automatic addition of the 251 * {@link hunt.stomp.MessageHeaders#TIMESTAMP} header. 252 * <p>By default, this property is set to {@code false}. 253 * @see IdTimestampMessageHeaderInitializer 254 */ 255 void setEnableTimestamp(bool enableTimestamp) { 256 this.enableTimestamp = enableTimestamp; 257 } 258 259 /** 260 * A package-private mechanism to configure the IdGenerator strategy to use. 261 * <p>By default this property is not set in which case the default IdGenerator 262 * in {@link hunt.stomp.MessageHeaders} is used. 263 * @see IdTimestampMessageHeaderInitializer 264 */ 265 void setIdGenerator(IdGenerator idGenerator) { 266 this.idGenerator = idGenerator; 267 } 268 269 270 // Accessors for the resulting MessageHeaders 271 272 /** 273 * Return the underlying {@code MessageHeaders} instance. 274 * <p>Unless {@link #setLeaveMutable()} was set to {@code true}, after 275 * this call, the headers are immutable and this accessor can no longer 276 * modify them. 277 * <p>This method always returns the same {@code MessageHeaders} instance if 278 * invoked multiples times. To obtain a copy of the underlying headers, use 279 * {@link #toMessageHeaders()} or {@link #toMap()} instead. 280 * @since 4.1 281 */ 282 MessageHeaders getMessageHeaders() { 283 if (!this.leaveMutable) { 284 setImmutable(); 285 } 286 return this.headers; 287 } 288 289 /** 290 * Return a copy of the underlying header values as a {@link MessageHeaders} object. 291 * <p>This method can be invoked many times, with modifications in between 292 * where each new call returns a fresh copy of the current header values. 293 * @since 4.1 294 */ 295 MessageHeaders toMessageHeaders() { 296 return new MessageHeaders(this.headers); 297 } 298 299 /** 300 * Return a copy of the underlying header values as a plain {@link Map} object. 301 * <p>This method can be invoked many times, with modifications in between 302 * where each new call returns a fresh copy of the current header values. 303 */ 304 Map!(string, Object) toMap() { 305 return new HashMap!(string, Object)(this.headers); 306 } 307 308 309 // Generic header accessors 310 311 /** 312 * Retrieve the value for the header with the given name. 313 * @param headerName the name of the header 314 * @return the associated value, or {@code null} if none found 315 */ 316 317 Object getHeader(string headerName) { 318 return this.headers.get(headerName); 319 } 320 321 T getHeaderAs(T)(string headerName) { 322 return this.headers.getAs!(T)(headerName); 323 } 324 325 void setHeader(T)(string name, T value) if(!is(T == class)) { 326 setHeader(name, new Nullable!T(value)); 327 } 328 329 /** 330 * Set the value for the given header name. 331 * <p>If the provided value is {@code null}, the header will be removed. 332 */ 333 void setHeader(string name, Object value) { 334 if (isReadOnly(name)) { 335 throw new IllegalArgumentException("'" ~ name ~ "' header is read-only"); 336 } 337 verifyType(name, value); 338 if (value !is null) { 339 // Modify header if necessary 340 if (!ObjectUtils.nullSafeEquals(value, getHeader(name))) { 341 this.modified = true; 342 this.headers.getRawHeaders().put(name, value); 343 } 344 } 345 else { 346 // Remove header if available 347 if (this.headers.containsKey(name)) { 348 this.modified = true; 349 this.headers.getRawHeaders().remove(name); 350 } 351 } 352 } 353 354 protected void verifyType(string headerName, Object headerValue) { 355 if (!headerName.empty && headerValue !is null) { 356 if (MessageHeaders.ERROR_CHANNEL == headerName || 357 MessageHeaders.REPLY_CHANNEL.endsWith(headerName)) { 358 MessageChannel mc = cast(MessageChannel)headerValue; 359 Nullable!string str = cast(Nullable!string)headerValue; 360 361 if (mc is null && str is null) { 362 throw new IllegalArgumentException( 363 "'" ~ headerName ~ "' header value must be a MessageChannel or string"); 364 } 365 } 366 } 367 } 368 369 /** 370 * Set the value for the given header name only if the header name is not 371 * already associated with a value. 372 */ 373 void setHeaderIfAbsent(string name, Object value) { 374 if (getHeader(name) is null) { 375 setHeader(name, value); 376 } 377 } 378 379 /** 380 * Remove the value for the given header name. 381 */ 382 void removeHeader(string headerName) { 383 if (!headerName.empty && !isReadOnly(headerName)) { 384 setHeader(headerName, null); 385 } 386 } 387 388 /** 389 * Removes all headers provided via array of 'headerPatterns'. 390 * <p>As the name suggests, array may contain simple matching patterns for header 391 * names. Supported pattern styles are: "xxx*", "*xxx", "*xxx*" and "xxx*yyy". 392 */ 393 void removeHeaders(string[] headerPatterns...) { 394 List!(string) headersToRemove = new ArrayList!string(); 395 foreach (string pattern ; headerPatterns) { 396 if (!pattern.empty()){ 397 if (pattern.canFind("*")){ 398 headersToRemove.addAll(getMatchingHeaderNames(pattern, this.headers)); 399 } 400 else { 401 headersToRemove.add(pattern); 402 } 403 } 404 } 405 foreach (string headerToRemove ; headersToRemove) { 406 removeHeader(headerToRemove); 407 } 408 } 409 410 private List!(string) getMatchingHeaderNames(string pattern, Map!(string, Object) headers) { 411 List!(string) matchingHeaderNames = new ArrayList!string(); 412 if (headers !is null) { 413 foreach (string key ; headers.byKey) { 414 if (PatternMatchUtils.simpleMatch(pattern, key)) { 415 matchingHeaderNames.add(key); 416 } 417 } 418 } 419 return matchingHeaderNames; 420 } 421 422 /** 423 * Copy the name-value pairs from the provided Map. 424 * <p>This operation will overwrite any existing values. Use 425 * {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. 426 */ 427 void copyHeaders(Map!(string, Object) headersToCopy) { 428 if (headersToCopy !is null) { 429 foreach(string key, Object value; headersToCopy) { 430 if (!isReadOnly(key)) setHeader(key, value); 431 } 432 } 433 } 434 435 /** 436 * Copy the name-value pairs from the provided Map. 437 * <p>This operation will <em>not</em> overwrite any existing values. 438 */ 439 void copyHeadersIfAbsent(Map!(string, Object) headersToCopy) { 440 if (headersToCopy !is null) { 441 foreach(string key, Object value; headersToCopy) { 442 if (!isReadOnly(key)) setHeaderIfAbsent(key, value); 443 } 444 } 445 } 446 447 protected bool isReadOnly(string headerName) { 448 return (MessageHeaders.ID == (headerName) || MessageHeaders.TIMESTAMP == (headerName)); 449 } 450 451 452 // Specific header accessors 453 454 UUID getId() { 455 Object value = getHeader(MessageHeaders.ID); 456 if (value is null) { 457 return UUID.init; 458 } 459 460 auto uu = cast(Nullable!UUID)value; 461 if(uu is null) { 462 auto us = cast(Nullable!string)value; 463 if(us is null) 464 throw new Exception("bad type"); 465 else 466 return UUID(us.value); 467 } else { 468 return uu.value; 469 } 470 471 // return (value instanceof UUID ? (UUID) value : UUID.fromString(value.toString())); 472 } 473 474 475 Long getTimestamp() { 476 Object value = getHeader(MessageHeaders.TIMESTAMP); 477 if (value is null) { 478 return null; 479 } 480 481 auto vl = cast(Long)value; 482 if(vl is null) { 483 auto vs = cast(Nullable!string)value; 484 if(vs is null) 485 throw new Exception("bad type"); 486 else 487 return new Long(vs.value); 488 } else { 489 return vl; 490 } 491 492 // return (value instanceof Long ? (Long) value : Long.parseLong(value.toString())); 493 } 494 495 void setContentType(MimeType contentType) { 496 setHeader(MessageHeaders.CONTENT_TYPE, contentType); 497 } 498 499 500 MimeType getContentType() { 501 Object value = getHeader(MessageHeaders.CONTENT_TYPE); 502 if (value is null) { 503 return null; 504 } 505 MimeType mt = cast(MimeType) value; 506 if(mt !is null) 507 return mt; 508 509 auto vs = cast(Nullable!string)value; 510 if(vs is null) 511 throw new Exception("bad type"); 512 else 513 return new MimeType(vs.value); 514 // return (value instanceof MimeType ? (MimeType) value : MimeType.valueOf(value.toString())); 515 } 516 517 private Charset getCharset() { 518 MimeType contentType = getContentType(); 519 Charset charset = (contentType !is null ? contentType.getCharsetString() : null); 520 return (charset !is null ? charset : DEFAULT_CHARSET); 521 } 522 523 void setReplyChannelName(string replyChannelName) { 524 setHeader(MessageHeaders.REPLY_CHANNEL, new Nullable!string(replyChannelName)); 525 } 526 527 void setReplyChannel(MessageChannel replyChannel) { 528 setHeader(MessageHeaders.REPLY_CHANNEL, cast(Object)replyChannel); 529 } 530 531 532 Object getReplyChannel() { 533 return getHeader(MessageHeaders.REPLY_CHANNEL); 534 } 535 536 void setErrorChannelName(string errorChannelName) { 537 setHeader(MessageHeaders.ERROR_CHANNEL, new Nullable!string(errorChannelName)); 538 } 539 540 void setErrorChannel(MessageChannel errorChannel) { 541 setHeader(MessageHeaders.ERROR_CHANNEL, cast(Object)errorChannel); 542 } 543 544 545 Object getErrorChannel() { 546 return getHeader(MessageHeaders.ERROR_CHANNEL); 547 } 548 549 550 // Log message stuff 551 552 /** 553 * Return a concise message for logging purposes. 554 * @param payload the payload that corresponds to the headers. 555 * @return the message 556 */ 557 string getShortLogMessage(Object payload) { 558 return "headers=" ~ this.headers.toString() ~ getShortPayloadLogMessage(payload); 559 } 560 561 /** 562 * Return a more detailed message for logging purposes. 563 * @param payload the payload that corresponds to the headers. 564 * @return the message 565 */ 566 string getDetailedLogMessage(Object payload) { 567 return "headers=" ~ this.headers.toString() ~ getDetailedPayloadLogMessage(payload); 568 } 569 570 protected string getShortPayloadLogMessage(Object payload) { 571 auto textPayload = cast(Nullable!string)payload; 572 if (textPayload !is null) { 573 string payloadText = textPayload.value; 574 return (payloadText.length < 80) ? 575 " payload=" ~ payloadText : 576 " payload=" ~ payloadText[0 ..80] ~ "...(truncated)"; 577 } 578 579 auto bytesPayload = cast(Nullable!(byte[]))payload; 580 if (bytesPayload !is null) { 581 byte[] bytes = bytesPayload.value; 582 if (isReadableContentType()) { 583 return (bytes.length < 80) ? 584 " payload=" ~ cast(string)(bytes) : 585 " payload=" ~ cast(string)(bytes[0..80]) ~ "...(truncated)"; 586 } else { 587 return " payload=byte[" ~ to!string(bytes.length) ~ "]"; 588 } 589 } 590 else { 591 string payloadText = payload.toString(); 592 return (payloadText.length < 80) ? 593 " payload=" ~ payloadText : 594 " payload=" ~ ObjectUtils.identityToString(payload); 595 } 596 } 597 598 protected string getDetailedPayloadLogMessage(Object payload) { 599 auto textPayload = cast(Nullable!string)payload; 600 if (textPayload !is null) { 601 return " payload=" ~ textPayload.value; 602 } 603 604 auto bytesPayload = cast(Nullable!(byte[]))payload; 605 if (bytesPayload !is null) { 606 byte[] bytes = bytesPayload.value; 607 if (isReadableContentType()) { 608 return " payload=" ~ cast(string)(bytes); 609 } else { 610 return " payload=byte[" ~ to!string(bytes.length) ~ "]"; 611 } 612 } 613 else { 614 return " payload=" ~ payload.toString(); 615 } 616 } 617 618 protected bool isReadableContentType() { 619 MimeType contentType = getContentType(); 620 // implementationMissing(false); 621 // TODO: Tasks pending completion -@zxp at 11/13/2018, 3:19:07 PM 622 // 623 foreach (MimeType mimeType ; READABLE_MIME_TYPES) { 624 // if (mimeType.includes(contentType)) { 625 // return true; 626 // } 627 } 628 return false; 629 } 630 631 override 632 string toString() { 633 return typeid(this).name ~ " [headers=" ~ this.headers.toString() ~ "]"; 634 } 635 636 637 // Static factory methods 638 639 /** 640 * Return the original {@code MessageHeaderAccessor} used to create the headers 641 * of the given {@code Message}, or {@code null} if that's not available or if 642 * its type does not match the required type. 643 * <p>This is for cases where the existence of an accessor is strongly expected 644 * (followed up with an assertion) or where an accessor will be created otherwise. 645 * @param message the message to get an accessor for 646 * @param requiredType the required accessor type (or {@code null} for any) 647 * @return an accessor instance of the specified type, or {@code null} if none 648 * @since 4.1 649 */ 650 651 static T getAccessor(T)(MessageBase message) { 652 return getAccessor!T(message.getHeaders()); 653 } 654 655 /** 656 * A variation of {@link #getAccessor(hunt.stomp.Message, Class)} 657 * with a {@code MessageHeaders} instance instead of a {@code Message}. 658 * <p>This is for cases when a full message may not have been created yet. 659 * @param messageHeaders the message headers to get an accessor for 660 * @param requiredType the required accessor type (or {@code null} for any) 661 * @return an accessor instance of the specified type, or {@code null} if none 662 * @since 4.1 663 */ 664 665 666 static T getAccessor(T)(MessageHeaders messageHeaders) { 667 MutableMessageHeaders mutableHeaders = cast(MutableMessageHeaders) messageHeaders; 668 669 if (mutableHeaders !is null) { 670 MessageHeaderAccessor headerAccessor = mutableHeaders.getAccessor(); 671 if (typeid(T) == typeid(headerAccessor)) { 672 return cast(T) headerAccessor; 673 } 674 } 675 return null; 676 } 677 678 /** 679 * Return a mutable {@code MessageHeaderAccessor} for the given message attempting 680 * to match the type of accessor used to create the message headers, or otherwise 681 * wrapping the message with a {@code MessageHeaderAccessor} instance. 682 * <p>This is for cases where a header needs to be updated in generic code 683 * while preserving the accessor type for downstream processing. 684 * @return an accessor of the required type (never {@code null}) 685 * @since 4.1 686 */ 687 static MessageHeaderAccessor getMutableAccessor(MessageBase message) { 688 MutableMessageHeaders mutableHeaders = cast(MutableMessageHeaders) message.getHeaders(); 689 if (mutableHeaders !is null) { 690 MessageHeaderAccessor accessor = mutableHeaders.getAccessor(); 691 return (accessor.isMutable() ? accessor : accessor.createAccessor(message)); 692 } 693 return new MessageHeaderAccessor(message); 694 } 695 696 697 698 private class MutableMessageHeaders : MessageHeaders { 699 700 private bool mutable = true; 701 702 this(Map!(string, Object) headers) { 703 super(headers, &MessageHeaders.ID_VALUE_NONE, (-1L)); 704 } 705 706 override 707 Map!(string, Object) getRawHeaders() { 708 assert(this.mutable, "Already immutable"); 709 return super.getRawHeaders(); 710 } 711 712 void setImmutable() { 713 if (!this.mutable) { 714 return; 715 } 716 717 if (getId().empty) { 718 // UUID id = randomUUID(); 719 720 IdGenerator idGenerator = this.outer.idGenerator; 721 if(idGenerator is null) 722 idGenerator = MessageHeaders.getIdGenerator(); 723 UUID id = idGenerator.generateId(); 724 if (id != MessageHeaders.ID_VALUE_NONE) { 725 getRawHeaders().put(ID, new Nullable!UUID(id)); 726 } 727 } 728 729 if (getTimestamp() is null) { 730 if (this.outer.enableTimestamp) { 731 Long timestamp = new Long(DateTime.currentTimeMillis()); 732 getRawHeaders().put(TIMESTAMP, timestamp); 733 } 734 } 735 736 this.mutable = false; 737 } 738 739 bool isMutable() { 740 return this.mutable; 741 } 742 743 MessageHeaderAccessor getAccessor() { 744 return this.outer; 745 } 746 747 protected Object writeReplace() { 748 // Serialize as regular MessageHeaders (without MessageHeaderAccessor reference) 749 return new MessageHeaders(this); 750 } 751 } 752 753 }