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 }