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        }