From e756ba0d849dc61a980bfdc4bbae06fde8905327 Mon Sep 17 00:00:00 2001 From: Erik Ostermueller Date: Fri, 6 Dec 2013 00:37:58 -0600 Subject: [PATCH] new [gzip-true parm will gzip traced message format on the wire --- org.intrace/lib/intrace-agent.jar | Bin 371863 -> 386624 bytes .../src/org/intrace/agent/AgentSettings.java | 22 +- .../org/intrace/agent/ClassTransformer.java | 10 + .../agent/server/AgentClientConnection.java | 6 +- .../intrace/client/gui/helper/InTraceUI.java | 5 + .../gui/helper/NetworkDataReceiverThread.java | 8 + .../client/gui/helper/ParsedSettingsData.java | 11 + .../output/NetworkDataSenderThread.java | 5 + .../intrace/shared/AgentConfigConstants.java | 2 + .../src/org/intrace/shared/Base64.java | 2064 +++++++++++++++++ .../org/intracetest/agent/AgentTest.java | 39 +- .../intracetest/agent/CompressionTest.java | 185 ++ .../intracetest/agent/TraceSettingsTest.java | 48 + 13 files changed, 2402 insertions(+), 3 deletions(-) create mode 100644 org.intrace/src/org/intrace/shared/Base64.java create mode 100644 org.intrace/testsrc/org/intracetest/agent/CompressionTest.java create mode 100644 org.intrace/testsrc/org/intracetest/agent/TraceSettingsTest.java diff --git a/org.intrace/lib/intrace-agent.jar b/org.intrace/lib/intrace-agent.jar index 47c11116902f1cd770473ea57a441a85b45ecd54..19af344e539fb7ecf802266ad42a2b8bbdeef064 100644 GIT binary patch delta 51391 zcmZ6yQ;;r9uq-;ZZQHhO+qTX1jcwc38r!yQd)C-G|2{YF!#)okol$Sy(UDnMRh_

3NhWKn_6L2LC|BNfs7SK>^s;g)bJe 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 settingsMap = new HashMap(); settingsMap.put(AgentConfigConstants.INSTRU_ENABLED, Boolean.toString(instruEnabled)); + settingsMap.put(AgentConfigConstants.GZIP, + Boolean.toString(gzipEnabled)); settingsMap.put(AgentConfigConstants.CLASS_REGEX, getPatternString(classRegex)); settingsMap.put(AgentConfigConstants.EXCLUDE_CLASS_REGEX, diff --git a/org.intrace/src/org/intrace/agent/ClassTransformer.java b/org.intrace/src/org/intrace/agent/ClassTransformer.java index e74440a..1ca65cd 100644 --- a/org.intrace/src/org/intrace/agent/ClassTransformer.java +++ b/org.intrace/src/org/intrace/agent/ClassTransformer.java @@ -27,6 +27,7 @@ import org.intrace.agent.server.AgentServer; import org.intrace.output.AgentHelper; import org.intrace.output.InstruRunnable; import org.intrace.output.trace.TraceHandler; +import org.intrace.output.trace.TraceSettings; import org.intrace.shared.AgentConfigConstants; import org.objectweb.asm.ClassReader; @@ -482,6 +483,9 @@ public class ClassTransformer implements ClassFileTransformer Integer.toString(modifiedClasses.size())); return settingsMap; } + public boolean isGzipEnabled() { + return settings.isGzipEnabled(); + } /** * Handle a message and return a response. @@ -507,6 +511,12 @@ public class ClassTransformer implements ClassFileTransformer // System.out.println("## Settings Changed"); setInstrumentationEnabled(settings.isInstrumentationEnabled()); } + else if (oldSettings.isGzipEnabled() != settings.isGzipEnabled()) + { + //For coding simplicity and user clarity, the gzip parameter can only be set on agent side + //So, return to the old setting, which originated with the intrace agent command line parameter -- [gzip-true + settings.setGzipEnabled(oldSettings.isGzipEnabled()); + } else if (!Arrays.equals(oldSettings.getClassRegex(), settings.getClassRegex())) { // System.out.println("## Settings Changed"); diff --git a/org.intrace/src/org/intrace/agent/server/AgentClientConnection.java b/org.intrace/src/org/intrace/agent/server/AgentClientConnection.java index d811fc9..4224ea5 100644 --- a/org.intrace/src/org/intrace/agent/server/AgentClientConnection.java +++ b/org.intrace/src/org/intrace/agent/server/AgentClientConnection.java @@ -25,7 +25,11 @@ public class AgentClientConnection extends InstruRunnable { private final Socket connectedClient; private final ClassTransformer transformer; - private boolean traceConnEstablished = false; + public ClassTransformer getTransformer() { + return transformer; +} + +private boolean traceConnEstablished = false; private final Object traceConnLock = new Object(); /** diff --git a/org.intrace/src/org/intrace/client/gui/helper/InTraceUI.java b/org.intrace/src/org/intrace/client/gui/helper/InTraceUI.java index 3cb5046..cfc1f8d 100644 --- a/org.intrace/src/org/intrace/client/gui/helper/InTraceUI.java +++ b/org.intrace/src/org/intrace/client/gui/helper/InTraceUI.java @@ -2026,6 +2026,11 @@ public class InTraceUI implements ISocketCallback, IControlConnectionListener { return settingsData.netOutEnabled; } + @Override + public boolean isGzipEnabled() + { + return settingsData.gzipEnabled; + } }; networkTraceThread = new NetworkDataReceiverThread(remoteAddress, networkTracePort, config, outputTabs.textOutputTab.filterThread); diff --git a/org.intrace/src/org/intrace/client/gui/helper/NetworkDataReceiverThread.java b/org.intrace/src/org/intrace/client/gui/helper/NetworkDataReceiverThread.java index d34e915..25970aa 100644 --- a/org.intrace/src/org/intrace/client/gui/helper/NetworkDataReceiverThread.java +++ b/org.intrace/src/org/intrace/client/gui/helper/NetworkDataReceiverThread.java @@ -6,11 +6,15 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import org.intrace.shared.Base64; + + public class NetworkDataReceiverThread implements Runnable { public static interface INetworkOutputConfig { public boolean isNetOutputEnabled(); + public boolean isGzipEnabled(); } private final Socket traceSocket; @@ -53,6 +57,10 @@ public class NetworkDataReceiverThread implements Runnable { if (outputConfig.isNetOutputEnabled()) { + if (outputConfig.isGzipEnabled()) { + byte[] tmp = Base64.decode(traceLine); + traceLine = new String(tmp); + } traceThread.addTraceLine(traceLine); } } diff --git a/org.intrace/src/org/intrace/client/gui/helper/ParsedSettingsData.java b/org.intrace/src/org/intrace/client/gui/helper/ParsedSettingsData.java index 7782e85..55cac4f 100644 --- a/org.intrace/src/org/intrace/client/gui/helper/ParsedSettingsData.java +++ b/org.intrace/src/org/intrace/client/gui/helper/ParsedSettingsData.java @@ -16,6 +16,7 @@ public class ParsedSettingsData public boolean entryExitEnabled; public boolean branchEnabled; + public boolean gzipEnabled = false; public boolean truncArraysEnabled; public boolean argsEnabled; public boolean stdOutEnabled; @@ -35,6 +36,15 @@ public class ParsedSettingsData { this.settingsMap = settingsMap; + if ("true".equals(settingsMap.get(AgentConfigConstants.GZIP))) + { + gzipEnabled = true; + } + else + { + gzipEnabled = false; + } + classRegex = settingsMap.get(AgentConfigConstants.CLASS_REGEX); classExcludeRegex = settingsMap .get(AgentConfigConstants.EXCLUDE_CLASS_REGEX); @@ -86,6 +96,7 @@ public class ParsedSettingsData branchEnabled = false; } + if ("true".equals(settingsMap.get(TraceConfigConstants.ARG))) { argsEnabled = true; diff --git a/org.intrace/src/org/intrace/output/NetworkDataSenderThread.java b/org.intrace/src/org/intrace/output/NetworkDataSenderThread.java index 5f211e2..a1e5416 100644 --- a/org.intrace/src/org/intrace/output/NetworkDataSenderThread.java +++ b/org.intrace/src/org/intrace/output/NetworkDataSenderThread.java @@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.intrace.agent.server.AgentClientConnection; +import org.intrace.shared.Base64; public class NetworkDataSenderThread extends InstruRunnable { @@ -100,6 +101,10 @@ public class NetworkDataSenderThread extends InstruRunnable Object traceLine = outgoingData.poll(5, TimeUnit.SECONDS); if (traceLine != null) { + if (traceLine instanceof String && connection.getTransformer().isGzipEnabled()) { + String tmp = (String)traceLine; + traceLine = Base64.encodeBytes(tmp.getBytes(), Base64.GZIP); + } traceWriter.writeObject(traceLine); } else diff --git a/org.intrace/src/org/intrace/shared/AgentConfigConstants.java b/org.intrace/src/org/intrace/shared/AgentConfigConstants.java index bea1b1e..a771d2f 100644 --- a/org.intrace/src/org/intrace/shared/AgentConfigConstants.java +++ b/org.intrace/src/org/intrace/shared/AgentConfigConstants.java @@ -16,6 +16,7 @@ public class AgentConfigConstants public static final String SERVER_PORT = "SERVER_PORT"; + public static final String GZIP = "[gzip-"; //Default is not enabled (false). public static final String CLASS_REGEX = "[regex-"; public static final String EXCLUDE_CLASS_REGEX = "[exclude-regex-"; public static final String INSTRU_ENABLED = "[instru-"; @@ -37,5 +38,6 @@ public class AgentConfigConstants COMMANDS.add(VERBOSE_MODE + ""); COMMANDS.add(OPT_SERVER_PORT + ""); COMMANDS.add(CALLBACK_PORT + ""); + COMMANDS.add(GZIP + ""); } } diff --git a/org.intrace/src/org/intrace/shared/Base64.java b/org.intrace/src/org/intrace/shared/Base64.java new file mode 100644 index 0000000..4f8d7fc --- /dev/null +++ b/org.intrace/src/org/intrace/shared/Base64.java @@ -0,0 +1,2064 @@ +package org.intrace.shared; +/** + *

Encodes and decodes to and from Base64 notation.

+ *

Homepage: http://iharder.net/base64.

+ * + *

Example:

+ * + * String encoded = Base64.encode( myByteArray ); + *
+ * byte[] myByteArray = Base64.decode( encoded ); + * + *

The options parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as + * encodeBytes( bytes, options ) the options parameter can be used to indicate such + * things as first gzipping the bytes before encoding them, not inserting linefeeds, + * and encoding using the URL-safe and Ordered dialects.

+ * + *

Note, according to RFC3548, + * Section 2.1, implementations should not add line feeds unless explicitly told + * to do so. I've got Base64 set to this behavior now, although earlier versions + * broke lines by default.

+ * + *

The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:

+ * + * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); + *

to compress the data before encoding it and then making the output have newline characters.

+ *

Also...

+ * String encoded = Base64.encodeBytes( crazyString.getBytes() ); + * + * + * + *

+ * Change Log: + *

+ * + * + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.7 + */ +public class Base64 +{ + +/* ******** P U B L I C F I E L D S ******** */ + + + /** No options specified. Value is zero. */ + public final static int NO_OPTIONS = 0; + + /** Specify encoding in first bit. Value is one. */ + public final static int ENCODE = 1; + + + /** Specify decoding in first bit. Value is zero. */ + public final static int DECODE = 0; + + + /** Specify that data should be gzip-compressed in second bit. Value is two. */ + public final static int GZIP = 2; + + /** Specify that gzipped data should not be automatically gunzipped. */ + public final static int DONT_GUNZIP = 4; + + + /** Do break lines when encoding. Value is 8. */ + public final static int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described + * in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * It is important to note that data encoded this way is not officially valid Base64, + * or at the very least should not be called Base64 without also specifying that is + * was encoded using the URL- and Filename-safe dialect. + */ + public final static int URL_SAFE = 16; + + + /** + * Encode using the special "ordered" dialect of Base64 described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + public final static int ORDERED = 32; + + +/* ******** P R I V A T E F I E L D S ******** */ + + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + + /** Preferred encoding. */ + private final static String PREFERRED_ENCODING = "US-ASCII"; + + + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + +/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] _STANDARD_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9,-9,-9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + + +/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private final static byte[] _URL_SAFE_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' + }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private final static byte[] _URL_SAFE_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + + + +/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, + * and it is described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + private final static byte[] _ORDERED_ALPHABET = { + (byte)'-', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', + (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'_', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' + }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private final static byte[] _ORDERED_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' + 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' + 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' + -9,-9,-9,-9,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + }; + + +/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URLSAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getAlphabet( int options ) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } // end getAlphabet + + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URL_SAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getDecodabet( int options ) { + if( (options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } // end getAlphabet + + + + /** Defeats instantiation. */ + private Base64(){} + + + + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes up to the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * The array threeBytes needs only be as big as + * numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) { + encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); + return b4; + } // end encode3to4 + + + /** + *

Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes.

