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 }