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}