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