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