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 key The name of the data item. 198 * @param value The value of the data item. 199 */ 200 public void put(final String key, final String value) { 201 if (value == null) { 202 throw new IllegalArgumentException("No value provided for key " + key); 203 } 204 validate(key, value); 205 data.putValue(key, value); 206 } 207 208 /** 209 * Adds all the elements from the specified Map. 210 * @param map The Map to add. 211 */ 212 public void putAll(final Map<String, String> map) { 213 for (final Map.Entry<String, String> entry : map.entrySet()) { 214 data.putValue(entry.getKey(), entry.getValue()); 215 } 216 } 217 218 /** 219 * Retrieves the value of the element with the specified key or null if the key is not present. 220 * @param key The name of the element. 221 * @return The value of the element or null if the key is not present. 222 */ 223 public String get(final String key) { 224 final Object result = data.getValue(key); 225 return ParameterFormatter.deepToString(result); 226 } 227 228 /** 229 * Removes the element with the specified name. 230 * @param key The name of the element. 231 * @return The previous value of the element. 232 */ 233 public String remove(final String key) { 234 final String result = get(key); 235 data.remove(key); 236 return result; 237 } 238 239 /** 240 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 241 * 242 * @return The formatted String. 243 */ 244 public String asString() { 245 return format((MapFormat) null, new StringBuilder()).toString(); 246 } 247 248 /** 249 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 250 * 251 * @param format The format identifier. 252 * @return The formatted String. 253 */ 254 public String asString(final String format) { 255 try { 256 return format(EnglishEnums.valueOf(MapFormat.class, format), new StringBuilder()).toString(); 257 } catch (final IllegalArgumentException ex) { 258 return asString(); 259 } 260 } 261 262 /** 263 * Performs the given action for each key-value pair in this data structure 264 * until all entries have been processed or the action throws an exception. 265 * <p> 266 * Some implementations may not support structural modifications (adding new elements or removing elements) while 267 * iterating over the contents. In such implementations, attempts to add or remove elements from the 268 * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a 269 * {@code ConcurrentModificationException} to be thrown. 270 * </p> 271 * 272 * @param action The action to be performed for each key-value pair in this collection 273 * @param <CV> type of the consumer value 274 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications 275 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or 276 * {@link #forEach(TriConsumer, Object)}. 277 * @see ReadOnlyStringMap#forEach(BiConsumer) 278 * @since 2.9 279 */ 280 public <CV> void forEach(final BiConsumer<String, ? super CV> action) { 281 data.forEach(action); 282 } 283 284 /** 285 * Performs the given action for each key-value pair in this data structure 286 * until all entries have been processed or the action throws an exception. 287 * <p> 288 * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs, 289 * so the TriConsumer implementation itself can be stateless and potentially reusable. 290 * </p> 291 * <p> 292 * Some implementations may not support structural modifications (adding new elements or removing elements) while 293 * iterating over the contents. In such implementations, attempts to add or remove elements from the 294 * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a 295 * {@code ConcurrentModificationException} to be thrown. 296 * </p> 297 * 298 * @param action The action to be performed for each key-value pair in this collection 299 * @param state the object to be passed as the third parameter to each invocation on the specified 300 * triconsumer 301 * @param <CV> type of the consumer value 302 * @param <S> type of the third parameter 303 * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications 304 * to this data structure while iterating over the contents with {@link #forEach(BiConsumer)} or 305 * {@link #forEach(TriConsumer, Object)}. 306 * @see ReadOnlyStringMap#forEach(TriConsumer, Object) 307 * @since 2.9 308 */ 309 public <CV, S> void forEach(final TriConsumer<String, ? super CV, S> action, final S state) { 310 data.forEach(action, state); 311 } 312 313 /** 314 * Formats the Structured data as described in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>. 315 * 316 * @param format The format identifier. 317 * @return The formatted String. 318 */ 319 private StringBuilder format(final MapFormat format, final StringBuilder sb) { 320 if (format == null) { 321 appendMap(sb); 322 } else { 323 switch (format) { 324 case XML : { 325 asXml(sb); 326 break; 327 } 328 case JSON : { 329 asJson(sb); 330 break; 331 } 332 case JAVA : { 333 asJava(sb); 334 break; 335 } 336 case JAVA_UNQUOTED: 337 asJavaUnquoted(sb); 338 break; 339 default : { 340 appendMap(sb); 341 } 342 } 343 } 344 return sb; 345 } 346 347 /** 348 * Formats this message as an XML fragment String into the given builder. 349 * 350 * @param sb format into this builder. 351 */ 352 public void asXml(final StringBuilder sb) { 353 sb.append("<Map>\n"); 354 for (int i = 0; i < data.size(); i++) { 355 sb.append(" <Entry key=\"") 356 .append(data.getKeyAt(i)) 357 .append("\">"); 358 final int size = sb.length(); 359 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); 360 StringBuilders.escapeXml(sb, size); 361 sb.append("</Entry>\n"); 362 } 363 sb.append("</Map>"); 364 } 365 366 /** 367 * Formats the message and return it. 368 * @return the formatted message. 369 */ 370 @Override 371 public String getFormattedMessage() { 372 return asString(); 373 } 374 375 /** 376 * 377 * @param formats 378 * An array of Strings that provide extra information about how to format the message. MapMessage uses 379 * the first format specifier it recognizes. The supported formats are XML, JSON, and JAVA. The default 380 * format is key1="value1" key2="value2" as required by <a href="https://tools.ietf.org/html/rfc5424">RFC 381 * 5424</a> messages. 382 * 383 * @return The formatted message. 384 */ 385 @Override 386 public String getFormattedMessage(final String[] formats) { 387 return format(getFormat(formats), new StringBuilder()).toString(); 388 } 389 390 private MapFormat getFormat(final String[] formats) { 391 if (formats == null || formats.length == 0) { 392 return null; 393 } 394 for (int i = 0; i < formats.length; i++) { 395 final MapFormat mapFormat = MapFormat.lookupIgnoreCase(formats[i]); 396 if (mapFormat != null) { 397 return mapFormat; 398 } 399 } 400 return null; 401 } 402 403 protected void appendMap(final StringBuilder sb) { 404 for (int i = 0; i < data.size(); i++) { 405 if (i > 0) { 406 sb.append(' '); 407 } 408 sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE); 409 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); 410 sb.append(Chars.DQUOTE); 411 } 412 } 413 414 protected void asJson(final StringBuilder sb) { 415 sb.append('{'); 416 for (int i = 0; i < data.size(); i++) { 417 if (i > 0) { 418 sb.append(", "); 419 } 420 sb.append(Chars.DQUOTE); 421 int start = sb.length(); 422 sb.append(data.getKeyAt(i)); 423 StringBuilders.escapeJson(sb, start); 424 sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE); 425 start = sb.length(); 426 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); 427 StringBuilders.escapeJson(sb, start); 428 sb.append(Chars.DQUOTE); 429 } 430 sb.append('}'); 431 } 432 433 protected void asJavaUnquoted(final StringBuilder sb) { 434 asJava(sb, false); 435 } 436 437 protected void asJava(final StringBuilder sb) { 438 asJava(sb, true); 439 } 440 441 private void asJava(final StringBuilder sb, boolean quoted) { 442 sb.append('{'); 443 for (int i = 0; i < data.size(); i++) { 444 if (i > 0) { 445 sb.append(", "); 446 } 447 sb.append(data.getKeyAt(i)).append(Chars.EQ); 448 if (quoted) { 449 sb.append(Chars.DQUOTE); 450 } 451 ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null); 452 if (quoted) { 453 sb.append(Chars.DQUOTE); 454 } 455 } 456 sb.append('}'); 457 } 458 459 /** 460 * Constructs a new instance based on an existing Map. 461 * @param map The Map. 462 * @return A new MapMessage 463 */ 464 @SuppressWarnings("unchecked") 465 public M newInstance(final Map<String, V> map) { 466 return (M) new MapMessage<>(map); 467 } 468 469 @Override 470 public String toString() { 471 return asString(); 472 } 473 474 @Override 475 public void formatTo(final StringBuilder buffer) { 476 format((MapFormat) null, buffer); 477 } 478 479 @Override 480 public void formatTo(final String[] formats, final StringBuilder buffer) { 481 format(getFormat(formats), buffer); 482 } 483 484 @Override 485 public boolean equals(final Object o) { 486 if (this == o) { 487 return true; 488 } 489 if (o == null || this.getClass() != o.getClass()) { 490 return false; 491 } 492 493 final MapMessage<?, ?> that = (MapMessage<?, ?>) o; 494 495 return this.data.equals(that.data); 496 } 497 498 @Override 499 public int hashCode() { 500 return data.hashCode(); 501 } 502 503 /** 504 * Always returns null. 505 * 506 * @return null 507 */ 508 @Override 509 public Throwable getThrowable() { 510 return null; 511 } 512 513 /** 514 * Default implementation does nothing. 515 * 516 * @since 2.9 517 */ 518 protected void validate(final String key, final boolean value) { 519 // do nothing 520 } 521 522 /** 523 * Default implementation does nothing. 524 * 525 * @since 2.9 526 */ 527 protected void validate(final String key, final byte value) { 528 // do nothing 529 } 530 531 /** 532 * Default implementation does nothing. 533 * 534 * @since 2.9 535 */ 536 protected void validate(final String key, final char value) { 537 // do nothing 538 } 539 540 /** 541 * Default implementation does nothing. 542 * 543 * @since 2.9 544 */ 545 protected void validate(final String key, final double value) { 546 // do nothing 547 } 548 549 /** 550 * Default implementation does nothing. 551 * 552 * @since 2.9 553 */ 554 protected void validate(final String key, final float value) { 555 // do nothing 556 } 557 558 /** 559 * Default implementation does nothing. 560 * 561 * @since 2.9 562 */ 563 protected void validate(final String key, final int value) { 564 // do nothing 565 } 566 567 /** 568 * Default implementation does nothing. 569 * 570 * @since 2.9 571 */ 572 protected void validate(final String key, final long value) { 573 // do nothing 574 } 575 576 /** 577 * Default implementation does nothing. 578 * 579 * @since 2.9 580 */ 581 protected void validate(final String key, final Object value) { 582 // do nothing 583 } 584 585 /** 586 * Default implementation does nothing. 587 * 588 * @since 2.9 589 */ 590 protected void validate(final String key, final short value) { 591 // do nothing 592 } 593 594 /** 595 * Default implementation does nothing. 596 * 597 * @since 2.9 598 */ 599 protected void validate(final String key, final String value) { 600 // do nothing 601 } 602 603 /** 604 * Adds an item to the data Map. 605 * @param key The name of the data item. 606 * @param value The value of the data item. 607 * @return this object 608 * @since 2.9 609 */ 610 @SuppressWarnings("unchecked") 611 public M with(final String key, final boolean value) { 612 validate(key, value); 613 data.putValue(key, value); 614 return (M) this; 615 } 616 617 /** 618 * Adds an item to the data Map. 619 * @param key The name of the data item. 620 * @param value The value of the data item. 621 * @return this object 622 * @since 2.9 623 */ 624 @SuppressWarnings("unchecked") 625 public M with(final String key, final byte value) { 626 validate(key, value); 627 data.putValue(key, value); 628 return (M) this; 629 } 630 631 /** 632 * Adds an item to the data Map. 633 * @param key The name of the data item. 634 * @param value The value of the data item. 635 * @return this object 636 * @since 2.9 637 */ 638 @SuppressWarnings("unchecked") 639 public M with(final String key, final char value) { 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 key 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 key, final double value) { 655 validate(key, value); 656 data.putValue(key, value); 657 return (M) this; 658 } 659 660 /** 661 * Adds an item to the data Map. 662 * @param key The name of the data item. 663 * @param value The value of the data item. 664 * @return this object 665 * @since 2.9 666 */ 667 @SuppressWarnings("unchecked") 668 public M with(final String key, final float value) { 669 validate(key, value); 670 data.putValue(key, value); 671 return (M) this; 672 } 673 674 /** 675 * Adds an item to the data Map. 676 * @param key The name of the data item. 677 * @param value The value of the data item. 678 * @return this object 679 * @since 2.9 680 */ 681 @SuppressWarnings("unchecked") 682 public M with(final String key, final int value) { 683 validate(key, value); 684 data.putValue(key, value); 685 return (M) this; 686 } 687 688 /** 689 * Adds an item to the data Map. 690 * @param key The name of the data item. 691 * @param value The value of the data item. 692 * @return this object 693 * @since 2.9 694 */ 695 @SuppressWarnings("unchecked") 696 public M with(final String key, final long value) { 697 validate(key, value); 698 data.putValue(key, value); 699 return (M) this; 700 } 701 702 /** 703 * Adds an item to the data Map. 704 * @param key The name of the data item. 705 * @param value The value of the data item. 706 * @return this object 707 * @since 2.9 708 */ 709 @SuppressWarnings("unchecked") 710 public M with(final String key, final Object value) { 711 validate(key, value); 712 data.putValue(key, value); 713 return (M) this; 714 } 715 716 /** 717 * Adds an item to the data Map. 718 * @param key The name of the data item. 719 * @param value The value of the data item. 720 * @return this object 721 * @since 2.9 722 */ 723 @SuppressWarnings("unchecked") 724 public M with(final String key, final short value) { 725 validate(key, value); 726 data.putValue(key, value); 727 return (M) this; 728 } 729 730 /** 731 * Adds an item to the data Map in fluent style. 732 * @param key The name of the data item. 733 * @param value The value of the data item. 734 * @return {@code this} 735 */ 736 @SuppressWarnings("unchecked") 737 public M with(final String key, final String value) { 738 put(key, value); 739 return (M) this; 740 } 741 742}