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.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
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   /**
079    * Creator.
080    *
081    * @return A new {@link MockLogger} object.
082    */
083   public static MockLogger create() {
084      return new MockLogger();
085   }
086
087   private final List<LogRecord> logRecords = list();
088   private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
089   private volatile Formatter formatter;
090
091   private volatile String format = "%4$s: %5$s%6$s%n";
092
093   /**
094    * Constructor.
095    */
096   public MockLogger() {
097      super("Mock", null);
098   }
099
100   /**
101    * Allows you to perform fluent-style assertions on the contents of the log file.
102    *
103    * @return A new fluent-style assertion object.
104    */
105   public synchronized FluentStringAssertion<MockLogger> assertContents() {
106      return new FluentStringAssertion<>(baos.toString(), this);
107   }
108
109   /**
110    * Asserts that the last message was logged at the specified level.
111    *
112    * @param level The level to match against.
113    * @return This object.
114    */
115   public synchronized MockLogger assertLastLevel(Level level) {
116      assertLogged();
117      if (last().getLevel() != level)
118         throw new AssertionError("Message logged at [" + last().getLevel() + "] instead of [" + level + "]");
119      return this;
120   }
121
122   /**
123    * Asserts that the last message matched the specified message.
124    *
125    * @return This object.
126    */
127   public synchronized FluentStringAssertion<MockLogger> assertLastMessage() {
128      assertLogged();
129      return new FluentStringAssertion<>(last().getMessage(), this);
130   }
131
132   /**
133    * Asserts that this logger was called.
134    *
135    * @return This object.
136    */
137   public synchronized MockLogger assertLogged() {
138      if (logRecords.isEmpty())
139         throw new AssertionError("Message not logged");
140      return this;
141   }
142
143   /**
144    * Asserts that the specified number of messages have been logged.
145    *
146    * @return This object.
147    */
148   public synchronized FluentIntegerAssertion<MockLogger> assertRecordCount() {
149      return new FluentIntegerAssertion<>(logRecords.size(), this);
150   }
151
152   /**
153    * Specifies the format for messages sent to the log file.
154    *
155    * <p>
156    * See {@link SimpleFormatter#format(LogRecord)} for the syntax of this string.
157    *
158    * @param format The format string.
159    * @return This object.
160    */
161   public synchronized MockLogger format(String format) {
162      this.format = format;
163      return this;
164   }
165
166   /**
167    * Overrides the formatter to use for formatting messages.
168    *
169    * <p>
170    * The default uses {@link SimpleFormatter}.
171    *
172    * @param formatter The log record formatter.
173    * @return This object.
174    */
175   public synchronized MockLogger formatter(Formatter formatter) {
176      this.formatter = formatter;
177      return this;
178   }
179
180   /**
181    * Sets the level for this logger.
182    *
183    * @param level The new level for this logger.
184    * @return This object.
185    */
186   public synchronized MockLogger level(Level level) {
187      super.setLevel(level);
188      return this;
189   }
190
191   @Override /* Overridden from Logger */
192   public synchronized void log(LogRecord record) {
193      logRecords.add(record);
194      try {
195         baos.write(getFormatter().format(record).getBytes("UTF-8"));
196      } catch (Exception e) {
197         throw toRex(e);
198      }
199   }
200
201   /**
202    * Resets this logger.
203    *
204    * @return This object.
205    */
206   public synchronized MockLogger reset() {
207      logRecords.clear();
208      baos.reset();
209      return this;
210   }
211
212   /**
213    * Returns the contents of this log file as a string.
214    */
215   @Override
216   public String toString() {
217      return baos.toString();
218   }
219
220   private Formatter getFormatter() {
221      if (formatter == null) {
222         synchronized (this) {
223            String oldFormat = System.getProperty(FORMAT_PROPERTY);
224            System.setProperty(FORMAT_PROPERTY, format);
225            formatter = new SimpleFormatter();
226            if (oldFormat == null)
227               System.clearProperty(FORMAT_PROPERTY);
228            else
229               System.setProperty(FORMAT_PROPERTY, oldFormat);
230         }
231      }
232      return formatter;
233   }
234
235   private LogRecord last() {
236      if (logRecords.isEmpty())
237         throw new AssertionError("Message not logged");
238      return logRecords.get(logRecords.size() - 1);
239   }
240}