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}