001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.decoration.capture; 018 019import static groovy.json.JsonOutput.*; 020 021import java.io.IOException; 022import java.io.PrintWriter; 023import java.io.StringWriter; 024import java.io.UnsupportedEncodingException; 025import java.nio.charset.IllegalCharsetNameException; 026import java.nio.charset.UnsupportedCharsetException; 027import java.util.Arrays; 028import java.util.HashSet; 029import java.util.LinkedHashMap; 030import java.util.Map; 031import java.util.Set; 032 033import org.forgerock.openig.header.ContentTypeHeader; 034import org.forgerock.openig.http.Exchange; 035import org.forgerock.openig.http.Message; 036import org.forgerock.openig.http.Request; 037import org.forgerock.openig.http.Response; 038import org.forgerock.openig.log.Logger; 039 040/** 041 * Capture a message. 042 */ 043public class MessageCapture { 044 045 /** Set of common textual content with non-text content-types to capture. */ 046 private static final Set<String> TEXT_TYPES = new HashSet<String>( 047 Arrays.asList("application/atom+xml", "application/javascript", "application/json", 048 "application/rss+xml", "application/xhtml+xml", "application/xml", "application/xml-dtd", 049 "application/x-www-form-urlencoded") 050 ); // make all entries lower case 051 052 private final Logger logger; 053 private final boolean captureEntity; 054 private final boolean captureExchange; 055 056 /** 057 * Builds a MessageCapture that will prints messages in the provided {@code logger}. 058 * 059 * @param logger 060 * where to write captured messages 061 * @param captureEntity 062 * capture the entity content (if not binary) 063 */ 064 public MessageCapture(final Logger logger, final boolean captureEntity) { 065 this(logger, captureEntity, false); 066 } 067 068 /** 069 * Builds a MessageCapture that will prints messages in the provided {@code logger}. 070 * 071 * @param logger 072 * where to write captured messages 073 * @param captureEntity 074 * capture the entity content (if not binary) 075 * @param captureExchange 076 * capture the exchange content (excluding request and response object) as json 077 */ 078 public MessageCapture(final Logger logger, final boolean captureEntity, final boolean captureExchange) { 079 this.logger = logger; 080 this.captureEntity = captureEntity; 081 this.captureExchange = captureExchange; 082 } 083 084 /** 085 * Captures the given exchanges, in the given mode. The provided mode helps to determine if the interesting bits are 086 * in the request or in the response of the exchange. 087 * 088 * @param exchange 089 * contains the captured messages 090 * @param mode 091 * one of {@link CapturePoint#REQUEST}, {@link CapturePoint#FILTERED_REQUEST}, 092 * {@link CapturePoint#FILTERED_RESPONSE} or {@link CapturePoint#RESPONSE} 093 * @throws IOException 094 * if the entity content could not be print 095 */ 096 public void capture(final Exchange exchange, final CapturePoint mode) throws IOException { 097 StringWriter out = new StringWriter(); 098 PrintWriter writer = new PrintWriter(out); 099 int exchangeId = System.identityHashCode(exchange); 100 switch (mode) { 101 case REQUEST: 102 captureRequest(writer, exchange.request, exchangeId); 103 break; 104 case FILTERED_REQUEST: 105 captureFilteredRequest(writer, exchange.request, exchangeId); 106 break; 107 case RESPONSE: 108 captureResponse(writer, exchange.response, exchangeId); 109 break; 110 case FILTERED_RESPONSE: 111 captureFilteredResponse(writer, exchange.response, exchangeId); 112 break; 113 default: 114 throw new IllegalArgumentException("The given mode is not accepted: " + mode.name()); 115 } 116 117 // Prints the exchange if required 118 if (captureExchange) { 119 writer.println("Exchange's content as JSON (without request/response):"); 120 captureExchangeAsJson(writer, exchange); 121 } 122 123 // Print the message 124 logger.info(out.toString()); 125 } 126 127 private void captureExchangeAsJson(final PrintWriter writer, final Exchange exchange) { 128 Map<String, Object> map = new LinkedHashMap<String, Object>(exchange); 129 map.remove("exchange"); 130 map.remove("request"); 131 map.remove("response"); 132 map.remove("javax.servlet.http.HttpServletRequest"); 133 map.remove("javax.servlet.http.HttpServletResponse"); 134 writer.println(prettyPrint(toJson(map))); 135 } 136 137 private void captureRequest(PrintWriter writer, Request request, long id) throws IOException { 138 writer.printf("%n%n--- (request) exchange:%d --->%n%n", id); 139 if (request != null) { 140 captureRequestMessage(writer, request); 141 } 142 } 143 144 private void captureFilteredRequest(PrintWriter writer, Request request, long id) throws IOException { 145 writer.printf("%n%n--- (filtered-request) exchange:%d --->%n%n", id); 146 if (request != null) { 147 captureRequestMessage(writer, request); 148 } 149 } 150 151 private void captureResponse(PrintWriter writer, Response response, long id) throws IOException { 152 writer.printf("%n%n<--- (response) exchange:%d ---%n%n", id); 153 if (response != null) { 154 captureResponseMessage(writer, response); 155 } 156 } 157 158 private void captureFilteredResponse(PrintWriter writer, Response response, long id) throws IOException { 159 writer.printf("%n%n<--- (filtered-response) exchange:%d ---%n%n", id); 160 if (response != null) { 161 captureResponseMessage(writer, response); 162 } 163 } 164 165 private void captureRequestMessage(final PrintWriter writer, Request request) throws IOException { 166 writer.println(request.getMethod() + " " + request.getUri() + " " + request.getVersion()); 167 writeHeaders(writer, request); 168 writeEntity(writer, request); 169 writer.flush(); 170 } 171 172 private void captureResponseMessage(final PrintWriter writer, Response response) throws IOException { 173 writer.println(response.getVersion() + " " + response.getStatus() + " " + response.getReason()); 174 writeHeaders(writer, response); 175 writeEntity(writer, response); 176 writer.flush(); 177 } 178 179 private void writeHeaders(final PrintWriter writer, Message<?> message) { 180 for (String key : message.getHeaders().keySet()) { 181 for (String value : message.getHeaders().get(key)) { 182 writer.println(key + ": " + value); 183 } 184 } 185 } 186 187 private void writeEntity(final PrintWriter writer, Message<?> message) throws IOException { 188 ContentTypeHeader contentType = new ContentTypeHeader(message); 189 if (message.getEntity() == null || contentType.getType() == null) { 190 return; 191 } 192 writer.println(); 193 if (!captureEntity) { 194 // simply show presence of an entity 195 writer.println("[entity]"); 196 return; 197 } 198 if (!isTextualContent(contentType)) { 199 writer.println("[binary entity]"); 200 return; 201 } 202 try { 203 message.getEntity().push(); 204 try { 205 message.getEntity().copyDecodedContentTo(writer); 206 } finally { 207 message.getEntity().pop(); 208 } 209 } catch (UnsupportedEncodingException uee) { 210 writer.println("[entity contains data in unsupported encoding]"); 211 } catch (UnsupportedCharsetException uce) { 212 writer.println("[entity contains characters in unsupported character set]"); 213 } catch (IllegalCharsetNameException icne) { 214 writer.println("[entity contains characters in illegal character set]"); 215 } 216 // entity may not terminate with new line, so here it is 217 writer.println(); 218 } 219 220 /** 221 * Decide if the given content-type is printable or not. The entity represents a textual/printable content if: <ul> 222 * <li>there is a charset associated to the content-type, we'll be able to print it correctly</li> <li>the content 223 * type is in the category 'text' or it is an accepted type</li> </ul> 224 * 225 * @param contentType 226 * the message's content-type 227 * @return {@literal true} if the content-type represents a textual content 228 */ 229 private static boolean isTextualContent(final ContentTypeHeader contentType) { 230 String type = (contentType.getType() != null ? contentType.getType().toLowerCase() : null); 231 return contentType.getCharset() != null 232 // text or white-listed type 233 || (type != null && (TEXT_TYPES.contains(type) || type.startsWith("text/"))); 234 } 235 236}