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 }