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