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.rest.mock;
018
019import static org.apache.juneau.common.utils.ThrowableUtils.*;
020import static org.apache.juneau.common.utils.Utils.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.logging.*;
025import java.util.logging.Formatter;
026
027import org.apache.juneau.assertions.*;
028
029/**
030 * Simplified logger for intercepting and asserting logging messages.
031 *
032 * <h5 class='figure'>Example:</h5>
033 * <p class='bjava'>
034 *    <jc>// Instantiate a mock logger.</jc>
035 *    MockLogger <jv>logger</jv> = <jk>new</jk> MockLogger();
036 *
037 *    <jc>// Associate it with a MockRestClient.</jc>
038 *    MockRestClient
039 *       .<jsm>create</jsm>(MyRestResource.<jk>class</jk>)
040 *       .json5()
041 *       .logger(<jv>logger</jv>)
042 *       .logRequests(DetailLevel.<jsf>FULL</jsf>, Level.<jsf>SEVERE</jsf>)
043 *       .build()
044 *       .post(<js>"/bean"</js>, <jv>bean</jv>)
045 *       .complete();
046 *
047 *    <jc>// Assert that logging occurred.</jc>
048 *    <jv>logger</jv>.assertLastLevel(Level.<jsf>SEVERE</jsf>);
049 *    <jv>logger</jv>.assertLastMessage().is(
050 *       <js>"=== HTTP Call (outgoing) ======================================================"</js>,
051 *       <js>"=== REQUEST ==="</js>,
052 *       <js>"POST http://localhost/bean"</js>,
053 *       <js>"---request headers---"</js>,
054 *       <js>" Accept: application/json5"</js>,
055 *       <js>"---request entity---"</js>,
056 *       <js>" Content-Type: application/json5"</js>,
057 *       <js>"---request content---"</js>,
058 *       <js>"{f:1}"</js>,
059 *       <js>"=== RESPONSE ==="</js>,
060 *       <js>"HTTP/1.1 200 "</js>,
061 *       <js>"---response headers---"</js>,
062 *       <js>" Content-Type: application/json"</js>,
063 *       <js>"---response content---"</js>,
064 *       <js>"{f:1}"</js>,
065 *       <js>"=== END ======================================================================="</js>,
066 *       <js>""</js>
067 *    );
068 * </p>
069 *
070 * <h5 class='section'>See Also:</h5><ul>
071 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestMockBasics">juneau-rest-mock Basics</a>
072 * </ul>
073 */
074public class MockLogger extends Logger {
075
076   private static final String FORMAT_PROPERTY = "java.util.logging.SimpleFormatter.format";
077
078   private final List<LogRecord> logRecords = list();
079   private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
080   private volatile Formatter formatter;
081   private volatile String format = "%4$s: %5$s%6$s%n";
082
083   /**
084    * Constructor.
085    */
086   public MockLogger() {
087      super("Mock", null);
088   }
089
090   /**
091    * Creator.
092    *
093    * @return A new {@link MockLogger} object.
094    */
095   public static MockLogger create() {
096      return new MockLogger();
097   }
098
099   @Override /* Logger */
100   public synchronized void log(LogRecord record) {
101      logRecords.add(record);
102      try {
103         baos.write(getFormatter().format(record).getBytes("UTF-8"));
104      } catch (Exception e) {
105         throw asRuntimeException(e);
106      }
107   }
108
109   private Formatter getFormatter() {
110      if (formatter == null) {
111         synchronized(this) {
112            String oldFormat = System.getProperty(FORMAT_PROPERTY);
113            System.setProperty(FORMAT_PROPERTY, format);
114            formatter = new SimpleFormatter();
115            if (oldFormat == null)
116               System.clearProperty(FORMAT_PROPERTY);
117            else
118               System.setProperty(FORMAT_PROPERTY, oldFormat);
119         }
120      }
121      return formatter;
122   }
123
124   /**
125    * Sets the level for this logger.
126    *
127    * @param level The new level for this logger.
128    * @return This object.
129    */
130   public synchronized MockLogger level(Level level) {
131      super.setLevel(level);
132      return this;
133   }
134
135   /**
136    * Specifies the format for messages sent to the log file.
137    *
138    * <p>
139    * See {@link SimpleFormatter#format(LogRecord)} for the syntax of this string.
140    *
141    * @param format The format string.
142    * @return This object.
143    */
144   public synchronized MockLogger format(String format) {
145      this.format = format;
146      return this;
147   }
148
149   /**
150    * Overrides the formatter to use for formatting messages.
151    *
152    * <p>
153    * The default uses {@link SimpleFormatter}.
154    *
155    * @param formatter The log record formatter.
156    * @return This object.
157    */
158   public synchronized MockLogger formatter(Formatter formatter) {
159      this.formatter = formatter;
160      return this;
161   }
162
163   /**
164    * Resets this logger.
165    *
166    * @return This object.
167    */
168   public synchronized MockLogger reset() {
169      logRecords.clear();
170      baos.reset();
171      return this;
172   }
173
174   /**
175    * Asserts that this logger was called.
176    *
177    * @return This object.
178    */
179   public synchronized MockLogger assertLogged() {
180      if (logRecords.isEmpty())
181         throw new AssertionError("Message not logged");
182      return this;
183   }
184
185   /**
186    * Asserts that the last message was logged at the specified level.
187    *
188    * @param level The level to match against.
189    * @return This object.
190    */
191   public synchronized MockLogger assertLastLevel(Level level) {
192      assertLogged();
193      if (last().getLevel() != level)
194         throw new AssertionError("Message logged at [" + last().getLevel() + "] instead of [" + level + "]");
195      return this;
196   }
197
198   /**
199    * Asserts that the last message matched the specified message.
200    *
201    * @return This object.
202    */
203   public synchronized FluentStringAssertion<MockLogger> assertLastMessage() {
204      assertLogged();
205      return new FluentStringAssertion<>(last().getMessage(), this);
206   }
207
208   /**
209    * Asserts that the specified number of messages have been logged.
210    *
211    * @return This object.
212    */
213   public synchronized FluentIntegerAssertion<MockLogger> assertRecordCount() {
214      return new FluentIntegerAssertion<>(logRecords.size(), this);
215   }
216
217   /**
218    * Allows you to perform fluent-style assertions on the contents of the log file.
219    *
220    * @return A new fluent-style assertion object.
221    */
222   public synchronized FluentStringAssertion<MockLogger> assertContents() {
223      return new FluentStringAssertion<>(baos.toString(), this);
224   }
225
226   private LogRecord last() {
227      if (logRecords.isEmpty())
228         throw new AssertionError("Message not logged");
229      return logRecords.get(logRecords.size()-1);
230   }
231
232   /**
233    * Returns the contents of this log file as a string.
234    */
235   @Override
236   public String toString() {
237      return baos.toString();
238   }
239}