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}