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.logging.log4j.message; 018 019import java.util.AbstractMap; 020import java.util.Collections; 021import java.util.Map; 022import java.util.TreeMap; 023 024import org.apache.logging.log4j.util.BiConsumer; 025import org.apache.logging.log4j.util.Chars; 026import org.apache.logging.log4j.util.EnglishEnums; 027import org.apache.logging.log4j.util.IndexedReadOnlyStringMap; 028import org.apache.logging.log4j.util.IndexedStringMap; 029import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable; 030import org.apache.logging.log4j.util.PerformanceSensitive; 031import org.apache.logging.log4j.util.ReadOnlyStringMap; 032import org.apache.logging.log4j.util.SortedArrayStringMap; 033import org.apache.logging.log4j.util.StringBuilders; 034import org.apache.logging.log4j.util.Strings; 035import org.apache.logging.log4j.util.TriConsumer; 036 037/** 038 * Represents a Message that consists of a Map. 039 * <p> 040 * Thread-safety note: the contents of this message can be modified after construction. 041 * When using asynchronous loggers and appenders it is not recommended to modify this message after the message is 042 * logged, because it is undefined whether the logged message string will contain the old values or the modified 043 * values. 044 * </p> 045 * <p> 046 * This class was pulled up from {@link StringMapMessage} to allow for Objects as values. 047 * </p> 048 * @param <M> Allow subclasses to use fluent APIs and override methods that return instances of subclasses. 049 * @param <V> The value type 050 */ 051@PerformanceSensitive("allocation") 052@AsynchronouslyFormattable 053public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStringBuilderFormattable { 054 055 private static final long serialVersionUID = -5031471831131487120L; 056 057 /** 058 * When set as the format specifier causes the Map to be formatted as XML. 059 */ 060 public enum MapFormat { 061 062 /** The map should be formatted as XML. */ 063 XML, 064 065 /** The map should be formatted as JSON. */ 066 JSON, 067 068 /** The map should be formatted the same as documented by {@link AbstractMap#toString()}. */ 069 JAVA, 070 071 /** 072 * The map should be formatted the same as documented by {@link AbstractMap#toString()} but without quotes. 073 * 074 * @since 2.11.2 075 */ 076 JAVA_UNQUOTED; 077 078 /** 079 * Maps a format name to an {@link MapFormat} while ignoring case. 080 * 081 * @param format a MapFormat name 082 * @return a MapFormat 083 */ 084 public static MapFormat lookupIgnoreCase(final String format) { 085 return XML.name().equalsIgnoreCase(format) ? XML // 086 : JSON.name().equalsIgnoreCase(format) ? JSON // 087 : JAVA.name().equalsIgnoreCase(format) ? JAVA // 088 : JAVA_UNQUOTED.name().equalsIgnoreCase(format) ? JAVA_UNQUOTED // 089 : null; 090 } 091 092 /** 093 * All {@code MapFormat} names. 094 * 095 * @return All {@code MapFormat} names. 096 */ 097 public static String[] names() { 098 return new String[] {XML.name(), JSON.name(), JAVA.name(), JAVA_UNQUOTED.name()}; 099 } 100 } 101 102 private final IndexedStringMap data; 103 104 /** 105 * Constructs a new instance. 106 */ 107 public MapMessage() { 108 this.data = new SortedArrayStringMap(); 109 } 110 111 /** 112 * Constructs a new instance. 113 * 114 * @param initialCapacity the initial capacity. 115 */ 116 public MapMessage(final int initialCapacity) { 117 this.data = new SortedArrayStringMap(initialCapacity); 118 } 119 120 /** 121 * Constructs a new instance based on an existing {@link Map}. 122 * @param map The Map. 123 */ 124 public MapMessage(final Map<String, V> map) { 125 this.data = new SortedArrayStringMap(map); 126 } 127 128 @Override 129 public String[] getFormats() { 130 return MapFormat.names(); 131 } 132 133 /** 134 * Returns the data elements as if they were parameters on the logging event. 135 * @return the data elements. 136 */ 137 @Override 138 public Object[] getParameters() { 139 final Object[] result = new Object[data.size()]; 140 for (int i = 0; i < data.size(); i++) { 141 result[i] = data.getValueAt(i); 142 } 143 return result; 144 } 145 146 /** 147 * Returns the message. 148 * @return the message. 149 */ 150 @Override 151 public String getFormat() { 152 return Strings.EMPTY; 153 } 154 155 /** 156 * Returns the message data as an unmodifiable Map. 157 * @return the message data as an unmodifiable map. 158 */ 159 @SuppressWarnings("unchecked") 160 public Map<String, V> getData() { 161 final TreeMap<String, V> result = new TreeMap<>(); // returned map must be sorted 162 for (int i = 0; i < data.size(); i++) { 163 // The Eclipse compiler does not need the typecast to V, but the Oracle compiler sure does. 164 result.put(data.getKeyAt(i), (V) data.getValueAt(i)); 165 } 166 return Collections.unmodifiableMap(result); 167 } 168 169 /** 170 * Returns a read-only view of the message data. 171 * @return the read-only message data. 172 */ 173 public IndexedReadOnlyStringMap getIndexedReadOnlyStringMap() { 174 return data; 175 } 176 177 /** 178 * Clear the data. 179 */ 180 public void clear() { 181 data.clear(); 182 } 183 184 /** 185 * Returns {@code true} if this data structure contains the specified key, {@code false} otherwise. 186 * 187 * @param key the key whose presence to check. May be {@code null}. 188 * @return {@code true} if this data structure contains the specified key, {@code false} otherwise 189 * @since 2.9 190 */ 191 public boolean containsKey(final String key) { 192 return data.containsKey(key); 193 } 194 195 /** 196 * Adds an item to the data Map. 197 * @param candidateKey The name of the data item. 198 * @param value The value of the data item. 199 */ 200 public void put(final String candidateKey, final String value) { 201 if (value == null) { 202 throw new IllegalArgumentException("No value provided for key " + candidateKey); 203 } 204 final String key = toKey(candidateKey); 205 validate(key, value); 206 data.putValue(key, value); 207 } 208 209 /** 210 * Adds all the elements from the specified Map. 211 * @param map The Map to add. 212 */ 213 public void putAll(final Map<String, String> map) { 214 for (final Map.Entry<String, String> entry : map.entrySet()) { 215 data.putValue(entry.getKey(), entry.getValue()); 216 } 217 } 218 219 /** 220 * Retrieves the value of the element with the specified key or null if the key is not present. 221 * @param key The name of the element. 222 * @return The value of the element or null if the key is not present. 223 */ 224 public String get(final String key) { 225 final Object result = data.getValue(key); 226 return ParameterFormatter.deepToString(result); 227 } 228 229 /** 230 * Removes the element with the specified name. 231 * @param key The name of the element. 232 * @return The previous value of the element. 233 */ 234 public String remove(final String key) { 235 final String result = get(key); 236 data.remove(key); 237 return result; 238 } 239 240 /** 241 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 242 * 243 * @return The formatted String. 244 */ 245 public String asString() { 246 return format((MapFormat) null, new StringBuilder()).toString(); 247 } 248 249 /** 250 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 251 * 252 * @param format The format identifier. 253 * @return The formatted String. 254 */ 255 public String asString(final String format) { 256 try { 257 return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString(); 258 } catch (final IllegalArgumentException ex) { 259 return asString(); 260 } 261 } 262 263 /** 264 * Performs the given action for each key-value pair in this data structure 265 * until all entries have been processed or the action throws an exception. 266 * <p> 267 * Some implementations may not support structural modifications (adding new elements or removing elements) while 268 * iterating over the contents. In such implementations, attempts to add or remove elements from the 269 * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a 270 * {@code ConcurrentModificationException} to be thrown. 271 * </p> 272 * 273 * @param action The action to be performed for each key-value pair in this collection 274 * @param <CV> type of the consumer value 275 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications 276 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or 277 * {@link #forEach(TriConsumer, Object)}. 278 * @see ReadOnlyStringMap#forEach(BiConsumer) 279 * @since 2.9 280 */ 281 public <CV> void forEach(final BiConsumer<String, ? super CV> action) { 282 data.forEach(action); 283 } 284 285 /** 286 * Performs the given action for each key-value pair in this data structure 287 * until all entries have been processed or the action throws an exception. 288 * <p> 289 * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs, 290 * so the TriConsumer implementation itself can be stateless and potentially reusable. 291 * </p> 292 * <p> 293 * Some implementations may not support structural modifications (adding new elements or removing elements) while 294 * iterating over the contents. In such implementations, attempts to add or remove elements from the 295 * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a 296 * {@code ConcurrentModificationException} to be thrown. 297 * </p> 298 * 299 * @param action The action to be performed for each key-value pair in this collection 300 * @param state the object to be passed as the third parameter to each invocation on the specified 301 * triconsumer 302 * @param <CV> type of the consumer value 303 * @param <S> type of the third parameter 304 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications 305 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or 306 * {@link #forEach(TriConsumer, Object)}. 307 * @see ReadOnlyStringMap#forEach(TriConsumer, Object) 308 * @since 2.9 309 */ 310 public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) { 311 data.forEach(action, state); 312 } 313 314 /** 315 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 316 * 317 * @param format The format identifier. 318 * @return The formatted String. 319 */ 320 private StringBuilder format(final MapFormat format, final StringBuilder sb) { 321 if (format == null) { 322 appendMap(sb); 323 } else { 324 switch (format) { 325 case XML : { 326 asXml(sb); 327 break; 328 } 329 case JSON : { 330 asJson(sb); 331 break; 332 } 333 case JAVA : { 334 asJava(sb); 335 break; 336 } 337 case JAVA_UNQUOTED: 338 asJavaUnquoted(sb); 339 break; 340 default : { 341 appendMap(sb); 342 } 343 } 344 } 345 return sb; 346 } 347 348 /** 349 * Formats this message as an XML fragment String into the given builder. 350 * 351 * @param sb format into this builder. 352 */ 353 public void asXml(final StringBuilder sb) { 354 sb.append("<Map>\n"); 355 for (int i = 0; i < data.size(); i++) { 356 sb.append(" <Entry key=\"") 357 .append(data.getKeyAt(i)) 358 .append("\">"); 359 final int size = sb.length(); 360 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb); 361 StringBuilders.escapeXml(sb, size); 362 sb.append("</Entry>\n"); 363 } 364 sb.append("</Map>"); 365 } 366 367 /** 368 * Formats the message and return it. 369 * @return the formatted message. 370 */ 371 @Override 372 public String getFormattedMessage() { 373 return asString(); 374 } 375 376 /** 377 * 378 * @param formats 379 * An array of Strings that provide extra information about how to format the message. MapMessage uses 380 * the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default 381 * format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC 382 * 5424</a> messages. 383 * 384 * @return The formatted message. 385 */ 386 @Override 387 public String getFormattedMessage(final String[] formats) { 388 return format(getFormat(formats), new StringBuilder()).toString(); 389 } 390 391 private MapFormat getFormat(final String[] formats) { 392 if (formats == null || formats.length == 0) { 393 return null; 394 } 395 for (int i = 0; i < formats.length; i++) { 396 final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]); 397 if (mapFormat != null) { 398 return mapFormat; 399 } 400 } 401 return null; 402 } 403 404 protected void appendMap(final StringBuilder sb) { 405 for (int i = 0; i < data.size(); i++) { 406 if (i > 0) { 407 sb.append(' '); 408 } 409 sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE); 410 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb); 411 sb.append(Chars.DQUOTE); 412 } 413 } 414 415 protected void asJson(final StringBuilder sb) { 416 MapMessageJsonFormatter.format(sb, data); 417 } 418 419 protected void asJavaUnquoted(final StringBuilder sb) { 420 asJava(sb, false); 421 } 422 423 protected void asJava(final StringBuilder sb) { 424 asJava(sb, true); 425 } 426 427 private void asJava(final StringBuilder sb, boolean quoted) { 428 sb.append('{'); 429 for (int i = 0; i < data.size(); i++) { 430 if (i > 0) { 431 sb.append(", "); 432 } 433 sb.append(data.getKeyAt(i)).append(Chars.EQ); 434 if (quoted) { 435 sb.append(Chars.DQUOTE); 436 } 437 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb); 438 if (quoted) { 439 sb.append(Chars.DQUOTE); 440 } 441 } 442 sb.append('}'); 443 } 444 445 /** 446 * Constructs a new instance based on an existing Map. 447 * @param map The Map. 448 * @return A new MapMessage 449 */ 450 @SuppressWarnings("unchecked") 451 public M newInstance(final Map<String, V> map) { 452 return (M) new MapMessage<>(map); 453 } 454 455 @Override 456 public String toString() { 457 return asString(); 458 } 459 460 @Override 461 public void formatTo(final StringBuilder buffer) { 462 format((MapFormat) null, buffer); 463 } 464 465 @Override 466 public void formatTo(final String[] formats, final StringBuilder buffer) { 467 format(getFormat(formats), buffer); 468 } 469 470 @Override 471 public boolean equals(final Object o) { 472 if (this == o) { 473 return true; 474 } 475 if (o == null || this.getClass() != o.getClass()) { 476 return false; 477 } 478 479 final MapMessage<?, ?> that = (MapMessage<?, ?>) o; 480 481 return this.data.equals(that.data); 482 } 483 484 @Override 485 public int hashCode() { 486 return data.hashCode(); 487 } 488 489 /** 490 * Always returns null. 491 * 492 * @return null 493 */ 494 @Override 495 public Throwable getThrowable() { 496 return null; 497 } 498 499 /** 500 * Default implementation does nothing. 501 * 502 * @since 2.9 503 */ 504 protected void validate(final String key, final boolean value) { 505 // do nothing 506 } 507 508 /** 509 * Default implementation does nothing. 510 * 511 * @since 2.9 512 */ 513 protected void validate(final String key, final byte value) { 514 // do nothing 515 } 516 517 /** 518 * Default implementation does nothing. 519 * 520 * @since 2.9 521 */ 522 protected void validate(final String key, final char value) { 523 // do nothing 524 } 525 526 /** 527 * Default implementation does nothing. 528 * 529 * @since 2.9 530 */ 531 protected void validate(final String key, final double value) { 532 // do nothing 533 } 534 535 /** 536 * Default implementation does nothing. 537 * 538 * @since 2.9 539 */ 540 protected void validate(final String key, final float value) { 541 // do nothing 542 } 543 544 /** 545 * Default implementation does nothing. 546 * 547 * @since 2.9 548 */ 549 protected void validate(final String key, final int value) { 550 // do nothing 551 } 552 553 /** 554 * Default implementation does nothing. 555 * 556 * @since 2.9 557 */ 558 protected void validate(final String key, final long value) { 559 // do nothing 560 } 561 562 /** 563 * Default implementation does nothing. 564 * 565 * @since 2.9 566 */ 567 protected void validate(final String key, final Object value) { 568 // do nothing 569 } 570 571 /** 572 * Default implementation does nothing. 573 * 574 * @since 2.9 575 */ 576 protected void validate(final String key, final short value) { 577 // do nothing 578 } 579 580 /** 581 * Default implementation does nothing. 582 * 583 * @since 2.9 584 */ 585 protected void validate(final String key, final String value) { 586 // do nothing 587 } 588 589 /** 590 * Allows subclasses to change a candidate key to an actual key. 591 * 592 * @param candidateKey The candidate key. 593 * @return The candidate key. 594 * @since 2.12 595 */ 596 protected String toKey(final String candidateKey) { 597 return candidateKey; 598 } 599 600 /** 601 * Adds an item to the data Map. 602 * @param candidateKey The name of the data item. 603 * @param value The value of the data item. 604 * @return this object 605 * @since 2.9 606 */ 607 @SuppressWarnings("unchecked") 608 public M with(final String candidateKey, final boolean value) { 609 final String key = toKey(candidateKey); 610 validate(key, value); 611 data.putValue(key, value); 612 return (M) this; 613 } 614 615 /** 616 * Adds an item to the data Map. 617 * @param candidateKey The name of the data item. 618 * @param value The value of the data item. 619 * @return this object 620 * @since 2.9 621 */ 622 @SuppressWarnings("unchecked") 623 public M with(final String candidateKey, final byte value) { 624 final String key = toKey(candidateKey); 625 validate(key, value); 626 data.putValue(key, value); 627 return (M) this; 628 } 629 630 /** 631 * Adds an item to the data Map. 632 * @param candidateKey The name of the data item. 633 * @param value The value of the data item. 634 * @return this object 635 * @since 2.9 636 */ 637 @SuppressWarnings("unchecked") 638 public M with(final String candidateKey, final char value) { 639 final String key = toKey(candidateKey); 640 validate(key, value); 641 data.putValue(key, value); 642 return (M) this; 643 } 644 645 646 /** 647 * Adds an item to the data Map. 648 * @param candidateKey The name of the data item. 649 * @param value The value of the data item. 650 * @return this object 651 * @since 2.9 652 */ 653 @SuppressWarnings("unchecked") 654 public M with(final String candidateKey, final double value) { 655 final String key = toKey(candidateKey); 656 validate(key, value); 657 data.putValue(key, value); 658 return (M) this; 659 } 660 661 /** 662 * Adds an item to the data Map. 663 * @param candidateKey The name of the data item. 664 * @param value The value of the data item. 665 * @return this object 666 * @since 2.9 667 */ 668 @SuppressWarnings("unchecked") 669 public M with(final String candidateKey, final float value) { 670 final String key = toKey(candidateKey); 671 validate(key, value); 672 data.putValue(key, value); 673 return (M) this; 674 } 675 676 /** 677 * Adds an item to the data Map. 678 * @param candidateKey The name of the data item. 679 * @param value The value of the data item. 680 * @return this object 681 * @since 2.9 682 */ 683 @SuppressWarnings("unchecked") 684 public M with(final String candidateKey, final int value) { 685 final String key = toKey(candidateKey); 686 validate(key, value); 687 data.putValue(key, value); 688 return (M) this; 689 } 690 691 /** 692 * Adds an item to the data Map. 693 * @param candidateKey The name of the data item. 694 * @param value The value of the data item. 695 * @return this object 696 * @since 2.9 697 */ 698 @SuppressWarnings("unchecked") 699 public M with(final String candidateKey, final long value) { 700 final String key = toKey(candidateKey); 701 validate(key, value); 702 data.putValue(key, value); 703 return (M) this; 704 } 705 706 /** 707 * Adds an item to the data Map. 708 * @param candidateKey The name of the data item. 709 * @param value The value of the data item. 710 * @return this object 711 * @since 2.9 712 */ 713 @SuppressWarnings("unchecked") 714 public M with(final String candidateKey, final Object value) { 715 final String key = toKey(candidateKey); 716 validate(key, value); 717 data.putValue(key, value); 718 return (M) this; 719 } 720 721 /** 722 * Adds an item to the data Map. 723 * @param candidateKey The name of the data item. 724 * @param value The value of the data item. 725 * @return this object 726 * @since 2.9 727 */ 728 @SuppressWarnings("unchecked") 729 public M with(final String candidateKey, final short value) { 730 final String key = toKey(candidateKey); 731 validate(key, value); 732 data.putValue(key, value); 733 return (M) this; 734 } 735 736 /** 737 * Adds an item to the data Map in fluent style. 738 * @param candidateKey The name of the data item. 739 * @param value The value of the data item. 740 * @return {@code this} 741 */ 742 @SuppressWarnings("unchecked") 743 public M with(final String candidateKey, final String value) { 744 final String key = toKey(candidateKey); 745 put(key, value); 746 return (M) this; 747 } 748 749}