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    }