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