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