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 015package org.apache.tapestry.record; 016 017import java.io.BufferedInputStream; 018import java.io.BufferedOutputStream; 019import java.io.ByteArrayInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.ObjectInputStream; 024import java.io.ObjectOutputStream; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.Iterator; 028import java.util.List; 029import java.util.zip.GZIPInputStream; 030import java.util.zip.GZIPOutputStream; 031 032import org.apache.commons.codec.binary.Base64; 033import org.apache.hivemind.ApplicationRuntimeException; 034import org.apache.hivemind.ClassResolver; 035import org.apache.hivemind.HiveMind; 036import org.apache.hivemind.util.Defense; 037import org.apache.tapestry.util.io.ResolvingObjectInputStream; 038import 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 */ 051public 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}