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