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