001    // Copyright 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.record;
016    
017    import java.io.BufferedInputStream;
018    import java.io.BufferedOutputStream;
019    import java.io.ByteArrayInputStream;
020    import java.io.ByteArrayOutputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.io.ObjectInputStream;
024    import java.io.ObjectOutputStream;
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Iterator;
028    import java.util.List;
029    import java.util.zip.GZIPInputStream;
030    import java.util.zip.GZIPOutputStream;
031    
032    import org.apache.commons.codec.binary.Base64;
033    import org.apache.hivemind.ApplicationRuntimeException;
034    import org.apache.hivemind.ClassResolver;
035    import org.apache.hivemind.HiveMind;
036    import org.apache.hivemind.util.Defense;
037    import org.apache.tapestry.util.io.ResolvingObjectInputStream;
038    import org.apache.tapestry.util.io.TeeOutputStream;
039    
040    /**
041     * Responsible for converting lists of {@link org.apache.tapestry.record.PropertyChange}s back and
042     * forth to a URL safe encoded string.
043     * <p>
044     * A possible improvement would be to encode the binary data with encryption both on and off, and
045     * select the shortest (prefixing with a character that identifies whether encryption should be used
046     * to decode).
047     * 
048     * @author Howard M. Lewis Ship
049     * @since 4.0
050     */
051    public class PersistentPropertyDataEncoderImpl implements PersistentPropertyDataEncoder
052    {
053        private ClassResolver _classResolver;
054    
055        /**
056         * Prefix on the MIME encoding that indicates that the encoded data is not encoded.
057         */
058    
059        public static final String BYTESTREAM_PREFIX = "B";
060    
061        /**
062         * Prefix on the MIME encoding that indicates that the encoded data is encoded with GZIP.
063         */
064    
065        public static final String GZIP_BYTESTREAM_PREFIX = "Z";
066    
067        public String encodePageChanges(List changes)
068        {
069            Defense.notNull(changes, "changes");
070    
071            if (changes.isEmpty())
072                return "";
073    
074            try
075            {
076                ByteArrayOutputStream bosPlain = new ByteArrayOutputStream();
077                ByteArrayOutputStream bosCompressed = new ByteArrayOutputStream();
078    
079                GZIPOutputStream gos = new GZIPOutputStream(bosCompressed);
080    
081                TeeOutputStream tos = new TeeOutputStream(bosPlain, gos);
082    
083                ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(tos));
084    
085                writeChangesToStream(changes, oos);
086    
087                oos.close();
088    
089                boolean useCompressed = bosCompressed.size() < bosPlain.size();
090    
091                byte[] data = useCompressed ? bosCompressed.toByteArray() : bosPlain.toByteArray();
092    
093                byte[] encoded = Base64.encodeBase64(data);
094    
095                String prefix = useCompressed ? GZIP_BYTESTREAM_PREFIX : BYTESTREAM_PREFIX;
096    
097                return prefix + new String(encoded);
098            }
099            catch (Exception ex)
100            {
101                throw new ApplicationRuntimeException(RecordMessages.encodeFailure(ex), ex);
102            }
103        }
104    
105        public List decodePageChanges(String encoded)
106        {
107            if (HiveMind.isBlank(encoded))
108                return Collections.EMPTY_LIST;
109    
110            String prefix = encoded.substring(0, 1);
111    
112            if (!(prefix.equals(BYTESTREAM_PREFIX) || prefix.equals(GZIP_BYTESTREAM_PREFIX)))
113                throw new ApplicationRuntimeException(RecordMessages.unknownPrefix(prefix));
114    
115            try
116            {
117                // Strip off the prefix, feed that in as a MIME stream.
118    
119                byte[] decoded = Base64.decodeBase64(encoded.substring(1).getBytes());
120    
121                InputStream is = new ByteArrayInputStream(decoded);
122    
123                if (prefix.equals(GZIP_BYTESTREAM_PREFIX))
124                    is = new GZIPInputStream(is);
125    
126                // I believe this is more efficient; the buffered input stream should ask the
127                // GZIP stream for large blocks of un-gzipped bytes, with should be more efficient.
128                // The object input stream will probably be looking for just a few bytes at
129                // a time. We use a resolving object input stream that knows how to find
130                // classes not normally acessible.
131    
132                ObjectInputStream ois = new ResolvingObjectInputStream(_classResolver,
133                        new BufferedInputStream(is));
134    
135                List result = readChangesFromStream(ois);
136    
137                ois.close();
138    
139                return result;
140            }
141            catch (Exception ex)
142            {
143                throw new ApplicationRuntimeException(RecordMessages.decodeFailure(ex), ex);
144            }
145        }
146    
147        private void writeChangesToStream(List changes, ObjectOutputStream oos) throws IOException
148        {
149            oos.writeInt(changes.size());
150    
151            Iterator i = changes.iterator();
152            while (i.hasNext())
153            {
154                PropertyChange pc = (PropertyChange) i.next();
155    
156                String componentPath = pc.getComponentPath();
157                String propertyName = pc.getPropertyName();
158                Object value = pc.getNewValue();
159    
160                oos.writeBoolean(componentPath != null);
161    
162                if (componentPath != null)
163                    oos.writeUTF(componentPath);
164    
165                oos.writeUTF(propertyName);
166                oos.writeObject(value);
167            }
168        }
169    
170        private List readChangesFromStream(ObjectInputStream ois) throws IOException,
171                ClassNotFoundException
172        {
173            List result = new ArrayList();
174    
175            int count = ois.readInt();
176    
177            for (int i = 0; i < count; i++)
178            {
179                boolean hasPath = ois.readBoolean();
180                String componentPath = hasPath ? ois.readUTF() : null;
181                String propertyName = ois.readUTF();
182                Object value = ois.readObject();
183    
184                PropertyChangeImpl pc = new PropertyChangeImpl(componentPath, propertyName, value);
185    
186                result.add(pc);
187            }
188    
189            return result;
190        }
191    
192        public void setClassResolver(ClassResolver resolver)
193        {
194            _classResolver = resolver;
195        }
196    }