| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sling.engine.impl.log; |
| |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.ServletRequest; |
| import javax.servlet.WriteListener; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.HttpServletResponseWrapper; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| import org.apache.sling.engine.impl.helper.ClientAbortException; |
| |
| class RequestLoggerResponse extends HttpServletResponseWrapper { |
| |
| // the content type header name |
| private static final String HEADER_CONTENT_TYPE = "Content-Type"; |
| |
| // the content length header name |
| private static final String HEADER_CONTENT_LENGTH = "Content-Length"; |
| |
| /** format for RFC 1123 date string -- "Sun, 06 Nov 1994 08:49:37 GMT" */ |
| private static final SimpleDateFormat RFC1123_FORMAT = |
| new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); |
| |
| /** |
| * The counter for request gone through this filter. As this is the first |
| * request level filter hit, this counter should actually count each request |
| * which at least enters the request level component filter processing. |
| * <p> |
| * This counter is reset to zero, when this component is activated. That is, |
| * each time this component is restarted (system start, bundle start, |
| * reconfiguration), the request counter restarts at zero. |
| */ |
| private static AtomicLong requestCounter = new AtomicLong(); |
| |
| // TODO: more content related headers, namely Content-Language should |
| // probably be supported |
| |
| // the request counter |
| private final long requestId; |
| |
| // the system time in ms when the request entered the system, this is |
| // the time this instance was created |
| private final long requestStart; |
| |
| // the system time in ms when the request exited the system, this is |
| // the time of the call to the requestEnd() method |
| private long requestEnd; |
| |
| // the output stream wrapper providing the transferred byte count |
| private LoggerResponseOutputStream out; |
| |
| // the print writer wrapper providing the transferred character count |
| private LoggerResponseWriter writer; |
| |
| // the caches status |
| private int status = SC_OK; |
| |
| // the cookies set during the request, indexed by cookie name |
| private Map<String, Cookie> cookies; |
| |
| // the headers set during the request, indexed by lower-case header |
| // name, value is string for single-valued and list for multi-valued |
| // headers |
| private Map<String, Object> headers; |
| |
| RequestLoggerResponse(final ServletRequest request, final HttpServletResponse response) { |
| super(response); |
| |
| this.requestId = requestCounter.getAndIncrement(); |
| this.requestStart = RequestLoggerPreprocessor.getRequestStartTime(request); |
| } |
| |
| /** |
| * Called to indicate the request processing has ended. This method |
| * currently sets the request end time returned by {@link #getRequestEnd()} |
| * and which is used to calculate the request duration. |
| */ |
| void requestEnd() { |
| this.requestEnd = System.currentTimeMillis(); |
| } |
| |
| // ---------- SlingHttpServletResponse interface |
| |
| @Override |
| public ServletOutputStream getOutputStream() throws IOException { |
| if (this.out == null) { |
| ServletOutputStream sos = super.getOutputStream(); |
| this.out = new LoggerResponseOutputStream(sos); |
| } |
| return this.out; |
| } |
| |
| @Override |
| public PrintWriter getWriter() throws IOException { |
| if (this.writer == null) { |
| PrintWriter pw = super.getWriter(); |
| this.writer = new LoggerResponseWriter(pw); |
| } |
| return this.writer; |
| } |
| |
| // ---------- Error handling through Sling Error Resolver ----------------- |
| |
| @Override |
| public void sendRedirect(String location) throws IOException { |
| super.sendRedirect(location); |
| |
| // replicate the status code of call to base class |
| this.status = SC_MOVED_TEMPORARILY; |
| } |
| |
| @Override |
| public void sendError(int status) throws IOException { |
| super.sendError(status); |
| this.status = status; |
| } |
| |
| @Override |
| public void sendError(int status, String message) throws IOException { |
| super.sendError(status, message); |
| this.status = status; |
| } |
| |
| @Override |
| public void setStatus(int status, String message) { |
| super.setStatus(status, message); |
| this.status = status; |
| } |
| |
| @Override |
| public void setStatus(int status) { |
| super.setStatus(status); |
| this.status = status; |
| } |
| |
| @Override |
| public void addCookie(Cookie cookie) { |
| |
| // register the cookie for later use |
| if (this.cookies == null) { |
| this.cookies = new HashMap<String, Cookie>(); |
| } |
| this.cookies.put(cookie.getName(), cookie); |
| |
| super.addCookie(cookie); |
| } |
| |
| @Override |
| public void addDateHeader(String name, long date) { |
| this.registerHeader(name, toDateString(date), true); |
| super.addDateHeader(name, date); |
| } |
| |
| @Override |
| public void addHeader(String name, String value) { |
| this.registerHeader(name, value, true); |
| super.addHeader(name, value); |
| } |
| |
| @Override |
| public void addIntHeader(String name, int value) { |
| this.registerHeader(name, String.valueOf(value), true); |
| super.addIntHeader(name, value); |
| } |
| |
| @Override |
| public void setContentLength(int len) { |
| this.registerHeader(HEADER_CONTENT_LENGTH, String.valueOf(len), false); |
| super.setContentLength(len); |
| } |
| |
| @Override |
| public void setContentType(String type) { |
| // SLING-726 No handling required since this seems to be correct |
| this.registerHeader(HEADER_CONTENT_TYPE, type, false); |
| super.setContentType(type); |
| } |
| |
| @Override |
| public void setCharacterEncoding(String charset) { |
| // SLING-726 Ignore call if getWriter() has been called |
| if (writer == null) { |
| super.setCharacterEncoding(charset); |
| } |
| } |
| |
| @Override |
| public void setDateHeader(String name, long date) { |
| this.registerHeader(name, toDateString(date), false); |
| super.setDateHeader(name, date); |
| } |
| |
| @Override |
| public void setHeader(String name, String value) { |
| this.registerHeader(name, value, false); |
| super.setHeader(name, value); |
| } |
| |
| @Override |
| public void setIntHeader(String name, int value) { |
| this.registerHeader(name, String.valueOf(value), false); |
| this.setHeader(name, String.valueOf(value)); |
| } |
| |
| @Override |
| public void setLocale(Locale loc) { |
| // TODO: Might want to register the Content-Language header |
| super.setLocale(loc); |
| } |
| |
| // ---------- Retrieving response information ------------------------------ |
| |
| public long getRequestId() { |
| return this.requestId; |
| } |
| |
| public long getRequestStart() { |
| return this.requestStart; |
| } |
| |
| public long getRequestEnd() { |
| return this.requestEnd; |
| } |
| |
| public long getRequestDuration() { |
| return this.requestEnd - this.requestStart; |
| } |
| |
| @Override |
| public int getStatus() { |
| return this.status; |
| } |
| |
| public int getCount() { |
| if (this.out != null) { |
| return this.out.getCount(); |
| } else if (this.writer != null) { |
| return this.writer.getCount(); |
| } |
| |
| // otherwise return zero |
| return 0; |
| } |
| |
| public Cookie getCookie(String name) { |
| return (this.cookies != null) ? (Cookie) this.cookies.get(name) : null; |
| } |
| |
| public String getHeadersString(String name) { |
| // normalize header name to lower case to support case-insensitive |
| // headers |
| name = name.toLowerCase(); |
| |
| Object header = (this.headers != null) ? this.headers.get(name) : null; |
| if (header == null) { |
| return null; |
| } else if (header instanceof String) { |
| return (String) header; |
| } else { |
| StringBuilder headerBuf = new StringBuilder(); |
| for (Iterator<?> hi = ((List<?>) header).iterator(); hi.hasNext(); ) { |
| if (headerBuf.length() > 0) { |
| headerBuf.append(","); |
| } |
| headerBuf.append(hi.next()); |
| } |
| return headerBuf.toString(); |
| } |
| } |
| |
| // ---------- Internal helper --------------------------------------------- |
| |
| /** |
| * Stores the name header-value pair in the header map. The name is |
| * converted to lower-case before using it as an index in the map. |
| * |
| * @param name The name of the header to register |
| * @param value The value of the header to register |
| * @param add If <code>true</code> the header value is added to the list of |
| * potentially existing header values. Otherwise the new value |
| * replaces any existing values. |
| */ |
| @SuppressWarnings("unchecked") |
| private void registerHeader(String name, String value, boolean add) { |
| // ensure the headers map |
| if (this.headers == null) { |
| this.headers = new HashMap<String, Object>(); |
| } |
| |
| // normalize header name to lower case to support case-insensitive |
| // headers |
| name = name.toLowerCase(); |
| |
| // retrieve the current contents if adding, otherwise assume no current |
| Object current = add ? this.headers.get(name) : null; |
| |
| if (current == null) { |
| // set the single value (forced if !add) |
| this.headers.put(name, value); |
| |
| } else if (current instanceof String) { |
| // create list if a single value is already set |
| List<String> list = new ArrayList<String>(); |
| list.add((String) current); |
| list.add(value); |
| this.headers.put(name, list); |
| |
| } else { |
| // append to the list of more than one already set |
| ((List<Object>) current).add(value); |
| } |
| } |
| |
| /** |
| * Converts the time value given as the number of milliseconds since January |
| * 1, 1970 to a date and time string compliant with RFC 1123 date |
| * specification. The resulting string is compliant with section 3.3.1, Full |
| * Date, of <a href="http://www.faqs.org/rfcs/rfc2616.html">RFC 2616</a> and |
| * may thus be used as the value of date header such as <code>Date</code>. |
| * |
| * @param date The date value to convert to a string |
| * @return The string representation of the date and time value. |
| */ |
| public static String toDateString(long date) { |
| synchronized (RFC1123_FORMAT) { |
| return RFC1123_FORMAT.format(new Date(date)); |
| } |
| } |
| |
| // ---------- byte/character counting output channels ---------------------- |
| |
| // byte transfer counting ServletOutputStream |
| private static class LoggerResponseOutputStream extends ServletOutputStream { |
| private ServletOutputStream delegatee; |
| |
| private int count; |
| |
| LoggerResponseOutputStream(ServletOutputStream delegatee) { |
| this.delegatee = delegatee; |
| } |
| |
| public int getCount() { |
| return this.count; |
| } |
| |
| @Override |
| public void write(int b) throws IOException { |
| try { |
| this.delegatee.write(b); |
| this.count++; |
| } catch (IOException ioe) { |
| throw new ClientAbortException(ioe); |
| } |
| } |
| |
| @Override |
| public void write(byte[] b) throws IOException { |
| try { |
| this.delegatee.write(b); |
| this.count += b.length; |
| } catch (IOException ioe) { |
| throw new ClientAbortException(ioe); |
| } |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| try { |
| this.delegatee.write(b, off, len); |
| this.count += len; |
| } catch (IOException ioe) { |
| throw new ClientAbortException(ioe); |
| } |
| } |
| |
| @Override |
| public void flush() throws IOException { |
| try { |
| this.delegatee.flush(); |
| } catch (IOException ioe) { |
| throw new ClientAbortException(ioe); |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| try { |
| this.delegatee.close(); |
| } catch (IOException ioe) { |
| throw new ClientAbortException(ioe); |
| } |
| } |
| |
| @Override |
| public boolean isReady() { |
| return this.delegatee.isReady(); |
| } |
| |
| @Override |
| public void setWriteListener(final WriteListener writeListener) { |
| this.delegatee.setWriteListener(writeListener); |
| } |
| } |
| |
| // character transfer counting PrintWriter |
| private static class LoggerResponseWriter extends PrintWriter { |
| |
| private static final int LINE_SEPARATOR_LENGTH = |
| System.getProperty("line.separator").length(); |
| |
| private int count; |
| |
| LoggerResponseWriter(PrintWriter delegatee) { |
| super(delegatee); |
| } |
| |
| public int getCount() { |
| return this.count; |
| } |
| |
| @Override |
| public void write(int c) { |
| super.write(c); |
| this.count++; |
| } |
| |
| @Override |
| public void write(char[] buf, int off, int len) { |
| super.write(buf, off, len); |
| this.count += len; |
| } |
| |
| @Override |
| public void write(String s, int off, int len) { |
| super.write(s, off, len); |
| this.count += len; |
| } |
| |
| @Override |
| public void println() { |
| super.println(); |
| this.count += LINE_SEPARATOR_LENGTH; |
| } |
| } |
| } |