001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.rest;
014
015import static org.apache.juneau.rest.Enablement.*;
016
017import java.util.*;
018import java.util.logging.*;
019
020import javax.servlet.http.*;
021
022import org.apache.juneau.*;
023import org.apache.juneau.json.*;
024
025/**
026 * Represents a set of logging rules for how to handle logging of HTTP requests/responses.
027 */
028public class RestCallLoggerConfig {
029
030   /**
031    * Default empty logging config.
032    */
033   public static final RestCallLoggerConfig DEFAULT = RestCallLoggerConfig.create().build();
034
035   /**
036    * Default debug logging config.
037    */
038   public static final RestCallLoggerConfig DEFAULT_DEBUG =
039      RestCallLoggerConfig
040         .create()
041         .useStackTraceHashing(false)
042         .level(Level.WARNING)
043         .rules(
044            RestCallLoggerRule
045               .create()
046               .codes("*")
047               .verbose()
048               .build()
049         )
050         .build();
051
052   private final RestCallLoggerRule[] rules;
053   private final boolean useStackTraceHashing;
054   private final Enablement disabled;
055   private final int stackTraceHashingTimeout;
056   private final Level level;
057
058   RestCallLoggerConfig(Builder b) {
059      RestCallLoggerConfig p = b.parent;
060
061      this.disabled = b.disabled != null ? b.disabled : p != null ? p.disabled : FALSE;
062      this.useStackTraceHashing = b.useStackTraceHashing != null ? b.useStackTraceHashing : p != null ? p.useStackTraceHashing : false;
063      this.stackTraceHashingTimeout = b.stackTraceHashingTimeout != null ? b.stackTraceHashingTimeout : p != null ? p.stackTraceHashingTimeout : Integer.MAX_VALUE;
064      this.level = b.level != null ? b.level : p != null ? p.level : Level.INFO;
065
066      ArrayList<RestCallLoggerRule> rules = new ArrayList<>();
067      rules.addAll(b.rules);
068      if (p != null)
069         rules.addAll(Arrays.asList(p.rules));
070      this.rules = rules.toArray(new RestCallLoggerRule[rules.size()]);
071   }
072
073   /**
074    * Creates a builder for this class.
075    *
076    * @return A new builder for this class.
077    */
078   public static Builder create() {
079      return new Builder();
080   }
081
082   /**
083    * Builder for {@link RestCallLoggerConfig} objects.
084    */
085   public static class Builder {
086      List<RestCallLoggerRule> rules = new ArrayList<>();
087      RestCallLoggerConfig parent;
088      Level level;
089      Boolean useStackTraceHashing;
090      Enablement disabled;
091      Integer stackTraceHashingTimeout;
092
093      /**
094       * Sets the parent logging config.
095       *
096       * @param parent
097       *    The parent logging config.
098       *    <br>Can be <jk>null</jk>.
099       * @return This object (for method chaining).
100       */
101      public Builder parent(RestCallLoggerConfig parent) {
102         this.parent = parent;
103         return this;
104      }
105
106      /**
107       * Adds a new logging rule to this config.
108       *
109       * <p>
110       *    The rule will be added to the END of list of current rules and thus checked last in the current list but
111       *    before any parent rules.
112       *
113       * @param rule The logging rule to add to this config.
114       * @return This object (for method chaining).
115       */
116      public Builder rule(RestCallLoggerRule rule) {
117         this.rules.add(rule);
118         return this;
119      }
120
121      /**
122       * Adds new logging rules to this config.
123       *
124       * <p>
125       *    The rules will be added in order to the END of list of current rules and thus checked last in the current list but
126       *    before any parent rules.
127       *
128       * @param rules The logging rules to add to this config.
129       * @return This object (for method chaining).
130       */
131      public Builder rules(RestCallLoggerRule...rules) {
132         for (RestCallLoggerRule rule : rules)
133            this.rules.add(rule);
134         return this;
135      }
136
137      /**
138       * Enables no-trace mode on this config.
139       *
140       * <p>
141       * No-trace mode prevents logging of messages to the log file.
142       *
143       * <p>
144       * Possible values (case-insensitive):
145       * <ul>
146       *    <li>{@link Enablement#TRUE TRUE} - No-trace mode enabled for all requests.
147       *    <li>{@link Enablement#FALSE FALSE} - No-trace mode disabled for all requests.
148       *    <li>{@link Enablement#PER_REQUEST PER_REQUEST} - No-trace mode enabled for requests that have a <js>"X-NoTrace: true"</js> header.
149       * </ul>
150       *
151       * @param value
152       *    The value for this property.
153       *    <br>Can be <jk>null</jk> (inherit from parent or default to {@link Enablement#FALSE NEVER}).
154       * @return This object (for method chaining).
155       */
156      public Builder disabled(Enablement value) {
157         this.disabled = value;
158         return this;
159      }
160
161      /**
162       * Shortcut for calling <c>disabled(<jsf>TRUE</jsf>)</c>.
163       *
164       * @return This object (for method chaining).
165       */
166      public Builder disabled() {
167         return disabled(TRUE);
168      }
169
170      /**
171       * Enables the use of stacktrace hashing.
172       *
173       * <p>
174       * When enabled, stacktraces will be replaced with hashes in the log file.
175       *
176       * @param value
177       *    The value for this property.
178       *    <br>Can be <jk>null</jk> (inherit from parent or default to <jk>false</jk>).
179       * @return This object (for method chaining).
180       */
181      public Builder useStackTraceHashing(Boolean value) {
182         this.useStackTraceHashing = value;
183         return this;
184      }
185
186      /**
187       * Shortcut for calling <c>useStackTraceHashing(<jk>true</jk>);</c>.
188       *
189       * @return This object (for method chaining).
190       */
191      public Builder useStackTraceHashing() {
192         this.useStackTraceHashing = true;
193         return this;
194      }
195
196      /**
197       * Enables a timeout after which stack traces hashes are flushed.
198       *
199       * @param timeout
200       *    Time in milliseconds to hash stack traces for.
201       *    <br>Can be <jk>null</jk> (inherit from parent or default to {@link Integer#MAX_VALUE MAX_VALUE}).
202       * @return This object (for method chaining).
203       */
204      public Builder stackTraceHashingTimeout(Integer timeout) {
205         this.stackTraceHashingTimeout = timeout;
206         return this;
207      }
208
209      /**
210       * The default logging level.
211       *
212       * <p>
213       * This defines the logging level for messages if they're not already defined on the matched rule.
214       *
215       * @param value
216       *    The value for this property.
217       *    <br>Can be <jk>null</jk> (inherit from parent or default to {@link Level#INFO INFO}).
218       * @return This object (for method chaining).
219       */
220      public Builder level(Level value) {
221         this.level = value;
222         return this;
223      }
224
225      /**
226       * Applies the properties in the specified object map to this builder.
227       *
228       * @param m The map containing properties to apply.
229       * @return This object (for method chaining).
230       */
231      public Builder apply(ObjectMap m) {
232         for (String key : m.keySet()) {
233            if ("useStackTraceHashing".equals(key))
234               useStackTraceHashing(m.getBoolean("useStackTraceHashing"));
235            else if ("stackTraceHashingTimeout".equals(key))
236               stackTraceHashingTimeout(m.getInt("stackTraceHashingTimeout"));
237            else if ("disabled".equals(key))
238               disabled(m.get("disabled", Enablement.class));
239            else if ("rules".equals(key))
240               rules(m.get("rules", RestCallLoggerRule[].class));
241            else if ("level".equals(key))
242               level(m.get("level", Level.class));
243         }
244         return this;
245      }
246
247      /**
248       * Creates the {@link RestCallLoggerConfig} object based on settings on this builder.
249       *
250       * @return A new {@link RestCallLoggerConfig} object.
251       */
252      public RestCallLoggerConfig build() {
253         return new RestCallLoggerConfig(this);
254      }
255   }
256
257   /**
258    * Given the specified servlet request/response, find the rule that applies to it.
259    *
260    * @param req The servlet request.
261    * @param res The servlet response.
262    * @return The applicable logging rule, or <jk>null<jk> if a match could not be found.
263    */
264   public RestCallLoggerRule getRule(HttpServletRequest req, HttpServletResponse res) {
265
266      int status = res.getStatus();
267      Throwable e = (Throwable)req.getAttribute("Exception");
268      boolean debug = isDebug(req);
269
270      for (RestCallLoggerRule r : rules) {
271         if (r.matches(status, debug, e)) {
272            Enablement disabled = r.getDisabled();
273            if (disabled == null)
274               disabled = this.disabled;
275            if (disabled == TRUE)
276               return null;
277            if (isNoTraceAttr(req))
278               return null;
279            if (disabled == FALSE)
280               return r;
281            if (isNoTraceHeader(req))
282               return null;
283            return r;
284         }
285      }
286
287      return null;
288   }
289
290   /**
291    * Returns <jk>true</jk> if logging is disabled for this request.
292    *
293    * @param req The HTTP request.
294    * @return <jk>true</jk> if logging is disabled for this request.
295    */
296   public boolean isDisabled(HttpServletRequest req) {
297      if (disabled == TRUE)
298         return true;
299      if (disabled == FALSE)
300         return false;
301      return isNoTraceAttr(req);
302   }
303
304   private boolean isDebug(HttpServletRequest req) {
305      Boolean b = boolAttr(req, "Debug");
306      return (b != null && b == true);
307   }
308
309   private boolean isNoTraceAttr(HttpServletRequest req) {
310      Boolean b = boolAttr(req, "NoTrace");
311      return (b != null && b == true);
312   }
313
314   private boolean isNoTraceHeader(HttpServletRequest req) {
315      return "true".equalsIgnoreCase(req.getHeader("X-NoTrace"));
316   }
317
318   /**
319    * Returns the default logging level.
320    *
321    * @return The default logging level.
322    */
323   public Level getLevel() {
324      return level;
325   }
326
327   /**
328    * Returns <jk>true</jk> if stack traces should be hashed.
329    *
330    * @return <jk>true</jk> if stack traces should be hashed.
331    */
332   public boolean isUseStackTraceHashing() {
333      return useStackTraceHashing;
334   }
335
336   /**
337    * Returns the time in milliseconds that stacktrace hashes should be persisted.
338    *
339    * @return The time in milliseconds that stacktrace hashes should be persisted.
340    */
341   public int getStackTraceHashingTimeout() {
342      return stackTraceHashingTimeout;
343   }
344
345   /**
346    * Returns the rules defined in this config.
347    *
348    * @return Thew rules defined in this config.
349    */
350   public List<RestCallLoggerRule> getRules() {
351      return Collections.unmodifiableList(Arrays.asList(rules));
352   }
353
354   //-----------------------------------------------------------------------------------------------------------------
355   // Other methods
356   //-----------------------------------------------------------------------------------------------------------------
357
358   private Boolean boolAttr(HttpServletRequest req, String name) {
359      Object o = req.getAttribute(name);
360      if (o == null || ! (o instanceof Boolean))
361         return null;
362      return (Boolean)o;
363   }
364
365   @Override /* Object */
366   public String toString() {
367      return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap());
368   }
369
370   /**
371    * Returns the properties defined on this bean context as a simple map for debugging purposes.
372    *
373    * @return A new map containing the properties defined on this context.
374    */
375   public ObjectMap toMap() {
376      return new DefaultFilteringObjectMap()
377         .append("useStackTraceHashing", useStackTraceHashing)
378         .append("disabled", disabled == FALSE ? null : disabled)
379         .append("stackTraceHashingTimeout", stackTraceHashingTimeout == Integer.MAX_VALUE ? null : stackTraceHashingTimeout)
380         .append("level", level == Level.INFO ? null : level)
381         .append("rules", rules.length == 0 ? null : rules)
382      ;
383   }
384}