+ *

This is the lowest level of the encoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset, int options ) { + + byte[] ALPHABET = getAlphabet( options ); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) + | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) + | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); + + switch( numSigBytes ) + { + case 3: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; + return destination; + + case 2: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + case 1: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = EQUALS_SIGN; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded ByteBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){ + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while( raw.hasRemaining() ){ + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); + encoded.put(enc4); + } // end input remaining + } + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded CharBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){ + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while( raw.hasRemaining() ){ + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); + for( int i = 0; i < 4; i++ ){ + encoded.put( (char)(enc4[i] & 0xFF) ); + } + } // end input remaining + } + + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @throws java.io.IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject( java.io.Serializable serializableObject ) + throws java.io.IOException { + return encodeObject( serializableObject, NO_OPTIONS ); + } // end encodeObject + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject( myObj, Base64.GZIP ) or + *

+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @since 2.0 + */ + public static String encodeObject( java.io.Serializable serializableObject, int options ) + throws java.io.IOException { + + if( serializableObject == null ){ + throw new NullPointerException( "Cannot serialize a null object." ); + } // end if: null + + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.util.zip.GZIPOutputStream gzos = null; + java.io.ObjectOutputStream oos = null; + + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | options ); + if( (options & GZIP) != 0 ){ + // Gzip + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream( gzos ); + } else { + // Not gzipped + oos = new java.io.ObjectOutputStream( b64os ); + } + oos.writeObject( serializableObject ); + } // end try + catch( java.io.IOException e ) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try{ oos.close(); } catch( Exception e ){} + try{ gzos.close(); } catch( Exception e ){} + try{ b64os.close(); } catch( Exception e ){} + try{ baos.close(); } catch( Exception e ){} + } // end finally + + // Return value according to relevant encoding. + try { + return new String( baos.toByteArray(), PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue){ + // Fall back to some Java default + return new String( baos.toByteArray() ); + } // end catch + + } // end encode + + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes( byte[] source ) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int options ) throws java.io.IOException { + return encodeBytes( source, 0, source.length, options ); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + *

As of v 2.3, if there is an error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return The Base64-encoded data as a String + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes( byte[] source, int off, int len ) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes( source, off, len, NO_OPTIONS ); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { + byte[] encoded = encodeBytesToBytes( source, off, len, options ); + + // Return value according to relevant encoding. + try { + return new String( encoded, PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String( encoded ); + } // end catch + + } // end encodeBytes + + + + + /** + * Similar to {@link #encodeBytes(byte[])} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes( byte[] source ) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS ); + } catch( java.io.IOException ex ) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { + + if( source == null ){ + throw new NullPointerException( "Cannot serialize a null array." ); + } // end if: null + + if( off < 0 ){ + throw new IllegalArgumentException( "Cannot have negative offset: " + off ); + } // end if: off < 0 + + if( len < 0 ){ + throw new IllegalArgumentException( "Cannot have length offset: " + len ); + } // end if: len < 0 + + if( off + len > source.length ){ + throw new IllegalArgumentException( + String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length)); + } // end if: off < 0 + + + + // Compress? + if( (options & GZIP) != 0 ) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | options ); + gzos = new java.util.zip.GZIPOutputStream( b64os ); + + gzos.write( source, off, len ); + gzos.close(); + } // end try + catch( java.io.IOException e ) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try{ gzos.close(); } catch( Exception e ){} + try{ b64os.close(); } catch( Exception e ){} + try{ baos.close(); } catch( Exception e ){} + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) != 0; + + //int len43 = len * 4 / 3; + //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding + if( breakLines ){ + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[ encLen ]; + + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for( ; d < len2; d+=3, e+=4 ) { + encode3to4( source, d+off, 3, outBuff, e, options ); + + lineLength += 4; + if( breakLines && lineLength >= MAX_LINE_LENGTH ) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if( d < len ) { + encode3to4( source, d+off, len - d, outBuff, e, options ); + e += 4; + } // end if: some padding needed + + + // Only resize array if we didn't guess it right. + if( e <= outBuff.length - 1 ){ + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff,0, finalOut,0,e); + //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); + return finalOut; + } else { + //System.err.println("No need to resize array."); + return outBuff; + } + + } // end else: don't compress + + } // end encodeBytesToBytes + + + + + +/* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + *

This is the lowest level of the decoding methods with + * all possible parameters.

+ * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid + * or there is not enough room in the array. + * @since 1.3 + */ + private static int decode4to3( + byte[] source, int srcOffset, + byte[] destination, int destOffset, int options ) { + + // Lots of error checking and exception throwing + if( source == null ){ + throw new NullPointerException( "Source array was null." ); + } // end if + if( destination == null ){ + throw new NullPointerException( "Destination array was null." ); + } // end if + if( srcOffset < 0 || srcOffset + 3 >= source.length ){ + throw new IllegalArgumentException( String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) ); + } // end if + if( destOffset < 0 || destOffset +2 >= destination.length ){ + throw new IllegalArgumentException( String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) ); + } // end if + + + byte[] DECODABET = getDecodabet( options ); + + // Example: Dk== + if( source[ srcOffset + 2] == EQUALS_SIGN ) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + return 1; + } + + // Example: DkL= + else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) + | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); + + + destination[ destOffset ] = (byte)( outBuff >> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); + destination[ destOffset + 2 ] = (byte)( outBuff ); + + return 3; + } + } // end decodeToBytes + + + + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.1 + */ + public static byte[] decode( byte[] source ) + throws java.io.IOException { + byte[] decoded = null; +// try { + decoded = decode( source, 0, source.length, Base64.NO_OPTIONS ); +// } catch( java.io.IOException ex ) { +// assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); +// } + return decoded; + } + + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * @return decoded data + * @throws java.io.IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode( byte[] source, int off, int len, int options ) + throws java.io.IOException { + + // Lots of error checking and exception throwing + if( source == null ){ + throw new NullPointerException( "Cannot decode null source array." ); + } // end if + if( off < 0 || off + len > source.length ){ + throw new IllegalArgumentException( String.format( + "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) ); + } // end if + + if( len == 0 ){ + return new byte[0]; + }else if( len < 4 ){ + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len ); + } // end if + + byte[] DECODABET = getDecodabet( options ); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiDecode = 0; // Special value from DECODABET + + for( i = off; i < off+len; i++ ) { // Loop through source + + sbiDecode = DECODABET[ source[i]&0xFF ]; + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if( sbiDecode >= WHITE_SPACE_ENC ) { + if( sbiDecode >= EQUALS_SIGN_ENC ) { + b4[ b4Posn++ ] = source[i]; // Save non-whitespace + if( b4Posn > 3 ) { // Time to decode? + outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if( source[i] == EQUALS_SIGN ) { + break; + } // end if: equals sign + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else { + // There's a bad input character in the Base64 stream. + throw new java.io.IOException( String.format( + "Bad Base64 input character decimal %d in array position %d", ((int)source[i])&0xFF, i ) ); + } // end else: + } // each input character + + byte[] out = new byte[ outBuffPosn ]; + System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); + return out; + } // end decode + + + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @throws java.io.IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode( String s ) throws java.io.IOException { + return decode( s, NO_OPTIONS ); + } + + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode( String s, int options ) throws java.io.IOException { + + if( s == null ){ + throw new NullPointerException( "Input string was null." ); + } // end if + + byte[] bytes; + try { + bytes = s.getBytes( PREFERRED_ENCODING ); + } // end try + catch( java.io.UnsupportedEncodingException uee ) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode( bytes, 0, bytes.length, options ); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + boolean dontGunzip = (options & DONT_GUNZIP) != 0; + if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) { + + int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream( bytes ); + gzis = new java.util.zip.GZIPInputStream( bais ); + + while( ( length = gzis.read( buffer ) ) >= 0 ) { + baos.write(buffer,0,length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch( java.io.IOException e ) { + e.printStackTrace(); + // Just return originally-decoded bytes + } // end catch + finally { + try{ baos.close(); } catch( Exception e ){} + try{ gzis.close(); } catch( Exception e ){} + try{ bais.close(); } catch( Exception e ){} + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject( String encodedObject ) + throws java.io.IOException, java.lang.ClassNotFoundException { + return decodeToObject(encodedObject,NO_OPTIONS,null); + } + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * If loader is not null, it will be the class loader + * used when deserializing. + * + * @param encodedObject The Base64 data to decode + * @param options Various parameters related to decoding + * @param loader Optional class loader to use in deserializing classes. + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 2.3.4 + */ + public static Object decodeToObject( + String encodedObject, int options, final ClassLoader loader ) + throws java.io.IOException, java.lang.ClassNotFoundException { + + // Decode and gunzip if necessary + byte[] objBytes = decode( encodedObject, options ); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream( objBytes ); + + // If no custom class loader is provided, use Java's builtin OIS. + if( loader == null ){ + ois = new java.io.ObjectInputStream( bais ); + } // end if: no loader provided + + // Else make a customized object input stream that uses + // the provided class loader. + else { + ois = new java.io.ObjectInputStream(bais){ + @Override + public Class resolveClass(java.io.ObjectStreamClass streamClass) + throws java.io.IOException, ClassNotFoundException { + Class c = Class.forName(streamClass.getName(), false, loader); + if( c == null ){ + return super.resolveClass(streamClass); + } else { + return c; // Class loader knows of this class. + } // end else: not null + } // end resolveClass + }; // end ois + } // end else: no custom class loader + + obj = ois.readObject(); + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch( java.lang.ClassNotFoundException e ) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + try{ bais.close(); } catch( Exception e ){} + try{ ois.close(); } catch( Exception e ){} + } // end finally + + return obj; + } // end decodeObject + + + + /** + * Convenience method for encoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile( byte[] dataToEncode, String filename ) + throws java.io.IOException { + + if( dataToEncode == null ){ + throw new NullPointerException( "Data to encode was null." ); + } // end iff + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.ENCODE ); + bos.write( dataToEncode ); + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try{ bos.close(); } catch( Exception e ){} + } // end finally + + } // end encodeToFile + + + /** + * Convenience method for decoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile( String dataToDecode, String filename ) + throws java.io.IOException { + + Base64.OutputStream bos = null; + try{ + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.DECODE ); + bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try{ bos.close(); } catch( Exception e ){} + } // end finally + + } // end decodeToFile + + + + + /** + * Convenience method for reading a base64-encoded + * file and decoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading encoded data + * @return decoded byte array + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile( String filename ) + throws java.io.IOException { + + byte[] decodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if( file.length() > Integer.MAX_VALUE ) + { + throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." ); + } // end if: file too big for int index + buffer = new byte[ (int)file.length() ]; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.DECODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { + length += numBytes; + } // end while + + // Save in a variable to return + decodedData = new byte[ length ]; + System.arraycopy( buffer, 0, decodedData, 0, length ); + + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try{ bis.close(); } catch( Exception e) {} + } // end finally + + return decodedData; + } // end decodeFromFile + + + + /** + * Convenience method for reading a binary file + * and base64-encoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading binary data + * @return base64-encoded string + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile( String filename ) + throws java.io.IOException { + + String encodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.ENCODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { + length += numBytes; + } // end while + + // Save in a variable to return + encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); + + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try{ bis.close(); } catch( Exception e) {} + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile( String infile, String outfile ) + throws java.io.IOException { + + String encoded = Base64.encodeFromFile( infile ); + java.io.OutputStream out = null; + try{ + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream( outfile ) ); + out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output. + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { out.close(); } + catch( Exception ex ){} + } // end finally + } // end encodeFileToFile + + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile( String infile, String outfile ) + throws java.io.IOException { + + byte[] decoded = Base64.decodeFromFile( infile ); + java.io.OutputStream out = null; + try{ + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream( outfile ) ); + out.write( decoded ); + } // end try + catch( java.io.IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { out.close(); } + catch( Exception ex ){} + } // end finally + } // end decodeFileToFile + + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + private int options; // Record options used to create the stream. + private byte[] decodabet; // Local copies to avoid extra method calls + + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * @since 1.3 + */ + public InputStream( java.io.InputStream in ) { + this( in, DECODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.InputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.InputStream( in, Base64.DECODE ) + * + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream( java.io.InputStream in, int options ) { + + super( in ); + this.options = options; // Record for later + this.breakLines = (options & DO_BREAK_LINES) > 0; + this.encode = (options & ENCODE) > 0; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[ bufferLength ]; + this.position = -1; + this.lineLength = 0; + this.decodabet = getDecodabet(options); + } // end constructor + + /** + * Reads enough of the input stream to convert + * to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + @Override + public int read() throws java.io.IOException { + + // Do we need to get data? + if( position < 0 ) { + if( encode ) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for( int i = 0; i < 3; i++ ) { + int b = in.read(); + + // If end of stream, b is -1. + if( b >= 0 ) { + b3[i] = (byte)b; + numBinaryBytes++; + } else { + break; // out of for loop + } // end else: end of stream + + } // end for: each needed input byte + + if( numBinaryBytes > 0 ) { + encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; // Must be end of stream + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for( i = 0; i < 4; i++ ) { + // Read four "meaningful" bytes: + int b = 0; + do{ b = in.read(); } + while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); + + if( b < 0 ) { + break; // Reads a -1 if end of stream + } // end if: end of stream + + b4[i] = (byte)b; + } // end for: each needed input byte + + if( i == 4 ) { + numSigBytes = decode4to3( b4, 0, buffer, 0, options ); + position = 0; + } // end if: got four characters + else if( i == 0 ){ + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException( "Improperly padded Base64 input." ); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if( position >= 0 ) { + // End of relevant data? + if( /*!encode &&*/ position >= numSigBytes ){ + return -1; + } // end if: got data + + if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[ position++ ]; + + if( position >= bufferLength ) { + position = -1; + } // end if: end + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + throw new java.io.IOException( "Error in Base64 code reading stream." ); + } // end else + } // end read + + + /** + * Calls {@link #read()} repeatedly until the end of stream + * is reached or len bytes are read. + * Returns number of bytes read into array or -1 if + * end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + @Override + public int read( byte[] dest, int off, int len ) + throws java.io.IOException { + int i; + int b; + for( i = 0; i < len; i++ ) { + b = read(); + + if( b >= 0 ) { + dest[off + i] = (byte) b; + } + else if( i == 0 ) { + return -1; + } + else { + break; // Out of 'for' loop + } // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + + + + + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out ) { + this( out, ENCODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.OutputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out, int options ) { + super( out ); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[ bufferLength ]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } // end constructor + + + /** + * Writes the byte to the output stream after + * converting to/from Base64 notation. + * When encoding, bytes are buffered three + * at a time before the output stream actually + * gets a write() call. + * When decoding, bytes are buffered four + * at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(int theByte) + throws java.io.IOException { + // Encoding suspended? + if( suspendEncoding ) { + this.out.write( theByte ); + return; + } // end if: supsended + + // Encode? + if( encode ) { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) { // Enough to encode. + + this.out.write( encode3to4( b4, buffer, bufferLength, options ) ); + + lineLength += 4; + if( breakLines && lineLength >= MAX_LINE_LENGTH ) { + this.out.write( NEW_LINE ); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) { // Enough to output. + + int len = Base64.decode4to3( buffer, 0, b4, 0, options ); + out.write( b4, 0, len ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) { + throw new java.io.IOException( "Invalid character in Base64 data." ); + } // end else: not white space either + } // end else: decoding + } // end write + + + + /** + * Calls {@link #write(int)} repeatedly until len + * bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write( byte[] theBytes, int off, int len ) + throws java.io.IOException { + // Encoding suspended? + if( suspendEncoding ) { + this.out.write( theBytes, off, len ); + return; + } // end if: supsended + + for( int i = 0; i < len; i++ ) { + write( theBytes[ off + i ] ); + } // end for: each byte written + + } // end write + + + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] + * This pads the buffer without closing the stream. + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws java.io.IOException { + if( position > 0 ) { + if( encode ) { + out.write( encode3to4( b4, buffer, position, options ) ); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException( "Base64 input not properly padded." ); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + + + /** + * Suspends encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @throws java.io.IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + + /** + * Resumes encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + + + } // end inner class OutputStream + + +} // end class Base64 diff --git a/org.intrace/testsrc/org/intracetest/agent/AgentTest.java b/org.intrace/testsrc/org/intracetest/agent/AgentTest.java index 0fe0b5b..e3f8ae4 100644 --- a/org.intrace/testsrc/org/intracetest/agent/AgentTest.java +++ b/org.intrace/testsrc/org/intracetest/agent/AgentTest.java @@ -109,9 +109,28 @@ public class AgentTest extends TestCase assertEquals(helpResponse, expectedHelpResponse); } + /** + Intentionally, testSetting(AgentConfigConstants.GZIP, "true") does not work. + For simplicity's sake, this setting can only be configured as an intrace agent command line parameter -- [gzip-true + Note that there is no GUI checkbox to enabled Gzip. + To instead enable the client to change this, just change how ClassTransformer#getResponse() deals with a new gzip parameter. + The following simply confirms that +

+ * + * @throws Exception + */ + public void testRetrievalOfGzipSetting() throws Exception { + testGetSetting(AgentConfigConstants.GZIP, "false"); + } public void testSettings() throws Exception { // Boolean settings + + + testSetting(AgentConfigConstants.INSTRU_ENABLED, "true"); testSetting(AgentConfigConstants.INSTRU_ENABLED, "false"); testSetting(AgentConfigConstants.SAVE_TRACED_CLASSFILES, "true"); @@ -124,8 +143,12 @@ public class AgentTest extends TestCase testSetting(AgentConfigConstants.INSTRU_ENABLED, "true"); testSetting(AgentConfigConstants.VERBOSE_MODE, "true"); testSetting(AgentConfigConstants.CLASS_REGEX, ".*"); + + //Need to learn more about why these don't work here. --ETO 12/1/2013 + //testSetting(TraceConfigConstants.BRANCH, "true"); + //testSetting(TraceConfigConstants.BRANCH, "false"); } - + public void testBranchPatterns() throws Throwable { // Create and init the mock @@ -205,6 +228,7 @@ public class AgentTest extends TestCase // Setup agent testSetting(AgentConfigConstants.INSTRU_ENABLED, "false"); + testSetting(AgentConfigConstants.GZIP, "false"); testSetting(AgentConfigConstants.CLASS_REGEX, "BranchPatterns"); testSetting(AgentConfigConstants.VERBOSE_MODE, "false"); testSetting(AgentConfigConstants.SAVE_TRACED_CLASSFILES, "true"); @@ -320,6 +344,7 @@ public class AgentTest extends TestCase // Setup agent testSetting(AgentConfigConstants.INSTRU_ENABLED, "false"); + testSetting(AgentConfigConstants.GZIP, "false"); testSetting(AgentConfigConstants.CLASS_REGEX, "ArgumentTypes"); testSetting(AgentConfigConstants.VERBOSE_MODE, "false"); testSetting(AgentConfigConstants.SAVE_TRACED_CLASSFILES, "true"); @@ -552,6 +577,18 @@ public class AgentTest extends TestCase Map settingsResponseMap = (Map) settingsResponse; assertEquals(configValue, settingsResponseMap.get(configConstant)); } + @SuppressWarnings("unchecked") + private void testGetSetting(String configConstant, String configValue) + throws Exception + { + // Get settings + sender.sendMessage("getsettings"); + Object settingsResponse = receiver.incomingMessages.take(); + assertNotNull(settingsResponse); + assertTrue(settingsResponse instanceof Map); + Map settingsResponseMap = (Map) settingsResponse; + assertEquals(configValue, settingsResponseMap.get(configConstant)); + } @Override protected void tearDown() throws Exception diff --git a/org.intrace/testsrc/org/intracetest/agent/CompressionTest.java b/org.intrace/testsrc/org/intracetest/agent/CompressionTest.java new file mode 100644 index 0000000..19a25e5 --- /dev/null +++ b/org.intrace/testsrc/org/intracetest/agent/CompressionTest.java @@ -0,0 +1,185 @@ +package org.intracetest.agent; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.intrace.shared.Base64; +/** + +This test proves out the following API using text data from an intrace Event + +* String encoded = Base64.encode( myByteArray ); + *
+ * byte[] myByteArray = Base64.decode( encoded ); + +
+
+				Plain ASCII		 Base64		Base64+GZIP
+				-----------------------------------------
+Tiny		   |	 70			   96			 116
+               |
+Small		   |	248			  332			 256
+               |
+Big     	   |   2306		     3076		  	 904
+               |
+Huge    	   |  12591		    16788		  	1036
+
+
+
+ How small can we compress the InTrace event text for various sized InTrace events? +
    +
  • Base64 without GZIP was detrimental to small compression size in all cases
  • +
  • The 70 and 248 byte events were slightly larger with both Base64 and Base64+GZIP -- bad but a not horrible show stopper.
  • +
  • The Big event was more than 2x as small with Base64+GZIP -- good.
  • +
  • The Huge event was more than 10x as small with Base64+GZIP -- great.
  • +
  • +
+ + Conclusions: + Will not use Base64 alone -- doesn't provide any compression with these samples. + Base64+GZIP doesn't hurt much with smaller event + + + + */ +public class CompressionTest extends TestCase +{ + private static final String ORIGINAL_TINY_INTRACE_EVENT = "[18:07:53.683]:[67]:org.hsqldb.jdbc.jdbcConnection:prepareStatement: {"; + private static final String ORIGINAL_SMALL_INTRACE_EVENT = "[18:07:53.683]:[67]:org.hsqldb.jdbc.jdbcConnection:prepareStatement" + + ": Return: org.hsqldb.jdbc.jdbcPreparedStatement@4b8efa2f[sql=" + + "[INSERT INTO Event (name, description, date, location) VALUES(?, ?, ?, ?)]" + + ", parameters=[[null], [null], [null], [null]]]"; + /** + * This isn't a real event (yet)...just adding some text to assess compression for larger messages. + */ + private static final String ORIGINAL_LARGE_STACK_TRACE = "[18:07:53.683]:[67]:org.hsqldb.jdbc.jdbcConnection:prepareStatement" + + ": Return: org.hsqldb.jdbc.jdbcPreparedStatement@4b8efa2f[sql=" + + "[INSERT INTO Event (name, description, date, location) VALUES(?, ?, ?, ?)]" + + ", parameters=[[null], [null], [null], [null]]]" + + "~java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "; + + /** + * This isn't a real event at all...just adding some text to assess compression for msg with more bytes. + */ + private static final String ORIGINAL_HUGE_STACK_TRACE = "[18:07:53.683]:[67]:org.hsqldb.jdbc.jdbcConnection:prepareStatement" + + ": Return: org.hsqldb.jdbc.jdbcPreparedStatement@4b8efa2f[sql=" + + "[INSERT INTO Event (name, description, date, location) VALUES(?, ?, ?, ?)]" + + ", parameters=[[null], [null], [null], [null]]]" + + "~java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "+ + "java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "+ + "java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "+ + "java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "+ + "java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "+ + "java.lang.Thread.getStackTrace(Thread.java:1567) example.webapp.servlet.HelloWorld.doGet(HelloWorld.java:38) javax.servlet.http.HttpServlet.service(HttpServlet.java:668) javax.servlet.http.HttpServlet.service(HttpServlet.java:770) org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:455) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137) org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:560) org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231) org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1072) org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:382) org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193) org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1006) org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) org.eclipse.jetty.server.Server.handle(Server.java:365) org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:485) org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:53) org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:926) org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:988) org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:635) org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:72) org.eclipse.jetty.server.bio.SocketConnector$ConnectorEndPoint.run(SocketConnector.java:264) org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) java.lang.Thread.run(Thread.java:722) "; + + + public void testBasicCompressionAndDecompression_smallerWithGZip() throws IOException + { +// System.out.println("Original length of short data [" + ORIGINAL_SMALL_INTRACE_EVENT.length() + "]"); +// System.out.println("Original length of long data [" + ORIGINAL_LARGE_STACK_TRACE.length() + "]"); + byte[] eventData = ORIGINAL_SMALL_INTRACE_EVENT.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.GZIP); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.GZIP); + String reconstitutedString = new String(reconstitutedData); + + assertEquals("Size of original text", 248, ORIGINAL_SMALL_INTRACE_EVENT.length() ); + assertEquals("Size of compressed text", 256, compressedString.length() ); +// System.out.println("Compressed data [" + compressedString + "]"); +// System.out.println("Compressed with GZIP length - small data [" + compressedString.length() + "]"); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_SMALL_INTRACE_EVENT, reconstitutedString); + } + public void testBasicCompressionAndDecompression_largerWithGZip() throws IOException + { + byte[] eventData = ORIGINAL_LARGE_STACK_TRACE.getBytes(); + String compressedData = Base64.encodeBytes(eventData, Base64.GZIP); + //String compressedData = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + byte[] reconstitutedData = Base64.decode(compressedData, Base64.GZIP); + String reconstitutedString = new String(reconstitutedData); + + assertEquals("Size of original text", 2306, ORIGINAL_LARGE_STACK_TRACE.length() ); + assertEquals("Size of compressed text", 904, compressedData.length() ); + +// System.out.println("Compressed data [" + compressedData + "]"); +// System.out.println("Compressed with GZIP data - long data [" + compressedData.length() + "]"); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_LARGE_STACK_TRACE, reconstitutedString); + } + public void testBasicCompressionAndDecompression_smallerWithoutGZip() throws IOException + { + byte[] eventData = ORIGINAL_SMALL_INTRACE_EVENT.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.NO_OPTIONS); + String reconstitutedString = new String(reconstitutedData); + + + assertEquals("Size of original text", 248, ORIGINAL_SMALL_INTRACE_EVENT.length() ); + assertEquals("Size of compressed text", 332, compressedString.length() ); +// System.out.println("Compressed data [" + compressedString + "]"); +// System.out.println("Compressed without gzip data - small data [" + compressedString.length() + "]"); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_SMALL_INTRACE_EVENT, reconstitutedString); + } + public void testBasicCompressionAndDecompression_largerWithoutGZip() throws IOException + { + byte[] eventData = ORIGINAL_LARGE_STACK_TRACE.getBytes(); + String compressedData = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + //String compressedData = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + byte[] reconstitutedData = Base64.decode(compressedData, Base64.NO_OPTIONS); + String reconstitutedString = new String(reconstitutedData); + + assertEquals("Size of original text", 2306, ORIGINAL_LARGE_STACK_TRACE.length() ); + assertEquals("Size of compressed text", 3076, compressedData.length() ); + +// System.out.println("Compressed data [" + compressedData + "]"); +// System.out.println("Compressed without gzip - large data [" + compressedData.length() + "]"); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_LARGE_STACK_TRACE, reconstitutedString); + } + + public void testBasicCompressionAndDecompression_tinyWithGZip() throws IOException + { + byte[] eventData = ORIGINAL_TINY_INTRACE_EVENT.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.GZIP); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.GZIP); + String reconstitutedString = new String(reconstitutedData); + + + assertEquals("Size of original text", 70, ORIGINAL_TINY_INTRACE_EVENT.length() ); + assertEquals("Size of compressed text", 116, compressedString.length() ); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_TINY_INTRACE_EVENT, reconstitutedString); + } + public void testBasicCompressionAndDecompression_tinyWithoutGZip() throws IOException + { + byte[] eventData = ORIGINAL_TINY_INTRACE_EVENT.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.NO_OPTIONS); + String reconstitutedString = new String(reconstitutedData); + + + assertEquals("Size of original text", 70, ORIGINAL_TINY_INTRACE_EVENT.length() ); + assertEquals("Size of compressed text", 96, compressedString.length() ); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_TINY_INTRACE_EVENT, reconstitutedString); + } + public void testBasicCompressionAndDecompression_hugeWithGZip() throws IOException + { + byte[] eventData = ORIGINAL_HUGE_STACK_TRACE.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.GZIP); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.GZIP); + String reconstitutedString = new String(reconstitutedData); + + + assertEquals("Size of original text", 12591, ORIGINAL_HUGE_STACK_TRACE.length() ); + assertEquals("Size of compressed text", 1036, compressedString.length() ); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_HUGE_STACK_TRACE, reconstitutedString); + } + public void testBasicCompressionAndDecompression_hugeWithoutGZip() throws IOException + { + byte[] eventData = ORIGINAL_HUGE_STACK_TRACE.getBytes(); + String compressedString = Base64.encodeBytes(eventData, Base64.NO_OPTIONS); + byte[] reconstitutedData = Base64.decode(compressedString, Base64.NO_OPTIONS); + String reconstitutedString = new String(reconstitutedData); + + + assertEquals("Size of original text", 12591, ORIGINAL_HUGE_STACK_TRACE.length() ); + assertEquals("Size of compressed text", 16788, compressedString.length() ); + assertEquals("Unable to compress and decompress intrace event data", ORIGINAL_HUGE_STACK_TRACE, reconstitutedString); + } +} diff --git a/org.intrace/testsrc/org/intracetest/agent/TraceSettingsTest.java b/org.intrace/testsrc/org/intracetest/agent/TraceSettingsTest.java new file mode 100644 index 0000000..26d769a --- /dev/null +++ b/org.intrace/testsrc/org/intracetest/agent/TraceSettingsTest.java @@ -0,0 +1,48 @@ +package org.intracetest.agent; + +import java.util.Map; + +import junit.framework.TestCase; + +import org.intrace.agent.AgentSettings; +import org.intrace.output.trace.TraceSettings; +import org.intrace.shared.AgentConfigConstants; +import org.intrace.shared.TraceConfigConstants; + +public class TraceSettingsTest extends TestCase +{ + public void testAgentSettings() + { + TraceSettings ts = new TraceSettings( + TraceConfigConstants.ARG + + "true" + + TraceConfigConstants.ARRAYS + + "true" + + TraceConfigConstants.ENTRY_EXIT + + "true" + + TraceConfigConstants.BRANCH + + "true" + ); + + assertTrue(ts.isArgTraceEnabled()); + assertTrue(ts.isEntryExitTraceEnabled()); + assertTrue(ts.isBranchTraceEnabled()); + assertTrue(ts.isTruncateArraysEnabled()); + + ts = new TraceSettings(ts); + assertTrue(ts.isArgTraceEnabled()); + assertTrue(ts.isEntryExitTraceEnabled()); + assertTrue(ts.isBranchTraceEnabled()); + assertTrue(ts.isTruncateArraysEnabled()); + + String toString = ts.toString(); + assertNotNull(toString); + + Map settingsMap = ts.getSettingsMap(); + assertEquals(settingsMap.get(TraceConfigConstants.ARG), "true"); + assertEquals(settingsMap.get(TraceConfigConstants.ARRAYS), "true"); + assertEquals(settingsMap.get(TraceConfigConstants.BRANCH), "true"); + assertEquals(settingsMap.get(TraceConfigConstants.ENTRY_EXIT), "true"); + } + +}