001    /*
002     * Created by IntelliJ IDEA.
003     * User: d@hd.org
004     * Date: 21-May-02
005     * Time: 21:35:07
006    
007    Copyright (c) 1996-2012, Damon Hart-Davis
008    All rights reserved.
009    
010    Redistribution and use in source and binary forms, with or without
011    modification, are permitted provided that the following conditions are
012    met:
013    
014      * Redistributions of source code must retain the above copyright
015        notice, this list of conditions and the following disclaimer.
016    
017      * Redistributions in binary form must reproduce the above copyright
018        notice, this list of conditions and the following disclaimer in the
019        documentation and/or other materials provided with the
020        distribution.
021    
022    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
023    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
024    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
025    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033    
034     */
035    package org.hd.d.pg2k.svrCore.uploader;
036    
037    import java.io.ByteArrayOutputStream;
038    import java.io.DataInputStream;
039    import java.io.DataOutputStream;
040    import java.io.IOException;
041    import java.io.InputStream;
042    import java.io.OutputStream;
043    import java.util.concurrent.atomic.AtomicLong;
044    
045    import org.hd.d.pg2k.svrCore.CoreConsts;
046    import org.hd.d.pg2k.svrCore.ExhibitName;
047    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
048    import org.hd.d.pg2k.svrCore.ROByteArray;
049    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
050    
051    
052    /**Utilities for exhibit uploader.
053     */
054    public final class UploaderUtils
055        {
056        /**Prevent creation of instances. */
057        private UploaderUtils() { }
058    
059    
060        /**Size of (large) buffers used for I/O efficiency; strictly positive. */
061        public static final int BATCH_UPLOAD_BUFSIZE = 65536;
062    
063        /**Make body of select HTML select statement, marking any extant value as selected.
064         * The labels[] array can be null, and if so then the values will be used
065         * as the text; if not null it must be the same length as values[] and must
066         * not contain nulls.
067         *
068         * @param values  the values returned by the select item
069         * @param labels  (optional) the text values shown for the select items
070         * @param selected  if equal to one of the values[] entries then that
071         *     item is marked as selected
072         */
073        public static String makeSelectBody(final String values[], final String labels[],
074                                            final String selected)
075            {
076            final StringBuilder sb = new StringBuilder(32 * values.length);
077            for(int i = 0; i < values.length; ++i)
078                {
079                // Do we have different text for value and label?
080                final boolean valueNELabel = (labels != null) &&
081                    !values[i].equals(labels[i]);
082    
083                sb.append("<option");
084                if(valueNELabel)
085                    {
086                    sb.append(" value=");
087                    sb.append(quoteHTMLArg(values[i]));
088                    }
089                if(values[i].equals(selected)) { sb.append(" selected"); }
090                sb.append('>');
091                if(valueNELabel) { sb.append(labels[i]); }
092                else { sb.append(values[i]); }
093                sb.append("</option>");
094                }
095            return(sb.toString());
096            }
097    
098        /**Puts double quotes round any tag attribute value that needs them.
099         * Any tag that does not just contain letters and digits will
100         * get "..." wrapped round it.  If an embedded " is found, or
101         * the tag is found to be too long, an exception is thrown.
102         * We include '_' in this notion of letters.
103         * <p>
104         * If this does not need to change the input it is returned as is.
105         * <p>
106         * TODO: jUnit tests
107         *
108         * @param s     non-null, unquoted HTML tag attribute value.
109         * @exception IllegalArgumentException    impossible to form tag
110         */
111        public static final String quoteHTMLArg(final String s)
112            throws IllegalArgumentException
113            {
114            final int l = s.length();
115            if(l > 1024)
116                { throw new IllegalArgumentException("tag attribute too long"); }
117            if(s.indexOf('"') != -1)
118                { throw new IllegalArgumentException("tag attribute contains `\"'"); }
119    
120            boolean needToQuote = false;
121    
122            // If the string is of zero length, it will need quoting to
123            // avoid ambiguity.
124            if(l == 0) { needToQuote = true; }
125    
126            // Else search the String for characters that would force us to use quotes...
127            else
128                {
129                // Get the data out of the String as chars to look for
130                // anything that will need to be quoted.
131                for(int i = l; --i >= 0; )
132                    {
133                    final char c = s.charAt(i);
134                    // Use the digit() routine as a hack to check if
135                    // character is [0-9A-Za-z].
136                    if((Character.digit(c, 36) == -1) && (c != '_'))
137                        { needToQuote = true; break; }
138                    }
139                }
140    
141            if(needToQuote)
142                {
143                final StringBuilder sb = new StringBuilder(l + 2);
144                sb.append('"');
145                sb.append(s);
146                sb.append('"');
147                return(sb.toString());
148                }
149    
150            return(s);
151            }
152    
153    
154        /**Terminator (7-bit ASCII) character byte for all messages. */
155        public static final char BATCH_UPLOAD_MSG_TERMINATOR = '\n';
156    
157        /**Send response to batch upload client.
158         * <p>
159         * This routine does not close the output stream when done.
160         * <p>
161         * This routine does not flush the output stream when done;
162         * the caller should possibly do so itself.
163         *
164         * @param logger
165         * @param os  stream to write to; never null
166         * @param isOK  if true then this is an "OK" response,
167         *     else a "FAIL" response
168         * @param exhibitName  the syntatically-correct name of the exhibit upload
169         *     to which this response applies, or null for before the first upload
170         * @param detail  the optional extra information on the failure;
171         *     null/"" if to be omitted
172         *
173         * @throws IOException  in case of error writing to the stream
174         */
175        public static void sendResponseToBatchUploadClient(final SimpleLoggerIF logger,
176                                                           final OutputStream os,
177                                                           final boolean isOK,
178                                                           final String exhibitName,
179                                                           final String detail)
180            throws IOException
181            {
182            if(os == null)
183                { throw new IllegalArgumentException(); }
184            if((exhibitName != null) && (!ExhibitName.validNameSyntax(exhibitName)))
185                { throw new IllegalArgumentException(); }
186    
187            // Buffer for efficiency...
188            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
189            final DataOutputStream dos = new DataOutputStream(baos);
190    
191            if(!isOK && (logger != null)) { logger.log(" [Batch client upload failed: "+detail+"]"); }
192    
193            // "O" for OK, "F" for fail.
194            dos.write(isOK ? 'O' : 'F');
195    
196            // Write the exhibit name.
197            dos.writeUTF((exhibitName == null) ? "" : exhibitName);
198    
199            // Write the optional detail.
200            dos.writeUTF((detail == null) ? "" : detail);
201    
202            // Write the termination byte.
203            dos.write(BATCH_UPLOAD_MSG_TERMINATOR);
204    
205            // Done; write to output stream as a block for efficiency.
206            dos.flush();
207            os.write(baos.toByteArray());
208            }
209    
210        /**Decode response to batch upload client from server.
211         * On a fail response the detail message
212         * will be embedded in the detail of the IOException.
213         * <p>
214         * If this detects malformed input
215         * then it will throw an IOException.
216         *
217         * @return  exhibitName or null on success/OK response
218         * @throws IOException  on problem reading the input stream
219         *     or on a fail response
220         */
221        public static String decodeResponseToBatchUploadClient(final InputStream is)
222            throws IOException
223            {
224            if(is == null) { throw new IllegalArgumentException(); }
225    
226            // Read all the fields.
227            // Validate the first field to reject random junk quickly.
228            final DataInputStream dis = new DataInputStream(is);
229            final int t = dis.readByte();
230            if((t != 'O') && (t != 'F')) { throw new IOException("invalid response type code: " + t); }
231            final String exhibitName = dis.readUTF();
232            final String detail = dis.readUTF();
233            final int term = dis.readByte();
234    
235            // Check terminator.
236            if(term != BATCH_UPLOAD_MSG_TERMINATOR)
237                {
238                // Corrupt message (invalid terminator).
239                throw new IOException("invalid message terminator: " + term);
240                }
241    
242            // Check name.
243            if((exhibitName.length() != 0) && !ExhibitName.validNameSyntax(exhibitName))
244                {
245                // Corrupt message (invalid name).
246                throw new IOException("invalid exhibit name");
247                }
248    
249            // Deal with the usual "OK" case...
250            if(t == 'O')
251                {
252                // "OK" message...
253                // Ignore the detail message.
254                return((exhibitName.length() == 0) ? null : exhibitName);
255                }
256    
257            assert(t == 'F');
258    
259            // Deal with the "FAIL" case.
260            throw new IOException((detail.length() == 0) ? null : detail);
261            }
262    
263        /**Compute length of upload request for given exhibit.
264         * Can be used to set a content-length header (etc)
265         * before calling sendRequestFromBatchUploadClient
266         * to deliver the actual data.
267         */
268        public static int computeLengthOfRequestFromBatchUploadClient(
269                                                        final ExhibitStaticAttr esa,
270                                                        final ROByteArray hashMD5,
271                                                        final String description)
272            {
273            if((esa == null) || (hashMD5 == null))
274                { throw new IllegalArgumentException(); }
275    
276            // The easiest way to do this is
277            // to write everything bar the data itself to a buffer
278            // and count the bytes written.
279            // Not marvelously efficient, but probably OK.
280            //
281            // An optimisation is to avoid writing some of the fixed-length parts
282            // and allow for them in the returned length instead.
283            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
284            final DataOutputStream dos = new DataOutputStream(baos);
285    
286            try
287                {
288                // Write the exhibit length.
289    //        dos.writeLong(esa.length); // Allowed for in result.
290    
291                // Write the exhibit name.
292                dos.writeUTF(esa.getFilePath());
293    
294                // Write the hash (as length then raw bytes).
295    //        dos.writeInt(hashMD5.length()); // Allowed for in result.
296    //        dos.write(hashMD5.getData()); // Allowed for in result.
297                // Write out the description (convert null to "").
298                dos.writeUTF((description == null) ? "" : description);
299    
300                // Skip writing the data...
301    
302                // Write the termination byte.
303    //        dos.write(BATCH_UPLOAD_MSG_TERMINATOR); // Allowed for in result.
304    
305                // Force everything into the buffer...
306                dos.flush();
307    
308                final long len =
309                       8 + // Exhibit length.
310                       4 + // Hash length.
311                       hashMD5.length() + // Hash data.
312                       baos.size() + // Variable-length parts...
313                       esa.length + // Data length.
314                       1; // Terminator byte.
315    
316                if(len > Integer.MAX_VALUE)
317                    { throw new IllegalArgumentException("exhibit too large"); }
318    
319                return((int) len);
320                }
321            catch(final IOException e)
322                {
323                throw new Error(e); // Should not happen.
324                }
325            }
326    
327        /**Send upload request and data from batch client to server.
328         * We send up the wire, in order:
329         * <ol>
330         * <li>The putative exhibit length.
331         * <li>The putative (full, syntactically-valid, unique) exhibit name.
332         * <li>The putative exhibit MD5 hash.
333         * <li>The putative exhibit description (or "" if none).
334         * <li>The raw exhibit data.
335         * </ol>
336         * <p>
337         * The exhibit hash serves two purposes:
338         * <ul>
339         * <li>To ensure that the exhibit is unique before starting to upload.
340         * <li>To ensure that the exhibit arrives uncorrupted.
341         * </ul>
342         * <p>
343         * This routine does not close the output stream when done.
344         * <p>
345         * This routine does not flush the output stream when done;
346         * the caller should possibly do so itself.
347         * The routine may flush the output from time to time,
348         * eg after writing a large block.
349         * <p>
350         * If the caller supplies a null esa/hash/datastream
351         * then we send a request up the wire for a zero-length exhibit upload,
352         * which is treated as a request to drop the connection
353         * (the client should drop it after sending this).
354         * <p>
355         * This can update a progress monitor value while it is working.
356         *
357         * @param progress  if non-null, is set to the amount of bytes uploaded
358         *     so far,
359         *     and -1 before any bytes have been uploaded
360         *     (and Long.MAX_VALUE after we have finished while buffered data xfers)
361         *
362         * @throws IOException  in case of error sending data to the output stream;
363         *     if this occurs then the output stream should be closed and discarded
364         */
365        public static void sendRequestFromBatchUploadClient(final OutputStream os,
366                                                            final ExhibitStaticAttr esa,
367                                                            final ROByteArray hashMD5,
368                                                            final String description,
369                                                            final InputStream exhibitData,
370                                                            final AtomicLong progress)
371            throws IOException
372            {
373            if(os == null)
374                { throw new IllegalArgumentException(); }
375    
376            final DataOutputStream dos = new DataOutputStream(os);
377    
378            if((esa == null) && (hashMD5 == null) && (exhibitData == null))
379                {
380                dos.writeLong(0);
381                dos.write(BATCH_UPLOAD_MSG_TERMINATOR);
382                return;
383                }
384    
385            if((esa == null) || (hashMD5 == null) || (exhibitData == null))
386                { throw new IllegalArgumentException(); }
387    
388            if((description != null) &&
389               (description.length() > CoreConsts.DESCRIPTION_MAX_CHARS))
390                { throw new IllegalArgumentException("description too long"); }
391    
392            // If present, update the progress monitor.
393            if(progress != null)
394                {
395                progress.set(-1); // Upload proper not started yet...
396                }
397    
398            // Write the exhibit length.
399            dos.writeLong(esa.length);
400    
401            // Write the exhibit name.
402            dos.writeUTF(esa.getFilePath());
403    
404            // Write the hash (as length then raw bytes).
405            dos.writeInt(hashMD5.length());
406            dos.write(hashMD5.toByteArray());
407    
408            // Write out the description (convert null to "").
409            dos.writeUTF((description == null) ? "" : description);
410    
411            // If present then set progress to show upload just starting.
412            if(progress != null)
413                {
414                progress.set(0); // Upload proper is just starting...
415                }
416    
417            // Copy the raw exhibit data one (large, efficient) block at a time.
418            final byte buf[] = new byte[(int) Math.min(esa.length, BATCH_UPLOAD_BUFSIZE)];
419            for(long bytesLeft = esa.length; bytesLeft > 0; )
420                {
421                final int maxXfer = (int) Math.min(buf.length, bytesLeft);
422                final int read = exhibitData.read(buf, 0, maxXfer);
423                if(read < 1) { throw new IOException("error reading data for exhibit: " + esa); }
424                dos.write(buf, 0, read);
425                bytesLeft -= read;
426    
427                // If present, update the progress monitor with bytes sent so far.
428                if(progress != null)
429                    {
430                    dos.flush(); /* Give truer view of progress... */
431                    progress.set(esa.length - bytesLeft); // Show bytes transferred.
432                    }
433                }
434    
435            // If present then update the progress monitor.
436            if(progress != null)
437                {
438                progress.set(Long.MAX_VALUE); // Buffered data may still need to be transferred, etc.
439                }
440    
441            // Write the termination byte.
442            dos.write(BATCH_UPLOAD_MSG_TERMINATOR);
443            }
444    
445        /**Encapsulated immutable upload request from batch-upload client. */
446        public static final class BatchUploadClientRequest
447            {
448            /**Exhibit full name; always valid, never null. */
449            public final String exhibitName;
450            /**Exhibit length; strictly positive. */
451            public final long exhibitLength;
452            /**Exhibit content MD5 hash; never null. */
453            public final ROByteArray hashMD5;
454            /**Exhibit description, never null but may be "". */
455            public final String description;
456    
457            public BatchUploadClientRequest(final String name,
458                                            final long length,
459                                            final ROByteArray hash,
460                                            final String desc)
461                {
462                if(!ExhibitName.validNameSyntax(name) ||
463                   (length <= 0) ||
464                   (hash == null) || (hash.length() != 16) ||
465                   (desc == null) || (desc.length() > CoreConsts.DESCRIPTION_MAX_CHARS))
466                    { throw new IllegalArgumentException(); }
467                exhibitName = name;
468                exhibitLength = length;
469                hashMD5 = hash;
470                description = desc;
471                }
472            }
473    
474        /**Decode an upload request from a batch client and return details.
475         * In case of success returns the name/hash/length data,
476         * with the input stream positioned directly before the exhibit data
477         * (which in turn is followed by the message terminator).
478         * <p>
479         * (The timestamp in the returned ExhibitStaticAttr currently contains
480         * no useful data.)
481         * <p>
482         * In case of failure this throws an IOException
483         * and the input stream should be discarded as being in an unknown state.
484         * <p>
485         * A zero length is treated as a request from the client to drop the
486         * connection and we return null in this case.
487         *
488         * @throws IOException  in case of error in input
489         *
490         * @return in case of successfuul decoding, a tuple of the exhibit data
491         *     and the MD5 exhibit content hash
492         */
493        public static BatchUploadClientRequest decodeRequestFromBatchUploadClient(final InputStream is)
494            throws IOException
495            {
496            if(is == null) { throw new IllegalArgumentException(); }
497    
498            // Read all the fields.
499            final DataInputStream dis = new DataInputStream(is);
500            final long exhibitLength = dis.readLong();
501            if(exhibitLength < 0) { throw new IOException("corrupt exhibit length"); }
502            if(exhibitLength == 0)
503                {
504                // Potentially the client asking to drop the connection.
505                if(BATCH_UPLOAD_MSG_TERMINATOR != dis.readByte())
506                    { throw new IOException("corrupt exhibit length or input termination"); }
507    
508                return(null); // EOF...
509                }
510            final String exhibitName = dis.readUTF();
511            // Check name.
512            if((exhibitName.length() != 0) && !ExhibitName.validNameSyntax(exhibitName))
513                {
514                // Corrupt message (invalid name).
515                throw new IOException("invalid exhibit name");
516                }
517    
518            // Get the hash length/content.
519            final int hashLen = dis.readInt();
520            if(hashLen != 16) { throw new IOException("bad MD5 hash length ("+hashLen+"B vs 16B expected)"); }
521            final byte hashBuf[] = new byte[hashLen];
522            dis.readFully(hashBuf);
523            final ROByteArray hash = new ROByteArray(hashBuf);
524    
525            // Read the description.
526            final String description = dis.readUTF();
527            if(description.length() > CoreConsts.DESCRIPTION_MAX_CHARS)
528                {
529                throw new IOException("over-length description");
530                }
531    
532            return(new BatchUploadClientRequest(exhibitName, exhibitLength, hash,  description));
533            }
534        }