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.juneau.commons.logging;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023
024import java.text.*;
025import java.util.*;
026import java.util.Formatter;
027import java.util.function.*;
028import java.util.logging.*;
029
030/**
031 * LogRecord that supports lazy message formatting.
032 *
033 * <p>
034 * This class extends {@link java.util.logging.LogRecord java.util.logging.LogRecord} and overrides {@link #getMessage()} to use
035 * {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} (printf-style <js>"%s"</js>/<js>"%d"</js>) for
036 * lazy formatting. The message is only formatted when {@link #getMessage()} is actually called.
037 *
038 * <h5 class='section'>Usage:</h5>
039 * <p>
040 * This class is used internally by {@link RichLogger} when logging formatted messages.
041 * The formatted message is only computed when the LogRecord's message is actually
042 * accessed (e.g., by a Handler or Formatter).
043 * </p>
044 *
045 * <h5 class='section'>Formatting contract:</h5>
046 * <p>
047 * Juneau {@link RichLogger} patterns are <b>printf-style</b> (rendered here via {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)}).
048 * Because this override returns the already-substituted message, downstream {@link java.util.logging.Handler Handlers} and
049 * {@link java.util.logging.Formatter Formatters} that call {@link #getMessage()} see fully-rendered text and never re-apply
050 * formatting — so the printf-vs-MessageFormat choice is purely internal. The JDK's {@link java.text.MessageFormat}
051 * parameter contract of {@link java.util.logging.LogRecord} only resurfaces if a record is serialized and consumed as a
052 * plain {@link java.util.logging.LogRecord} by a non-Juneau handler; such a consumer would apply MessageFormat to the raw
053 * <c>{0}</c> pattern, so printf patterns are not intended for that scenario.
054 * </p>
055 *
056 * <h5 class='section'>See Also:</h5><ul>
057 *    <li class='jc'>{@link RichLogger}
058 *    <li class='jc'>{@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)}
059 *    <li class='jc'>{@link java.util.logging.LogRecord}
060 * </ul>
061 */
062@SuppressWarnings({
063   "java:S100", // Method names match java.util.logging.LogRecord for API compatibility
064   "java:S115", // Constants use UPPER_snakeCase convention
065   "java:S1192", // String literals intentionally duplicated for clarity
066   "java:S2143", // Date required for java.util.logging compatibility and Formatter %t conversions; not a behavior-preserving java.time swap.
067   "java:S2176" // Class name intentionally matches java.util.logging.LogRecord; extends it to enrich logging with Juneau-specific fields
068})
069public class LogRecord extends java.util.logging.LogRecord {
070
071   private static final long serialVersionUID = 1L;
072   private final transient MessageGenerator generator;
073
074   /**
075    * Constructor.
076    *
077    * @param loggerName The logger name.  Can be <jk>null</jk>.
078    * @param level The log level.  Must not be <jk>null</jk>.
079    * @param msg The message pattern (will be used as-is if args is null or empty).
080    * @param parameters The format arguments, or <jk>null</jk> if none.
081    * @param throwable The throwable, or <jk>null</jk> if none.
082    */
083   public LogRecord(String loggerName, Level level, String msg, Object[] parameters, Throwable throwable) {
084      this(loggerName, level, msg, parameters, throwable, MessageGenerator.PRINTF);
085   }
086
087   /**
088    * Constructor.
089    *
090    * @param loggerName The logger name.  Can be <jk>null</jk>.
091    * @param level The log level.  Must not be <jk>null</jk>.
092    * @param msg The message pattern (will be used as-is if args is null or empty).
093    * @param parameters The format arguments, or <jk>null</jk> if none.
094    * @param throwable The throwable, or <jk>null</jk> if none.
095    * @param generator The message generator.
096    */
097   public LogRecord(String loggerName, Level level, String msg, Object[] parameters, Throwable throwable, MessageGenerator generator) {
098      super(level, msg);
099      setLoggerName(loggerName);
100      setParameters(isNotEmptyArray(parameters) ? parameters : null);
101      setThrown(throwable);
102      this.generator = generator == null ? MessageGenerator.PRINTF : generator;
103   }
104
105   /**
106    * Returns the log message, formatting it lazily if needed.
107    *
108    * <p>
109    * If parameters were set via {@link #setParameters(Object[])}, the message is formatted
110    * using {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} on first access.
111    *
112    * @return The log message, formatted if parameters are present.
113    */
114   @Override
115   public String getMessage() {
116      var m = super.getMessage();
117      var p = getParameters();
118      if (isEmptyArray(p))
119         return m;
120      var messageGenerator = generator == null ? MessageGenerator.PRINTF : generator;
121      return safeOptCatch(() -> messageGenerator.format(m, p), x -> m).orElse(m);
122   }
123
124   /**
125    * Formats this log record as a string using the specified format pattern.
126    *
127    * <p>
128    * Similar to how {@link SimpleFormatter} formats log records, this method
129    * allows you to specify a custom format string with placeholders that are replaced with
130    * actual values from the log record.
131    *
132    * <p>
133    * The format string supports both named placeholders (e.g., <js>"{date}"</js>) and
134    * {@link java.util.Formatter Formatter}-style format specifiers (e.g., <js>"%1$tc"</js>).
135    * Named placeholders are replaced first, then the resulting string is formatted using
136    * {@link java.util.Formatter Formatter} with the following arguments:
137    * <ol>
138    *    <li><c>date</c> - The date/time as a {@link Date} object
139    *    <li><c>logger</c> - The logger name
140    *    <li><c>level</c> - The log level
141    *    <li><c>message</c> - The formatted log message
142    *    <li><c>thrown</c> - The throwable object (if any)
143    * </ol>
144    *
145    * <h5 class='section'>Named Placeholders:</h5>
146    * <ul>
147    *    <li><js>"{date}"</js> - The date/time formatted using {@link SimpleFormatter SimpleFormatter}'s default date format
148    *    <li><js>"{timestamp}"</js> - The date/time formatted as ISO-8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)
149    *    <li><js>"{logger}"</js> - The logger name
150    *    <li><js>"{level}"</js> - The log level name
151    *    <li><js>"{msg}"</js> - The log message (formatted if parameters are present)
152    *    <li><js>"{thread}"</js> - The thread ID
153    *    <li><js>"{threadid}"</js> - The thread ID (alias for {thread})
154    *    <li><js>"{exception}"</js> - The exception message (if thrown)
155    *    <li><js>"{thrown}"</js> - The throwable object (if thrown)
156    * </ul>
157    *
158    * <h5 class='section'>Formatter-Style Format Specifiers:</h5>
159    * <p>
160    * You can use {@link java.util.Formatter Formatter}-style format specifiers directly in the format string.
161    * The arguments are passed in the same order as {@link java.util.logging.SimpleFormatter SimpleFormatter}:
162    * </p>
163    * <ul>
164    *    <li><js>"%1$s"</js> or <js>"%1$tc"</js> - The date/time (argument 1)
165    *    <li><js>"%2$s"</js> - The logger name (argument 2)
166    *    <li><js>"%3$s"</js> - The log level (argument 3)
167    *    <li><js>"%4$s"</js> - The log message (argument 4)
168    *    <li><js>"%5$s"</js> - The throwable and its backtrace (argument 5)
169    * </ul>
170    *
171    * <p>
172    * For date/time formatting, you can use date/time conversion characters with argument 1:
173    * </p>
174    * <ul>
175    *    <li><js>"%1$tc"</js> - Complete date and time (e.g., "Tue Mar 22 13:11:31 PDT 2011")
176    *    <li><js>"%1$tb"</js> - Month abbreviation (e.g., "Mar")
177    *    <li><js>"%1$td"</js> - Day of month (01-31)
178    *    <li><js>"%1$tY"</js> - Year (4 digits)
179    *    <li><js>"%1$tH"</js> - Hour (00-23)
180    *    <li><js>"%1$tM"</js> - Minute (00-59)
181    *    <li><js>"%1$tS"</js> - Second (00-59)
182    *    <li><js>"%1$tL"</js> - Milliseconds (000-999)
183    *    <li><js>"%1$tN"</js> - Nanoseconds (000000000-999999999, since JDK 9)
184    *    <li><js>"%1$Tp"</js> - AM/PM marker (uppercase)
185    *    <li><js>"%1$tp"</js> - AM/PM marker (lowercase)
186    * </ul>
187    *
188    * <p>
189    * The format string can also contain <js>"%n"</js> for newlines.
190    *
191    * <h5 class='section'>Examples:</h5>
192    * <p class='bjava'>
193    *    LogRecord <jv>record</jv> = <jk>new</jk> LogRecord(Level.INFO, <js>"User {0} logged in"</js>, <js>"John"</js>);
194    *
195    *    <jc>// Using named placeholders</jc>
196    *    String <jv>formatted1</jv> = <jv>record</jv>.formatted(<js>"[{timestamp} {level}] {msg}"</js>);
197    *    <jc>// Result: "[2025-01-22T07:42:45.123-0500 INFO] User John logged in"</jc>
198    *
199    *    <jc>// Using Formatter-style specifiers</jc>
200    *    String <jv>formatted2</jv> = <jv>record</jv>.formatted(<js>"%3$s: %4$s [%1$tc]%n"</js>);
201    *    <jc>// Result: "INFO: User John logged in [Tue Jan 22 07:42:45 PST 2025]"</jc>
202    *
203    *    <jc>// Mixed named placeholders and Formatter specifiers</jc>
204    *    String <jv>formatted3</jv> = <jv>record</jv>.formatted(<js>"%1$tb %1$td, %1$tY {level}: {msg}%n"</js>);
205    *    <jc>// Result: "Jan 22, 2025 INFO: User John logged in"</jc>
206    * </p>
207    *
208    * @param format The format string with placeholders and/or Formatter-style format specifiers.
209    * @return The formatted string.
210    * @see SimpleFormatter
211    * @see Formatter
212    */
213   public String formatted(String format) {
214      return formatted(this, format);
215   }
216
217   /**
218    * Formats a JUL log record with the same placeholders as {@link #formatted(String)}.
219    *
220    * @param rec The record to format.
221    * @param format The format string.
222    * @return The formatted string.
223    */
224   @SuppressWarnings({
225      "deprecation" // Date constructor is deprecated but needed for compatibility
226   })
227   public static String formatted(java.util.logging.LogRecord rec, String format) {
228      var date = new Date(rec.getMillis());
229
230      Function<String,Object> resolver = key -> switch (key) {
231         case "date" -> "%1$s";
232         case "logger" -> "%2$s";
233         case "level" -> "%3$s";
234         case "msg" -> "%4$s";
235         case "thrown" -> "%5$s";
236         case "timestamp" -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(date);
237         case "thread", "threadid" -> s(rec.getThreadID());
238         case "exception" -> o(rec.getThrown()).map(x -> x.getMessage()).orElse("");
239         default -> "";
240      };
241
242      return safeOptCatch(() -> f(formatNamed(format, resolver), date, rec.getLoggerName(), rec.getLevel(), rec.getMessage(), rec.getThrown()), x -> x.getLocalizedMessage()).orElse(null);
243   }
244}