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}