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 }