From b580226698d07964241b24dae6b0e522050e42f3 Mon Sep 17 00:00:00 2001 From: Rob Daugherty Date: Tue, 10 Apr 2018 17:54:48 -0400 Subject: Non-GPL implementation of JerseyLoggingFilter Does the same thing, and can carry the Apache 2.0 license. 97.3% unit test coverage. Change-Id: I5ebe78616af2c0e4402deb30a165b3c62ed2efd8 Issue-ID: SO-398 Signed-off-by: Rob Daugherty --- .../jersey-connector/pom.xml | 11 +- .../openstack/connector/JerseyLoggingFilter.java | 444 +++++++++------------ .../connector/JerseyLoggingFilterTest.java | 267 +++++++++++++ 3 files changed, 461 insertions(+), 261 deletions(-) create mode 100644 openstack-client-connectors/jersey-connector/src/test/java/com/woorea/openstack/connector/JerseyLoggingFilterTest.java diff --git a/openstack-client-connectors/jersey-connector/pom.xml b/openstack-client-connectors/jersey-connector/pom.xml index 1915b5f..2c995cd 100644 --- a/openstack-client-connectors/jersey-connector/pom.xml +++ b/openstack-client-connectors/jersey-connector/pom.xml @@ -20,6 +20,15 @@ jackson-jaxrs 1.9.12 + + junit + junit + test + + + org.mockito + mockito-all + test + - diff --git a/openstack-client-connectors/jersey-connector/src/main/java/com/woorea/openstack/connector/JerseyLoggingFilter.java b/openstack-client-connectors/jersey-connector/src/main/java/com/woorea/openstack/connector/JerseyLoggingFilter.java index 43323f8..6d98005 100644 --- a/openstack-client-connectors/jersey-connector/src/main/java/com/woorea/openstack/connector/JerseyLoggingFilter.java +++ b/openstack-client-connectors/jersey-connector/src/main/java/com/woorea/openstack/connector/JerseyLoggingFilter.java @@ -3,13 +3,12 @@ * ONAP - SO * ================================================================================ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. - * Copyright (C) 2017 Huawei Intellectual Property. All rights reserved. * ================================================================================ * Licensed 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 + * 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, @@ -18,46 +17,6 @@ * limitations under the License. * ============LICENSE_END========================================================= */ - -/** - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright (c) 2010-2011 Oracle and/or its affiliates. All rights reserved. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common Development - * and Distribution License("CDDL") (collectively, the "License"). You - * may not use this file except in compliance with the License. You can - * obtain a copy of the License at - * http://glassfish.java.net/public/CDDL+GPL_1_1.html - * or packager/legal/LICENSE.txt. See the License for the specific - * language governing permissions and limitations under the License. - * - * When distributing the software, include this License Header Notice in each - * file and include the License file at packager/legal/LICENSE.txt. - * - * GPL Classpath Exception: - * Oracle designates this particular file as subject to the "Classpath" - * exception as provided by Oracle in the GPL Version 2 section of the License - * file that accompanied this code. - * - * Modifications: - * If applicable, add the following below the License Header, with the fields - * enclosed by brackets [] replaced by your own identifying information: - * "Portions Copyright [year] [name of copyright owner]" - * - * Contributor(s): - * If you wish your version of this file to be governed by only the CDDL or - * only the GPL Version 2, indicate your decision by adding "[Contributor] - * elects to include this software in this distribution under the [CDDL or GPL - * Version 2] license." If you don't indicate a single choice of license, a - * recipient has the option to distribute your version of this file under - * either the CDDL, the GPL Version 2 or to extend the choice of license to - * its licensees as provided above. However, if you add GPL Version 2 code - * and therefore, elected the GPL Version 2 license, then the option applies - * only if the new code is made subject to such option by the copyright - * holder. - */ package com.woorea.openstack.connector; import java.io.ByteArrayInputStream; @@ -65,237 +24,202 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PrintStream; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; -import javax.ws.rs.core.MultivaluedMap; - import com.sun.jersey.api.client.AbstractClientRequestAdapter; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientRequestAdapter; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; -import com.sun.jersey.api.client.filter.LoggingFilter; import com.sun.jersey.core.util.ReaderWriter; /** - * A logging filter. - * + * A Jersey client filter that writes the request and response to a specified logger. */ public class JerseyLoggingFilter extends ClientFilter { - private static final Logger LOGGER = Logger.getLogger(LoggingFilter.class.getName()); - - private static final String NOTIFICATION_PREFIX = "* "; - - private static final String REQUEST_PREFIX = "> "; - - private static final String RESPONSE_PREFIX = "< "; - - private static final String PASSWORD_PATTERN = "\"password\".*:.*\"(.*)\""; - - private final class Adapter extends AbstractClientRequestAdapter { - private final StringBuilder b; - - Adapter(ClientRequestAdapter cra, StringBuilder b) { - super(cra); - this.b = b; - } - - @Override - public OutputStream adapt(ClientRequest request, OutputStream out) throws IOException { - return new LoggingOutputStream(getAdapter().adapt(request, out), b); - } - - } - - private final class LoggingOutputStream extends OutputStream { - private final OutputStream out; - - private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - private final StringBuilder b; - - LoggingOutputStream(OutputStream out, StringBuilder b) { - this.out = out; - this.b = b; - } - - @Override - public void write(byte[] b) throws IOException { - baos.write(b); - out.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - baos.write(b, off, len); - out.write(b, off, len); - } - - @Override - public void write(int b) throws IOException { - baos.write(b); - out.write(b); - } - - @Override - public void close() throws IOException { - printEntity(b, baos.toByteArray()); - log(b); - out.close(); - } - } - - private final PrintStream loggingStream; - - private final Logger logger; - - private long _id = 0; - - /** - * Create a logging filter logging the request and response to - * a default JDK logger, named as the fully qualified class name of this - * class. - */ - public JerseyLoggingFilter() { - this(LOGGER); - } - - /** - * Create a logging filter logging the request and response to - * a JDK logger. - * - * @param logger the logger to log requests and responses. - */ - public JerseyLoggingFilter(Logger logger) { - this.loggingStream = null; - this.logger = logger; - } - - /** - * Create a logging filter logging the request and response to - * print stream. - * - * @param loggingStream the print stream to log requests and responses. - */ - public JerseyLoggingFilter(PrintStream loggingStream) { - this.loggingStream = loggingStream; - this.logger = null; - } - - private void log(StringBuilder b) { - if (logger != null) { - logger.info(b.toString()); - } else { - loggingStream.print(b); - } - } - - private StringBuilder prefixId(StringBuilder b, long id) { - b.append(Long.toString(id)).append(" "); - return b; - } - - @Override - public ClientResponse handle(ClientRequest request) throws ClientHandlerException { - long id = ++this._id; - - logRequest(id, request); - - ClientResponse response = getNext().handle(request); - - logResponse(id, response); - - return response; - } - - private void logRequest(long id, ClientRequest request) { - StringBuilder b = new StringBuilder(); - - printRequestLine(b, id, request); - printRequestHeaders(b, id, request.getHeaders()); - - if (request.getEntity() != null) { - request.setAdapter(new Adapter(request.getAdapter(), b)); - } else { - log(b); - } - } - - private void printRequestLine(StringBuilder b, long id, ClientRequest request) { - prefixId(b, id).append(NOTIFICATION_PREFIX).append("Client out-bound request").append("\n"); - prefixId(b, id).append(REQUEST_PREFIX).append(request.getMethod()).append(" "). - append(request.getURI().toASCIIString()).append("\n"); - } - - private void printRequestHeaders(StringBuilder b, long id, MultivaluedMap headers) { - for (Map.Entry> e : headers.entrySet()) { - List val = e.getValue(); - String header = e.getKey(); - - if(val.size() == 1) { - prefixId(b, id).append(REQUEST_PREFIX).append(header).append(": ").append(ClientRequest.getHeaderValue(val.get(0))).append("\n"); - } else { - StringBuilder sb = new StringBuilder(); - boolean add = false; - for(Object o : val) { - if(add) sb.append(','); - add = true; - sb.append(ClientRequest.getHeaderValue(o)); - } - prefixId(b, id).append(REQUEST_PREFIX).append(header).append(": ").append(sb.toString()).append("\n"); - } - } - } - - private void logResponse(long id, ClientResponse response) { - StringBuilder b = new StringBuilder(); - - printResponseLine(b, id, response); - printResponseHeaders(b, id, response.getHeaders()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStream in = response.getEntityInputStream(); - try { - ReaderWriter.writeTo(in, out); - - byte[] requestEntity = out.toByteArray(); - printEntity(b, requestEntity); - response.setEntityInputStream(new ByteArrayInputStream(requestEntity)); - } catch (IOException ex) { - throw new ClientHandlerException(ex); - } - log(b); - } - - private void printResponseLine(StringBuilder b, long id, ClientResponse response) { - prefixId(b, id).append(NOTIFICATION_PREFIX). - append("Client in-bound response").append("\n"); - prefixId(b, id).append(RESPONSE_PREFIX). - append(Integer.toString(response.getStatus())). - append("\n"); - } - - private void printResponseHeaders(StringBuilder b, long id, MultivaluedMap headers) { - for (Map.Entry> e : headers.entrySet()) { - String header = e.getKey(); - for (String value : e.getValue()) { - prefixId(b, id).append(RESPONSE_PREFIX).append(header).append(": "). - append(value).append("\n"); - } - } - prefixId(b, id).append(RESPONSE_PREFIX).append("\n"); - } - - private void printEntity(StringBuilder b, byte[] entity) throws IOException { - if (entity.length == 0) - return; - String entityString = new String(entity); - entityString = entityString.replaceAll(PASSWORD_PATTERN, "\"password\" : \"******\""); - b.append(entityString).append("\n"); - } -} + private final AtomicLong counter = new AtomicLong(0); + private final Logger logger; + + /** + * Constructor + * @param logger the logger to which the request and response are written. + */ + public JerseyLoggingFilter(Logger logger) { + this.logger = logger; + } + + @Override + public ClientResponse handle(ClientRequest request) throws ClientHandlerException { + long id = counter.incrementAndGet(); + logRequest(id, request); + ClientResponse response = getNext().handle(request); + logResponse(id, response); + return response; + } + + /** + * Logs a request. + * @param id the request id (counter) + * @param request the request + */ + private void logRequest(long id, ClientRequest request) { + StringBuilder builder = new StringBuilder(); + + builder.append(String.valueOf(id)); + builder.append(" * Client out-bound request\n"); + + builder.append(String.valueOf(id)); + builder.append(" > "); + builder.append(request.getMethod()); + builder.append(" "); + builder.append(request.getURI().toASCIIString()); + builder.append("\n"); + + // Request headers + + for (Map.Entry> entry : request.getHeaders().entrySet()) { + String header = entry.getKey(); + List values = entry.getValue(); + + if (values.size() == 1) { + builder.append(String.valueOf(id)); + builder.append(" > "); + builder.append(header); + builder.append(": "); + builder.append(ClientRequest.getHeaderValue(values.get(0))); + builder.append("\n"); + } else { + StringBuilder buf = new StringBuilder(); + boolean first = true; + + for(Object value : values) { + if (first) { + first = false; + } else { + buf.append(","); + } + + buf.append(ClientRequest.getHeaderValue(value)); + } + + builder.append(String.valueOf(id)); + builder.append(" > "); + builder.append(header); + builder.append(": "); + builder.append(buf.toString()); + builder.append("\n"); + } + } + + // Request body + + if (request.getEntity() != null) { + request.setAdapter(new JerseyLoggingAdapter(request.getAdapter(), builder)); + } else { + logger.info(builder.toString()); + } + } + + /** + * Logs a response. + * @param id the request id (counter) + * @param response the response + */ + private void logResponse(long id, ClientResponse response) { + StringBuilder builder = new StringBuilder(); + + builder.append(String.valueOf(id)); + builder.append(" * Client in-bound response\n"); + + builder.append(String.valueOf(id)); + builder.append(" < "); + builder.append(String.valueOf(response.getStatus())); + builder.append("\n"); + + // Response headers + + for (Map.Entry> entry : response.getHeaders().entrySet()) { + String header = entry.getKey(); + for (String value : entry.getValue()) { + builder.append(String.valueOf(id)); + builder.append(" < "); + builder.append(header); + builder.append(": "); + builder.append(value).append("\n"); + } + } + + // Response body + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream in = response.getEntityInputStream(); + try { + ReaderWriter.writeTo(in, out); + + byte[] requestEntity = out.toByteArray(); + appendToBuffer(builder, requestEntity); + response.setEntityInputStream(new ByteArrayInputStream(requestEntity)); + } catch (IOException ex) { + throw new ClientHandlerException(ex); + } + + logger.info(builder.toString()); + } + + /** + * Appends bytes to the builder. If the bytes contain the password pattern, + * the password is obliterated. + * @param builder the builder + * @param bytes the bytes to append + */ + private void appendToBuffer(StringBuilder builder, byte[] bytes) { + if (bytes.length != 0) { + String s = new String(bytes); + builder.append(s.replaceAll("\"password\".*:.*\"(.*)\"", "\"password\" : \"******\"")); + builder.append("\n"); + } + } + + private class JerseyLoggingAdapter extends AbstractClientRequestAdapter { + private final StringBuilder builder; + + JerseyLoggingAdapter(ClientRequestAdapter adapter, StringBuilder builder) { + super(adapter); + this.builder = builder; + } + + @Override + public OutputStream adapt(ClientRequest request, OutputStream out) throws IOException { + return new JerseyLoggingOutputStream(getAdapter().adapt(request, out), builder); + } + } + + private class JerseyLoggingOutputStream extends OutputStream { + private final OutputStream stream; + private final StringBuilder builder; + private final ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + + JerseyLoggingOutputStream(OutputStream stream, StringBuilder builder) { + this.stream = stream; + this.builder = builder; + } + + @Override + public void write(int value) throws IOException { + logStream.write(value); + stream.write(value); + } + + @Override + public void close() throws IOException { + appendToBuffer(builder, logStream.toByteArray()); + logger.info(builder.toString()); + stream.close(); + } + } +} \ No newline at end of file diff --git a/openstack-client-connectors/jersey-connector/src/test/java/com/woorea/openstack/connector/JerseyLoggingFilterTest.java b/openstack-client-connectors/jersey-connector/src/test/java/com/woorea/openstack/connector/JerseyLoggingFilterTest.java new file mode 100644 index 0000000..3bd0717 --- /dev/null +++ b/openstack-client-connectors/jersey-connector/src/test/java/com/woorea/openstack/connector/JerseyLoggingFilterTest.java @@ -0,0 +1,267 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package com.woorea.openstack.connector; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +import javax.ws.rs.core.MultivaluedMap; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.sun.jersey.api.client.ClientHandler; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientRequestAdapter; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.filter.ClientFilter; + +public class JerseyLoggingFilterTest { + + private static Logger logger; + private static LogFormatter logFormatter; + + @BeforeClass + public static void setUpClass() throws Exception { + logger = Logger.getLogger(JerseyLoggingFilterTest.class.getSimpleName()); + logger.setLevel(Level.ALL); + logger.setUseParentHandlers(false); + + ConsoleHandler handler = new ConsoleHandler(); + logFormatter = new LogFormatter(); + handler.setFormatter(logFormatter); + handler.setLevel(Level.ALL); + logger.addHandler(handler); + } + + @Before + public void setUpTest() { + logFormatter.clearLog(); + } + + /** + * Tests a scenario with no request content (GET). + * @throws Exception for unexpected errors + */ + @Test + public void testGET() throws Exception { + String responseContent = "Hello, I am Eliza."; + execute("GET", "http://www.onap.org/eliza", null, responseContent); + } + + /** + * Tests a scenario with request content (POST). + * @throws Exception for unexpected errors + */ + @Test + public void testPOST() throws Exception { + String requestContent = "I feel sad."; + String responseContent = "Do you often feel sad?"; + execute("POST", "http://www.onap.org/eliza", requestContent, responseContent); + } + + /** + * Runs a single test. + * @param httpMethod any HTTP method (POST, GET, ...) + * @param url any URL + * @param requestContent mock request content, possibly null + * @param responseContent mock response content, never null + * @throws Exception for unexpected errors + */ + private void execute(String httpMethod, String url, String requestContent, String responseContent) + throws Exception { + JerseyLoggingFilter loggingFilter = new JerseyLoggingFilter(logger); + + // Mock multi-valued and single valued request headers + + HashMap> requestHeaderMap = new HashMap<>(); + requestHeaderMap.put("Accept", Arrays.asList(new Object[]{"application/xml","application/json"})); + + if (requestContent != null) { + requestHeaderMap.put("Content-Type", Arrays.asList(new Object[]{"application/xml"})); + requestHeaderMap.put("Content-Length", Arrays.asList(new Object[]{String.valueOf(requestContent.length())})); + } + + @SuppressWarnings("unchecked") + MultivaluedMap requestHeaders = mock(MultivaluedMap.class); + when(requestHeaders.entrySet()).thenReturn(requestHeaderMap.entrySet()); + + // Mock the request object + + ClientRequest request = mock(TestClientRequest.class); + when(request.getURI()).thenReturn(new URI(url)); + when(request.getMethod()).thenReturn(httpMethod); + when(request.getHeaders()).thenReturn(requestHeaders); + + if (requestContent != null) { + when(request.getEntity()).thenReturn(requestContent.getBytes("UTF-8")); + } + + doCallRealMethod().when(request).setAdapter(any(ClientRequestAdapter.class)); + when(request.getAdapter()).thenCallRealMethod(); + request.setAdapter(new DefaultClientRequestAdapter()); + + // Mock multi-valued and single valued response headers + + HashMap> responseHeaderMap = new HashMap<>(); + responseHeaderMap.put("Cache-Control", Arrays.asList(new String[]{"no-cache","no-store"})); + responseHeaderMap.put("Content-Type", Arrays.asList(new String[]{"application/xml"})); + responseHeaderMap.put("Content-Length", Arrays.asList(new String[]{String.valueOf(responseContent.length())})); + @SuppressWarnings("unchecked") + MultivaluedMap responseHeaders = mock(MultivaluedMap.class); + when(responseHeaders.entrySet()).thenReturn(responseHeaderMap.entrySet()); + + // Mock the response object + + ClientResponse response = mock(ClientResponse.class); + when(response.getStatus()).thenReturn(200); + when(response.getHeaders()).thenReturn(responseHeaders); + when(response.getEntityInputStream()).thenReturn( + new ByteArrayInputStream(responseContent.getBytes("UTF-8"))); + + // Mock a handler that returns the response object and set + // it to be the next filter after the logging filter. + + ClientFilter handler = mock(ClientFilter.class); + when(handler.handle(request)).then(produceResponse(response)); + Method setNext = ClientFilter.class.getDeclaredMethod("setNext", new Class[]{ClientHandler.class}); + setNext.setAccessible(true); + setNext.invoke(loggingFilter, new Object[]{handler}); + + // Run the request into the logging filter + + loggingFilter.handle(request); + + // Validate resulting the log content + + String log = logFormatter.getLog(); + + assertContains(log, "* Client out-bound request"); + assertContains(log, "> " + httpMethod + " " + url); + + for (String header : requestHeaderMap.keySet()) { + assertContains(log, "> " + header + ": "); + } + + if (requestContent != null) { + assertContains(log, requestContent); + } + + assertContains(log, "* Client in-bound response"); + assertContains(log, "< 200"); + + for (String header : responseHeaderMap.keySet()) { + assertContains(log, "< " + header + ": "); + } + + assertContains(log, responseContent); + } + + private void assertContains(String log, String expect) { + assertTrue("Log does not contain '" + expect + "'", log.contains(expect)); + } + + private class DefaultClientRequestAdapter implements ClientRequestAdapter { + @Override + public OutputStream adapt(ClientRequest request, OutputStream out) throws IOException { + return out; + } + } + + private abstract class TestClientRequest extends ClientRequest { + private ClientRequestAdapter adapter; + + @Override + public ClientRequestAdapter getAdapter() { + return adapter; + } + + @Override + public void setAdapter(ClientRequestAdapter adapter) { + this.adapter = adapter; + } + } + + private Answer produceResponse(final ClientResponse response) { + return new Answer() { + public ClientResponse answer(InvocationOnMock invocation) throws IOException { + ClientRequest request = (ClientRequest) invocation.getArguments()[0]; + byte[] entity = (byte[]) request.getEntity(); + + if (entity != null) { + ClientRequestAdapter adapter = request.getAdapter(); + + OutputStream nullOutputStream = new OutputStream() { + @Override + public void write(int b) { + // Discard + } + }; + + OutputStream outputStream = adapter.adapt(request, nullOutputStream); + outputStream.write(entity); + outputStream.close(); + } + + return response; + } + }; + } + + private static class LogFormatter extends SimpleFormatter { + StringBuilder buffer = new StringBuilder(); + + public synchronized String getLog() { + return buffer.toString(); + } + + public synchronized void clearLog() { + buffer.setLength(0); + } + + @Override + public synchronized String format(LogRecord record) { + String logData = super.format(record); + buffer.append(logData); + return logData; + } + } +} \ No newline at end of file -- cgit 1.2.3-korg