001 /*
002 Copyright (c) 1996-2011, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029 package org.hd.d.pg2k.svrCore;
030
031 import java.io.ByteArrayInputStream;
032 import java.io.ByteArrayOutputStream;
033 import java.io.DataInputStream;
034 import java.io.DataOutputStream;
035 import java.io.IOException;
036 import java.io.InputStream;
037 import java.io.Serializable;
038 import java.util.Arrays;
039
040 /**An immutable, Serializable, wrapper for a byte array.
041 * This enables byte array data to be safely shared between multiple users,
042 * thus avoid copies and duplicates.
043 * <p>
044 * We clone at entry and exit and deserialisation to protect our internal data.
045 * <p>
046 * This also has helper methods for storing/extracting (with zlib)
047 * lossless-compressed String values.
048 * (The internal form is deflated UTF-8 without headers or checksum.)
049 * <p>
050 * These instances are suitable for pooling with MemoryTools.intern().
051 * <p>
052 * Intended to be efficient on the wire and in memory.
053 */
054 public final class ROByteArray implements Serializable, MemoryTools.Internable
055 {
056 /**Construct with (copy of) non-null data block.
057 * @param data non-null array of bytes
058 */
059 public ROByteArray(final byte[] data)
060 {
061 if(data == null) { throw new IllegalArgumentException(); }
062 payload = data.clone();
063 }
064
065 /**The (non-null) payload reference. */
066 private final byte[] payload;
067
068 /**Get (a copy of) the entire data block; never null. */
069 public byte[] toByteArray() { return(payload.clone()); }
070
071 /**Get (a copy of) a portion of the data block.
072 */
073 public byte[] getData(final int start, final int afterEnd)
074 {
075 final int length = afterEnd - start;
076 final byte result[] = new byte[length];
077 System.arraycopy(payload, start, result, 0, length);
078 return(result);
079 }
080
081 /**Get length of the data; non-negative. */
082 public int length() { return(payload.length); }
083
084 /**Get value at specified index. */
085 public byte get(final int index) { return(payload[index]); }
086
087 /**Compute a hash based on the length and some of the payload.
088 * This tries to fix the time taken to compute the hash
089 * even for large payloads at the cost of possible/probable extra collisions
090 * (as per the String experience).
091 * <p>
092 * This takes a fixed small number of sample points whose relative positions
093 * partly depend on the length, and the length itself is part of the hash.
094 */
095 @Override
096 public int hashCode()
097 {
098 final int len = payload.length;
099 final int dataHash = (len == 0) ? 907 :
100 (
101 (len * 0x305011) ^
102 payload[0] ^
103 (~payload[len/5] << 8) +
104 ((payload[(23603 + (0xff & payload[len/3])) % len]) * 65053) -
105 (payload[len-1] << 23)
106 );
107 return(dataHash);
108 }
109
110 /**Is equal to an instance with a payload of the same length and content; may be slow to compute. */
111 @Override
112 public boolean equals(final Object obj)
113 {
114 if(this == obj) { return(true); }
115 if(!(obj instanceof ROByteArray)) { return(false); }
116 return(Arrays.equals(payload, ((ROByteArray)obj).payload));
117 }
118
119 /**Returns a human-readable representation of this object. */
120 @Override
121 public String toString()
122 { return(toHexString()); }
123
124
125
126 /**Get internal data as InputStream (supporting mark()).
127 * This gives read-only access to the data as a markable/rewindable stream.
128 * <p>
129 * Each call produces a completely independent stream.
130 */
131 public InputStream getInputStream()
132 {
133 // This relies for safety on ByteArrayInputStream
134 // not allowing the payload to be altered.
135 // This relies for efficiency on ByteArrayInputStream
136 // not copying the data.
137 return(new ByteArrayInputStream(payload));
138 }
139
140
141 /**The set of characters from which we produce our hex digits. */
142 private static final String HEX_DIGITS = "0123456789abcdef";
143
144 /**Returns a fixed-length unsigned lower-case hex representation of the data.
145 * This returns exactly two (lower-case) hex digits for each byte of data,
146 * with the high nybble before the low nybble,
147 * and lower-index bytes before higher-index bytes.
148 * <p>
149 * For example, a data array containing { 13, 241 }
150 * would produce "0df1".
151 */
152 public String toHexString()
153 {
154 final StringBuilder result = new StringBuilder(payload.length * 2);
155 for(int i = 0; i < payload.length; ++i)
156 {
157 result.append(HEX_DIGITS.charAt((payload[i] >> 4) & 0xf));
158 result.append(HEX_DIGITS.charAt(payload[i] & 0xf));
159 }
160 return(result.toString());
161 }
162
163 /**Creates instance from a hex representation as generated by toHexString().
164 * Input must be non-null, even number of lower-case hex characters.
165 *
166 * @throws IllegalArgumentException for invalid input
167 */
168 public static ROByteArray fromHexString(final String s)
169 {
170 if((s == null) || ((s.length() & 1) == 1))
171 { throw new IllegalArgumentException(); }
172
173 final byte payload[] = new byte[s.length() / 2];
174
175 for(int i = payload.length; --i >= 0; )
176 {
177 final char hc = s.charAt(i*2);
178 final char lc = s.charAt(i*2 + 1);
179
180 final int hn = HEX_DIGITS.indexOf(hc);
181 final int ln = HEX_DIGITS.indexOf(lc);
182
183 if((hn == -1) || (ln == -1))
184 { throw new IllegalArgumentException("invalid character"); }
185
186 // Assemble the byte from the high and low nybbles.
187 payload[i] = (byte) ((hn << 4) | ln);
188 }
189
190 return(new ROByteArray(payload));
191 }
192
193
194
195 /**Create an instance from the compressed UTF-8 form of the argumentString.
196 * Unlikely to work well on short Strings.
197 * <p>
198 * The compression is zlib deflate without any checksums or other avoidable
199 * redundant data, so error checking had better be done elsewhere.
200 * <p>
201 * The String value can be recovered with the uncompressToString() method.
202 *
203 * @param in non-null String
204 */
205 public static final ROByteArray compressFromString(final String in)
206 {
207 if(in == null) { throw new IllegalArgumentException(); }
208 try
209 {
210 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
211 final DataOutputStream dos = new DataOutputStream(baos);
212 dos.writeUTF(in);
213 dos.flush();
214 final byte raw[] = baos.toByteArray();
215 dos.close();
216 final byte comp[] = FileTools.compressDeflatableData(raw, 0, raw.length);
217 return(new ROByteArray(comp));
218 }
219 catch(final IOException e)
220 {
221 // Should never happen...
222 throw new Error("Unexpected exception", e);
223 }
224 }
225
226 /**Extract a String from an ROByteArray created by compressFromString(); never null.
227 * Results are undefined on other inputs.
228 *
229 * @param in non-null, non-empty value generated by compressFromString()
230 *
231 * @throws IOException when having difficulty decoding the input
232 */
233 public static final String uncompressToString(final ROByteArray in)
234 throws IOException
235 {
236 if((in == null) || (in.length() == 0)) { throw new IllegalArgumentException(); }
237
238 final ByteArrayInputStream bais = new ByteArrayInputStream(
239 FileTools.decompressDeflatedData(in.payload));
240 final DataInputStream dis = new DataInputStream(bais);
241 final String s = dis.readUTF();
242 dis.close(); // Help GC...
243 return(s);
244 }
245
246
247 /**Empty array. */
248 public static final ROByteArray EMPTY = new ROByteArray(new byte[0]);
249
250 /**Deserialise: use constructor for validation, defensive copying, etc.
251 * Also resolve all empty instances to a single value as a minor optimisation.
252 */
253 protected Object readResolve()
254 // throws ObjectStreamException
255 {
256 // Avoid duplicates of empty case.
257 if(payload.length == 0) { return(EMPTY); }
258
259 // Construct new instance of object in normal defensive way.
260 return(new ROByteArray(payload));
261 }
262
263 /**Unique Serialisation class ID generated by http://random.hd.org/. */
264 private static final long serialVersionUID = 3806652319473305560L;
265 }