001 package org.apache.tapestry.util.io;
002
003 import org.apache.commons.codec.binary.Base64;
004 import org.apache.hivemind.ApplicationRuntimeException;
005 import org.apache.hivemind.HiveMind;
006 import org.apache.hivemind.util.Defense;
007
008 import java.io.*;
009 import java.util.zip.GZIPInputStream;
010 import java.util.zip.GZIPOutputStream;
011
012 /**
013 * Utility class used by {@link org.apache.tapestry.IRequestCycle} to compress {@link org.apache.tapestry.util.IdAllocator}
014 * state.
015 */
016 public class CompressedDataEncoder {
017
018 /**
019 * Prefix on the MIME encoding that indicates that the encoded data is not encoded.
020 */
021
022 public static final String BYTESTREAM_PREFIX = "B";
023
024 /**
025 * Prefix on the MIME encoding that indicates that the encoded data is encoded with GZIP.
026 */
027
028 public static final String GZIP_BYTESTREAM_PREFIX = "Z";
029
030 private CompressedDataEncoder() {}
031
032 /**
033 * Encodes the given string into a compressed string representation that can later be decoded.
034 *
035 * @param input
036 * String input to compress and encode into a persistable form.
037 *
038 * @return encoded string (possibly empty, but not null)
039 */
040 public static String encodeString(String input)
041 {
042 Defense.notNull(input, "input");
043
044 if (HiveMind.isBlank(input))
045 return "";
046
047 try {
048 ByteArrayOutputStream bosPlain = new ByteArrayOutputStream();
049 ByteArrayOutputStream bosCompressed = new ByteArrayOutputStream();
050
051 GZIPOutputStream gos = new GZIPOutputStream(bosCompressed);
052 TeeOutputStream tos = new TeeOutputStream(bosPlain, gos);
053 ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(tos));
054
055 oos.writeUTF(input);
056
057 oos.close();
058
059 boolean useCompressed = bosCompressed.size() < bosPlain.size();
060
061 byte[] data = useCompressed ? bosCompressed.toByteArray() : bosPlain.toByteArray();
062
063 byte[] encoded = Base64.encodeBase64(data);
064
065 String prefix = useCompressed ? GZIP_BYTESTREAM_PREFIX : BYTESTREAM_PREFIX;
066
067 return prefix + new String(encoded);
068 }
069 catch (Exception ex) {
070 throw new ApplicationRuntimeException(IoMessages.encodeFailure(input, ex), ex);
071 }
072 }
073
074 /**
075 * Takes a string with an encoded and compressed input as produced by {@link #encodeString(String)} , and converts it back
076 * into the original String representation.
077 *
078 * @param input
079 * The data to un-encode, which should be equivalent to the same that
080 * was passed in to {@link #encodeString(String)}.
081 *
082 * @return The decoded string data.
083 */
084 public static String decodeString(String input)
085 {
086 if (HiveMind.isBlank(input))
087 return "";
088
089 String prefix = input.substring(0, 1);
090
091 if (!(prefix.equals(BYTESTREAM_PREFIX) || prefix.equals(GZIP_BYTESTREAM_PREFIX)))
092 throw new ApplicationRuntimeException(IoMessages.unknownPrefix(prefix));
093
094 try {
095 // Strip off the prefix, feed that in as a MIME stream.
096
097 byte[] decoded = Base64.decodeBase64(input.substring(1).getBytes());
098
099 InputStream is = new ByteArrayInputStream(decoded);
100
101 if (prefix.equals(GZIP_BYTESTREAM_PREFIX))
102 is = new GZIPInputStream(is);
103
104 // I believe this is more efficient; the buffered input stream should ask the
105 // GZIP stream for large blocks of un-gzipped bytes, with should be more efficient.
106 // The object input stream will probably be looking for just a few bytes at
107 // a time. We use a resolving object input stream that knows how to find
108 // classes not normally acessible.
109
110 ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(is));
111
112 String result = ois.readUTF();
113
114 is.close();
115
116 return result;
117 }
118 catch (Exception ex) {
119 throw new ApplicationRuntimeException(IoMessages.decodeFailure(ex), ex);
120 }
121 }
122 }