001 /*
002 Copyright (c) 1996-2012, 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
030 package org.hd.d.pg2k.clApp.uploader;
031
032 import java.io.BufferedInputStream;
033 import java.io.ByteArrayOutputStream;
034 import java.io.IOException;
035 import java.io.InputStream;
036 import java.io.InterruptedIOException;
037 import java.io.ObjectInputStream;
038 import java.io.ObjectOutputStream;
039 import java.io.OutputStream;
040 import java.net.HttpURLConnection;
041 import java.net.URL;
042 import java.net.URLConnection;
043 import java.util.ArrayList;
044 import java.util.Arrays;
045 import java.util.Collections;
046 import java.util.Comparator;
047 import java.util.Date;
048 import java.util.HashSet;
049 import java.util.List;
050 import java.util.Set;
051 import java.util.TimerTask;
052 import java.util.concurrent.atomic.AtomicLong;
053 import java.util.zip.GZIPInputStream;
054 import java.util.zip.GZIPOutputStream;
055
056 import javax.jnlp.BasicService;
057 import javax.jnlp.ExtendedService;
058 import javax.jnlp.FileContents;
059 import javax.jnlp.FileOpenService;
060 import javax.jnlp.PersistenceService;
061 import javax.jnlp.ServiceManager;
062 import javax.jnlp.SingleInstanceService;
063 import javax.jnlp.UnavailableServiceException;
064
065 import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
066 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
067 import org.hd.d.pg2k.svrCore.CoreConsts;
068 import org.hd.d.pg2k.svrCore.ExhibitName;
069 import org.hd.d.pg2k.svrCore.ExhibitPropsComputable;
070 import org.hd.d.pg2k.svrCore.ExhibitPropsGlobalImmutable;
071 import org.hd.d.pg2k.svrCore.ExhibitPropsLoadable;
072 import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
073 import org.hd.d.pg2k.svrCore.FileTools;
074 import org.hd.d.pg2k.svrCore.Name;
075 import org.hd.d.pg2k.svrCore.ROByteArray;
076 import org.hd.d.pg2k.svrCore.Rnd;
077 import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
078 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
079 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataHTTPTunnelSource;
080 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource;
081 import org.hd.d.pg2k.svrCore.props.GenProps;
082 import org.hd.d.pg2k.svrCore.uploader.UploadInfoBean;
083 import org.hd.d.pg2k.svrCore.uploader.UploaderConsts;
084 import org.hd.d.pg2k.svrCore.uploader.UploaderUtils;
085
086 import ORG.hd.d.IsDebug;
087
088
089 /**Uploader "business-logic" holder.
090 * Is designed to be GUI-free and just contain the logic.
091 * <p>
092 * Package visible since need be seen only by the main GUI class.
093 */
094 final class UploaderLogic
095 {
096 /**Package-visible constructor since need be seen only by the main GUI class.
097 * @param logger reference to central logger; never null
098 */
099 UploaderLogic(final SimpleLoggerIF logger)
100 {
101 assert(logger != null);
102 this.logger = logger;
103
104 // Get access to basic services.
105 BasicService basicService = null;
106 try { basicService = (BasicService) ServiceManager.lookup("javax.jnlp.BasicService"); }
107 catch(final UnavailableServiceException e) { }
108 bs = basicService;
109
110 // Get access to persistence management.
111 PersistenceService persistenceService = null;
112 try { persistenceService = (PersistenceService) ServiceManager.lookup("javax.jnlp.PersistenceService"); }
113 catch(final UnavailableServiceException e) { }
114 ps = persistenceService;
115
116 // Get access to file open services.
117 FileOpenService fileOpenService = null;
118 try { fileOpenService = (FileOpenService) ServiceManager.lookup("javax.jnlp.FileOpenService"); }
119 catch(final UnavailableServiceException e) { }
120 fos = fileOpenService;
121
122 // Get access to single-instance services.
123 SingleInstanceService singleInstanceService = null;
124 try { singleInstanceService = (SingleInstanceService) ServiceManager.lookup("javax.jnlp.SingleInstanceService"); }
125 catch(final UnavailableServiceException e) { }
126 sis = singleInstanceService;
127
128 // Get access to extended services.
129 ExtendedService extendedService = null;
130 try { extendedService = (ExtendedService) ServiceManager.lookup("javax.jnlp.ExtendedService"); }
131 catch(final UnavailableServiceException e) { }
132 exs = extendedService;
133
134 if((ps != null) && (bs != null))
135 {
136 // Try to reload the properties/preferences...
137 // This should be quick,
138 // and doing it here should avoid races with the UI.
139 try
140 {
141 final FileContents fc = ps.get(makePropsURL(bs));
142 final InputStream inputStream = fc.getInputStream();
143 try { props.loadFromPersistentData(inputStream); }
144 finally { inputStream.close(); }
145 }
146 catch(final IOException e)
147 {
148 logger.log("No saved properties/preferences to reload (yet).");
149 }
150 }
151 else
152 {
153 // If there is no persistence,
154 // then create a default AEP,
155 // for testing, and to get users started.
156 final Set<ExhibitStaticAttr> esas = new HashSet<ExhibitStaticAttr>();
157 esas.add(new ExhibitStaticAttr(DEFAULT_EXHIBIT_NAME, 1, System.currentTimeMillis()));
158 final AllExhibitProperties defaultAEP =
159 new AllExhibitProperties(new ExhibitPropsGlobalImmutable(),
160 new AllExhibitImmutableData(esas, 1),
161 Collections.<Name.ExhibitFull, ExhibitPropsLoadable>emptyMap(),
162 Collections.<Name.ExhibitFull, ExhibitPropsComputable>emptyMap());
163
164 aep = defaultAEP;
165 logger.log("Created default AEP.");
166
167 // Set ID to match for now.
168 props.setUserID(ExhibitName.getAuthorComponent(DEFAULT_EXHIBIT_NAME).toString());
169 }
170
171 // Create our tunnel endpoint if at least basic service is available.
172 // Create the tunnel before any async threads are running.
173 URL masterURL = null;
174 if(bs != null)
175 {
176 try { masterURL = new URL(bs.getCodeBase(), CoreConsts.TUNNEL_URI); }
177 catch(final Exception e) { }
178 }
179 tunnel = ((bs == null) || (masterURL == null)) ? null :
180 new ExhibitDataHTTPTunnelSource(masterURL.toString(), "", logger);
181
182 // Create our worker threads for uploads.
183 // They get start()ed after the constructor has completed, by startup().
184 final List<Thread> workers = new ArrayList<Thread>(MAX_CONC_UPLOADS);
185 for(int i = 0; i < MAX_CONC_UPLOADS; ++i)
186 { workers.add(new UploaderWorkerThread(i)); }
187 // Store immutable List view of unstarted uploader threads.
188 uploaderWorkerThreads = Collections.unmodifiableList(workers);
189 }
190
191 /**Reference to central logger; never null.
192 * We make this package-visible,
193 * though the preferred access is via the Main class.
194 */
195 final SimpleLoggerIF logger;
196
197 /**Persistence properties; never null.
198 * Package-visible so as to be directly usable by GUI classes.
199 */
200 final UploaderProps props = new UploaderProps();
201
202
203 /**Handle on JWS basic service; null if none.
204 * Package-visible so as to be directly usable by GUI classes.
205 */
206 final BasicService bs;
207
208 /**Handle on JWS persistence service; null if none.
209 * Package-visible so as to be directly usable by GUI classes.
210 */
211 final PersistenceService ps;
212
213 /**Handle on JWS file-open service; null if none.
214 * Package-visible so as to be directly usable by GUI classes.
215 */
216 final FileOpenService fos;
217
218 /**Handle on JWS singleton service; null if none.
219 * Package-visible so as to be directly usable by GUI classes.
220 */
221 final SingleInstanceService sis;
222
223 /**Handle on JWS extended service; null if none.
224 * Package-visible so as to be directly usable by GUI classes.
225 */
226 final ExtendedService exs;
227
228
229 /**Our end of main tunnel to server; null if no tunnel possible.
230 * Package-visible so as to be directly usable by GUI classes.
231 */
232 final ExhibitDataHTTPTunnelSource tunnel;
233
234
235 /**Uploader worker thread.
236 * Private to this class.
237 */
238 private final class UploaderWorkerThread extends Thread
239 {
240 UploaderWorkerThread(final int index)
241 {
242 super("upload worker thread " + index);
243
244 if((index < 0) || (index > MAX_CONC_UPLOADS))
245 { throw new IllegalArgumentException(); }
246
247 setDaemon(true); // Daemon by default.
248
249 this.index = index;
250 }
251
252 /**Index of this worker thread. */
253 final int index;
254
255 /**Time an idle worker sleeps for in ms; strictly positive. */
256 private static final int WORKER_SLEEP_MS = 1011;
257
258
259 /**Do any uploading; sleep when nothing to do.
260 */
261 @Override
262 public void run()
263 {
264 // Run forever...
265 for( ; ; )
266 {
267 // At the start of each loop our slot must be empty.
268 assert(inProgressSlots[index] == null);
269
270 try
271 {
272 // Short-cut when no work to be done...
273 // If nothing at all to upload
274 // or no basic service
275 // or invalid user credentials
276 // or broken connection to the server
277 // then sleep awhile...
278 if((uploadingFiles.size() == 0) ||
279 (bs == null) ||
280 (!ExhibitName.validAuthorSyntax(userID)) ||
281 (tunnel.isBroken()))
282 {
283 // Sleep then go back and check again.
284 // Use a randomised time to help avoid collisions.
285 Thread.sleep(WORKER_SLEEP_MS + Rnd.fastRnd.nextInt(WORKER_SLEEP_MS));
286 continue;
287 }
288
289 // Find the first available item in the upload queue
290 // with no error,
291 // and not in any other upload slot (ie not being uploaded).
292 //
293 // Use a lock on inProgressSlots[]
294 // to avoid selection races with more than one worker
295 // starting to upload the same file.
296 //
297 // Grab the uploadingFiles lock inside it.
298 AtomicLong bytesUploaded = null;
299 UploadStatus stats = null;
300 SelectedFileDetails selectedFileDetails = null;
301 synchronized(inProgressSlots)
302 {
303 synchronized(uploadingFiles)
304 {
305 selectLoop: for(int i = uploadingFiles.size(); --i >= 0; )
306 {
307 selectedFileDetails = uploadingFiles.get(i);
308
309 // This file is not a candidate
310 // if it has any error or other non-null status.
311 if(selectedFileDetails.getStatus(UploaderLogic.this) != null)
312 { continue; }
313
314 // This file is not a candidate
315 // if any (other) worker is already uploading it.
316 for(int j = MAX_CONC_UPLOADS; --j >= 0; )
317 {
318 if(selectedFileDetails.equals(inProgressSlots[j]))
319 { continue selectLoop; }
320 }
321
322 // OK, mark this as uploading
323 // and copy it to our slot.
324 selectedFileDetails.setStatus("Uploading... (worker "+index+")");
325 bytesUploaded = new AtomicLong(-1); // Not started data yet...
326 inProgressSlots[index] = stats = new UploadStatus(selectedFileDetails, bytesUploaded);
327 break;
328 }
329 }
330 }
331
332 // If we didn't find anything to do, then sleep.
333 // This might happen in a race for work with other threads.
334 if(stats == null)
335 {
336 // Use a randomised time to help avoid collisions.
337 Thread.sleep(WORKER_SLEEP_MS + Rnd.fastRnd.nextInt(WORKER_SLEEP_MS));
338 continue;
339 }
340
341 // We expect all the refs to be filled in (non-null).
342 assert(selectedFileDetails != null);
343 assert(bytesUploaded != null);
344
345 // Note time before starting this upload.
346 final long startTime = System.currentTimeMillis();
347
348 URLConnection uc = null; // openURLConnection;
349 HttpURLConnection huc = null;
350 try
351 {
352 // Compute path at which batch-upload JSP is mounted...
353 final URL u = (new URL(bs.getCodeBase(), "/_upload/batchupload"));
354 uc = u.openConnection();
355
356 if(!(uc instanceof HttpURLConnection))
357 { throw new IOException("incorrect connection type from URL: " + u); }
358 huc = (HttpURLConnection) uc;
359
360 // Set properties of connection.
361 uc.setDoInput(true);
362 uc.setDoOutput(true);
363 uc.setUseCaches(false);
364 // Wait only a short while for a connection.
365 // If we can't even get a connection quickly
366 // then we might well have problems with uploads anyway.
367 uc.setConnectTimeout(2001 + (UploaderConsts.BATCH_UPLOAD_MAX_IDLE_MS / 2));
368 // Make read timeout MUCH longer than others
369 // so as to try hard not to lose uploads in progress.
370 uc.setReadTimeout(12001 + (UploaderConsts.BATCH_UPLOAD_MAX_IDLE_MS * 2));
371 // Don't bother the user for any interaction.
372 uc.setAllowUserInteraction(false);
373
374 // Set auth credentials...
375 uc.setRequestProperty("X-" + UploaderConsts.USERID_FIELD_NAME, String.valueOf(userID));
376 uc.setRequestProperty("X-" + UploaderConsts.PASSWD_FIELD_NAME, String.valueOf(passwd));
377
378 // We know what the content length will be
379 // so set it here to allow some HTTP optimisations.
380 final int contentLength = UploaderUtils.computeLengthOfRequestFromBatchUploadClient(
381 selectedFileDetails.getEsa(),
382 selectedFileDetails.getHashMD5(),
383 selectedFileDetails.getDescription());
384 huc.setRequestProperty("Content-Length", String.valueOf(contentLength));
385 huc.setFixedLengthStreamingMode(contentLength);
386
387 // This must be a POST request.
388 huc.setRequestMethod("POST");
389
390 // No redirects should be needed.
391 huc.setInstanceFollowRedirects(false);
392
393 // Force connection now...
394 uc.connect();
395 }
396 catch(final Exception e)
397 {
398 e.printStackTrace();
399 logger.log("CANNOT CONNECT TO SERVER: " + e.getMessage());
400
401 // Check for a 403 (forbidden) error code.
402 if(huc != null)
403 {
404 if(huc.getResponseCode() == 403)
405 {
406 logger.log("CANNOT CONNECT TO SERVER: uploads disabled, or user name or password wrong?: " + e.getMessage());
407 }
408 }
409
410 // Whinge but don't penalise the file itself
411 // if we could not even get a server connection.
412 inProgressSlots[index] = null;
413 selectedFileDetails.setStatus(null); // Not the file's problem...
414 // Use a randomised time to help avoid collisions.
415 // A longer-than-usual time for the server to recover.
416 Thread.sleep(3 * WORKER_SLEEP_MS + Rnd.fastRnd.nextInt(5 * WORKER_SLEEP_MS));
417 continue;
418 }
419
420 IOException writeIOException = null;
421
422 // Send request and data up the wire...
423 final OutputStream os = uc.getOutputStream();
424 try
425 {
426 final InputStream exhibitDataStream =
427 new BufferedInputStream(selectedFileDetails.getFc().getInputStream());
428 try
429 {
430 // Not within any locks
431 // (to allow good concurrency),
432 // upload the new exhibit...
433 UploaderUtils.sendRequestFromBatchUploadClient(
434 os,
435 selectedFileDetails.getEsa(),
436 selectedFileDetails.getHashMD5(),
437 selectedFileDetails.getDescription(),
438 exhibitDataStream,
439 bytesUploaded);
440 os.flush(); // Force all data upstream.
441 }
442 finally { exhibitDataStream.close(); /* Release resources. */ }
443 }
444 catch(final IOException e)
445 {
446 // Note error in upload...
447 selectedFileDetails.setStatus("ERROR: " + e.getMessage());
448 writeIOException = e;
449 }
450 finally { os.close(); }
451
452 final InputStream is = uc.getInputStream();
453 try
454 {
455 // Collect OK/FAIL response from server...
456 if(!selectedFileDetails.getEsa().getFilePath().equals(UploaderUtils.decodeResponseToBatchUploadClient(is)))
457 { throw new IOException("got invalid response to upload from server"); }
458 }
459 finally { is.close(); }
460
461 // If we got an exception sending then rethrow it now.
462 if(writeIOException != null) { throw writeIOException; }
463
464 // If OK then remove the file entirely
465 // (though record the hash to avoid uploading a dup later).
466 selectedFileDetails.setStatus(null); // Done!
467 uploadedHashesMD5.add(selectedFileDetails.getHashMD5()); // Avoid dup upload.
468 uploadingFiles.remove(selectedFileDetails); // Uploaded!
469
470 // Clear slot...
471 inProgressSlots[index] = null;
472
473 // Record finish time including most of the overheads
474 // to give a conservative Bps throughput measure.
475 final long endTime = System.currentTimeMillis();
476 final long durationMS = endTime - startTime;
477
478 final long length = selectedFileDetails.getEsa().length;
479 final long Bps = (1000*length + 500) / Math.max(1, durationMS);
480 updateSmoothedUploadSpeedBps(Bps);
481
482 logger.log("Finished uploading exhibit #"+getCompletedUploadsThisSession()+" at "+Bps+"Bps: "+length+" bytes in "+durationMS+"ms.");
483
484 // If no files left to upload then we could disconnect...
485 if((uploadingFiles.size() == 0) && (huc != null))
486 { huc.disconnect(); }
487 }
488 catch(final Throwable e)
489 {
490 // Log the problem.
491 e.printStackTrace();
492 logger.log("UPLOAD ERROR: " + e.getMessage());
493
494 // Mark any in-progress item as in error.
495 final UploadStatus inProgress = inProgressSlots[index];
496 if(inProgress != null)
497 { inProgress.file.setStatus("ERROR: " + e.getMessage()); }
498
499 // Kill the content of our slot.
500 inProgressSlots[index] = null;
501
502 // Sleep a bit in case the error condition keeps occurring.
503 try { Thread.sleep(2*WORKER_SLEEP_MS + Rnd.fastRnd.nextInt(WORKER_SLEEP_MS)); }
504 catch(final InterruptedException ignored) { }
505 }
506 }
507 }
508 }
509
510
511 /**Time-constant for updating smoothedUploadSpeedBps; strictly positive.
512 * The larger this is, the more smoothed is the computed value.
513 * <p>
514 * A value of 1 gives no smoothing at all.
515 * <p>
516 * A power of two may result in very slightly faster computations.
517 * <p>
518 * A value in the range 2 to 64 is probably good.
519 */
520 private static final int TC_SUSB = 16;
521
522 /**Current smoothed upload performance in bytes-per-second, initially -1; never null.
523 * Initially -1 before any exhibit is uploaded.
524 * Thereafter updated via a smoothing (low-pass-filter) algorithm
525 * to reflect recent history.
526 * <p>
527 * This represents a conservative performance measure,
528 * ie including overheads such as connection set-up time,
529 * but not dead time between uploads.
530 * <p>
531 * AtomicLong allows thread-safe lockless access for read and write.
532 */
533 private final AtomicLong smoothedUploadSpeedBps = new AtomicLong(-1);
534
535 /**Get current smoothed upload performance in bytes-per-second, initially -1.
536 * Initially -1 before any exhibit is uploaded.
537 * Thereafter updated via a smoothing (low-pass-filter) algorithm
538 * to reflect recent history.
539 * <p>
540 * This represents a conservative performance measure,
541 * ie including overheads such as connection set-up time,
542 * but not dead time between uploads.
543 * <p>
544 * This is thread-safe and lock-free.
545 * <p>
546 * Package-visible to allow access by GUI classes.
547 */
548 final long getSmoothedUploadSpeedBps()
549 { return(smoothedUploadSpeedBps.get()); }
550
551 /**Update our notion of mean upload speed (in bytes per second).
552 * The argument must be non-negative,
553 * and should be conservative,
554 * ie including overheads such as connection set-up time,
555 * but not dead time between uploads.
556 * <p>
557 * This will be used to update a smoothed performance measure.
558 * <p>
559 * This tries to allow for concurrent uploads,
560 * ie where the total throughput is shared by several workers.
561 * <p>
562 * This is thread-safe and lock-free.
563 *
564 * @param Bps non-negative speed of upload in bytes per second
565 */
566 private void updateSmoothedUploadSpeedBps(long Bps)
567 {
568 if(Bps < 0) { throw new IllegalArgumentException(); }
569
570 // We may adjust Bps to allow for multiple concurrent workers.
571 final int filesStillQueuedToUpload = uploadingFiles.size();
572 if((MAX_CONC_UPLOADS > 1) && (filesStillQueuedToUpload > 0))
573 {
574 // We probably just finished uploading a file,
575 // and were probably uploading up to the maximum concurrency
576 // that that previous queue length implies.
577 Bps *= Math.max(1, Math.min(MAX_CONC_UPLOADS, filesStillQueuedToUpload+1));
578 }
579
580 // Retry until we manage to set the value...
581 for( ; ; )
582 {
583 final long current = smoothedUploadSpeedBps.get();
584 // Compute a smoothed updated value,
585 // though in the first instance (value is -1)
586 // set the value to the one passed in with no smoothing.
587 final long updated = (current < 0) ? Bps :
588 ((current + (Bps * (TC_SUSB-1))) / TC_SUSB);
589 if(smoothedUploadSpeedBps.compareAndSet(current, updated))
590 {
591 return; /* Update succeeded, so return! */
592 }
593 }
594 }
595
596
597 /**Set of MD5 hashes of exhibits already uploaded (or rejected as duplicate content); never null.
598 * This information currently does not persist from one run to the next,
599 * so can be cleared by restarting the app.
600 * <p>
601 * Private so as to prevent accidental deletion of entries!
602 * <p>
603 * Thread-safe.
604 */
605 private final Set<ROByteArray> uploadedHashesMD5 =
606 Collections.synchronizedSet(new HashSet<ROByteArray>());
607
608 /**Check (using the content MD5 hash) if a putative exhibit has already been uploaded (or rejected as duplicate content).
609 * Package-visible so as to be directly usable by GUI classes.
610 */
611 boolean checkIfAlreadyUploadedByHashMD5(final ROByteArray hashMD5)
612 {
613 if((hashMD5 == null) || (hashMD5.length() != 16))
614 { throw new IllegalArgumentException(); }
615 return(uploadedHashesMD5.contains(hashMD5));
616 }
617
618 /**Get number of exhibits successfully uploaded this session; non-negative. */
619 int getCompletedUploadsThisSession()
620 { return(uploadedHashesMD5.size()); }
621
622
623 /**Details of one selected file being uploaded; immutable except for progress value. */
624 static final class UploadStatus
625 {
626 /**Selected file; never null. */
627 final SelectedFileDetails file;
628
629 /**Current upload progress (bytes uploaded); never null.
630 * Private so that only get/read access is possible.
631 */
632 private final AtomicLong progress;
633
634 /**Get bytes of this exhibit uploaded so far.
635 * Thread-safe.
636 *
637 * @return -1 if upload of data not yet started,
638 * Long.MAX_VALUE once all data has been sent,
639 * else the number of bytes sent/uploaded
640 */
641 long getBytesUploaded() { return(progress.get()); }
642
643 /**Create an instance.
644 * All args must be non-null and unique to this instance.
645 */
646 UploadStatus(final SelectedFileDetails selectedFile,
647 final AtomicLong bytesUploaded)
648 {
649 if((selectedFile == null) || (bytesUploaded == null))
650 { throw new IllegalArgumentException(); }
651 file = selectedFile;
652 progress = bytesUploaded;
653 }
654 }
655
656
657 /**Maximum simultaneous uploads (to overcome network latency, etc); strictly positive.
658 * A value of much higher than 2 is more likely to cause network congestion
659 * than actually fix anything, so a value of 1 or 2 is probably optimal.
660 */
661 static final int MAX_CONC_UPLOADS = 2;
662
663 /**Our (daemon) uploader worker threads started by startup(); never null. */
664 private final List<Thread> uploaderWorkerThreads;
665
666 /**Our current uploads-in-progress; never null and always MAX_CONC_UPLOADS long.
667 * Each uploader worker thread controls the content of the slot/index
668 * indicated by its own index.
669 * <p>
670 * A slot is empty when not in use; all slots are initially empty.
671 * <p>
672 * Private so as to prevent accidental deletion of entries!
673 * <p>
674 * All access to the contents of this array should grab a lock
675 * on this array to ensure that an up-to-date/valid view is seen.
676 * <p>
677 * Worker threads keep a lock on this while looking for a
678 * file to start uploading,
679 * and thus the uploadingFiles instance lock may be grabbed inside this one;
680 * the reverse is not allowed so as to avoid deadlocks.
681 */
682 private final UploadStatus inProgressSlots[] = new UploadStatus[MAX_CONC_UPLOADS];
683
684 /**Get the current contents of an in-progress slot; may return null.
685 * Package-visible so as to be directly usable by GUI classes.
686 */
687 UploadStatus getInProgressSlot(final int index)
688 { synchronized(inProgressSlots) { return(inProgressSlots[index]); } }
689
690
691 /**Name under the codebase where we persist the properties/preferences. */
692 private static final String FNAME_MAIN_PROPS = "main.properties";
693
694 /**Name under the codebase where we persist the AEP. */
695 private static final String FNAME_AEP = "aep.ser.gz";
696
697
698 /**Database of selected files; never null.
699 * Package-visible for direct manipulation by GUI classes.
700 * <p>
701 * Thread-safe.
702 */
703 final SelectedFilesDB selectedFiles = new SelectedFilesDB();
704
705 /**Database of files queued for upload; never null.
706 * Package-visible for direct manipulation by GUI classes.
707 * <p>
708 * Thread-safe.
709 */
710 final SelectedFilesDB uploadingFiles = new SelectedFilesDB();
711
712
713 /**Last time the user was active in this run, eg in the UI; initially zero.
714 * Volatile to allow access without a lock.
715 */
716 private volatile long userLastActive;
717
718 /**Get the last time the user was active in this run, eg in the UI; initially zero.
719 * No harm in letting everyone see this, so is public.
720 * <p>
721 * When the user has not been active for a long time,
722 * some activities, such as polling the server, may halt or slow down
723 * to conserve resources.
724 */
725 public long getUserLastActive() { return(userLastActive); }
726
727 /**Note user as active.
728 * Made package-visible (ie not public) for security.
729 * <p>
730 * This may be triggered/called by a number of events
731 * that indicate that the user is active.
732 * <p>
733 * When the user has not been active for a long time,
734 * some activities, such as polling the server, may halt or slow down
735 * to conserve resources.
736 */
737 void setUserLastActive(final String doingWhat)
738 {
739 userLastActive = System.currentTimeMillis();
740 //logger.log("UI active: " + doingWhat + ": " + (new Date())); }
741 }
742
743 /**Default exhibit name when there is an empty AEP. */
744 final String DEFAULT_EXHIBIT_NAME = "places-and-sights/example-ANON.jpg";
745
746 /**Load any (large) persisted data from a previous execution and start a background worker thread.
747 * Do this as early as possible after construction,
748 * and preferably before allowing (much) user interaction.
749 * <p>
750 * We spin this off into a separate thread to avoid blocking startup.
751 * <p>
752 * This should be called at most once.
753 * <p>
754 * Package-visible so as to be directly usable by GUI classes.
755 */
756 void startup()
757 {
758 final Thread th = new Thread("UploaderLogic startup()"){
759 /**Do load work as background thread. */
760 @Override
761 public final void run()
762 {
763 if((ps != null) && (bs != null))
764 {
765 // Try to reload the AEP...
766 // This may take a while.
767 logger.log("Reloading saved AEP...");
768 try
769 {
770 final FileContents fc = ps.get(makeAEPURL(bs));
771 final ObjectInputStream ois = new ObjectInputStream(new GZIPInputStream(fc.getInputStream()));
772 try
773 {
774 final AllExhibitProperties allExhibitProperties = (AllExhibitProperties) ois.readObject();
775 if(null != allExhibitProperties)
776 {
777 aep = allExhibitProperties;
778 logger.log("Reloaded saved AEP.");
779 }
780 }
781 finally { ois.close(); }
782 }
783 catch(final Exception e)
784 {
785 logger.log("No valid saved AEP to reload.");
786 }
787 }
788
789 // Once we have finished trying to load any saved AEP,
790 // start a (daemon) poller thread for non-UI async activity
791 // such as fetching any updated AEP over the tunnel.
792 // After a short delay, run approximately every second or so.
793 // This is created and started AFTER the UI object construction
794 // is complete to avoid any unpleasant races.
795 final java.util.Timer timer = new java.util.Timer(true);
796 timer.schedule(new TimerTask(){
797 /**The action to be performed by this timerUI task. */
798 @Override
799 public final void run() { poll(); }
800 }, 3000, 2000 + Rnd.fastRnd.nextInt(1000));
801
802 // Finally start our uploader worker threads...
803 for(final Thread t : uploaderWorkerThreads)
804 { t.start(); }
805 }
806 };
807 th.start();
808 }
809
810 /**Make the props persistence URL; only viable when in JWS/JNLP. */
811 private static URL makePropsURL(final BasicService bs) throws IOException
812 {
813 final URL codebase = bs.getCodeBase();
814 final URL urlProps = new URL(codebase, FNAME_MAIN_PROPS);
815 return(urlProps);
816 }
817
818 /**Make the AEP persistence URL; only viable when in JWS/JNLP. */
819 private static URL makeAEPURL(final BasicService bs) throws IOException
820 {
821 final URL codebase = bs.getCodeBase();
822 final URL urlAEP = new URL(codebase, FNAME_AEP);
823 return(urlAEP);
824 }
825
826 /**Our private copy of the (less sensitive) valid user ID, or null if none or invalid ID/pass presented. */
827 private volatile String userID;
828
829 /**Our private note of the password supplied. */
830 private volatile String passwd;
831
832 /**Set whenever the userID changes; cleared when we have attempted to test the connection.
833 * This is <em>not</em> critical, and may suffer races.
834 * <p>
835 * This is simply a device to try to get a new user ID/passwd tested
836 * quietly in the background.
837 */
838 private volatile boolean userIDChanged;
839
840 /**Set authentication information for this user.
841 * To set authentication arguments must be non-null and non-empty,
842 * else all arguments must be null to clear authentication data.
843 * <p>
844 * Package-visible so as to be directly usable by the main GUI class.
845 * <p>
846 * Ignored if there is no tunnel.
847 */
848 void setAuthenticationInfo(final String userID, final char[] passwd)
849 {
850 if(tunnel == null)
851 { return; }
852
853 // Any null/empty argument implies unsetting the authentication data.
854 if((userID == null) || (userID.length() == 0) ||
855 (passwd == null) || (passwd.length == 0))
856 {
857 this.userID = null;
858 this.passwd = null;
859 tunnel.setAuthenticationInfo(null, null);
860 return;
861 }
862
863 // If the arguments passed are syntactically invalid,
864 // then ignore them and clear any extant authentication data instead.
865 if(!ExhibitName.validAuthorSyntax(userID) ||
866 (passwd.length < CoreConsts.MIN_PASSWORD_LEN) ||
867 (passwd.length > CoreConsts.MAX_PASSWORD_LEN))
868 {
869 this.userID = null;
870 this.passwd = null;
871 tunnel.setAuthenticationInfo(null, null);
872 return;
873 }
874
875 // The authentication data provided is syntactically OK.
876 // Set it for the tunnel.
877 // Also make it available for the upload worker threads.
878 final String s = new String(passwd);
879 final boolean changed =
880 !userID.equals(this.userID) ||
881 !this.passwd.equals(s);
882 this.userID = userID;
883 this.passwd = s;
884 tunnel.setAuthenticationInfo(userID, passwd);
885
886 if(changed)
887 {
888 // We take an ID change to be indicative of user activity.
889 setUserLastActive("user ID changed");
890
891 // Set this flag *after* setting the value on the tunnel, etc.
892 userIDChanged = true;
893 }
894 }
895
896 /**Exhibit properties; never null.
897 * Set by poll().
898 * <p>
899 * Volatile so as to be safe to access without a lock if need be.
900 */
901 private volatile AllExhibitProperties aep = new AllExhibitProperties();
902
903 /**Get the current AEP (exhibit properties); never null. */
904 AllExhibitProperties getAep() { return(aep); }
905
906 /**Generic properties; never null.
907 * Set and used by poll().
908 * <p>
909 * Volatile so as to be safe to access without a lock if need be.
910 */
911 private volatile GenProps gp = new GenProps();
912
913 /**Get the current set of GenProps (generic system properties); never null. */
914 GenProps getGenProps() { return(gp); }
915
916 /**Note of polled connection state; TRUE is good, FALSE is bad, null is unknown.
917 * Set and used by poll().
918 * <p>
919 * Volatile so as to be safe to access without a lock if need be.
920 */
921 private volatile Boolean connectionGood;
922
923 /**Time we last polled for GP/AEP updates.
924 * Set and used by poll() and used to put off another poll,
925 * so should always be set after a successful poll,
926 * but may also be set after some unsuccessful polls to reduce traffic to an unhappy master.
927 * <p>
928 * Volatile so as to be safe to access without a lock if need be.
929 */
930 private volatile long lastAEPUpdatePoll;
931
932 /**Maximum interval between polls for new values of AEP (and GP); strictly positive.
933 * Relatively short for this interactive application.
934 * <p>
935 * Short enough to capture an AEP generated asynchronously for us
936 * at the server side of the HTTP tunnel.
937 * <p>
938 * We may suspend polling entirely if the UI/app appears to be idle.
939 */
940 private static final int MAX_AEP_POLL_MS = Math.min(10 * 60 * 1000, ExhibitDataTunnelSource.MIN_AEP_RETENTION_MS/2);
941
942 /**Called (by a daemon thread) to perform async activity.
943 * This is not Swing-safe, and does not block Swing if blocked.
944 * <p>
945 * This is not called until construction is complete.
946 * <p>
947 * Package-visible so as to be directly usable by the main GUI class.
948 */
949 void poll()
950 {
951 try
952 {
953 // Perform tunnel processing, if any.
954 final ExhibitDataHTTPTunnelSource t = tunnel;
955 if((t != null) && (userID != null))
956 {
957 try
958 {
959 // Perform background activity for the tunnel, if extant.
960 t.poll(gp);
961
962 // Try a new/updated user/password combo in the background.
963 // Force recheck if state is not known to be good.
964 if((userID != null) &&
965 (userIDChanged || !Boolean.TRUE.equals(connectionGood)))
966 {
967 // Don't try again unless ID/passwd is changed again...
968 userIDChanged = false;
969
970 try
971 {
972 // Try a real connection if we're not seeing errors.
973 t.doNOOP(false);
974
975 // Poll was successful.
976 connectionGood = Boolean.TRUE;
977 logger.log("Connection to server established.");
978 }
979 catch(final IOException e)
980 {
981 connectionGood = Boolean.FALSE;
982 logger.log("Connection to server bad/unauthorised: " + e.getMessage());
983 }
984 }
985
986 // If we have a good connection
987 // and we have no AEP (or GP)
988 // or it is a while since we last polled for them
989 // then try to fetch updates/changes to them.
990 // We use the same short-ish refetch interval
991 // for both GenProps and AEP
992 // for this very interactive app.
993 //
994 // We don't do this if the user hasn't been active
995 // since the last poll.
996 // This conserves resources and enhances security.
997 if(Boolean.TRUE.equals(connectionGood) &&
998 (getUserLastActive() > lastAEPUpdatePoll) &&
999 ((aep.aeid.length == 0) || (gp.timestamp == 0) || // Missing vital data...
1000 (System.currentTimeMillis() > lastAEPUpdatePoll +
1001 Math.min(MAX_AEP_POLL_MS, gp.getWEBSVR_MIN_EX_IMATTR_RECHECK_MS()))))
1002 {
1003 logger.log("PLEASE WAIT: fetching generic system properties from server...");
1004 final GenProps gpNew = t.getGenProps(gp.timestamp);
1005 if(gpNew != null)
1006 {
1007 gp = gpNew;
1008 logger.log("Updated GenProps as of: " + new Date(gpNew.timestamp) + ", exhibit max size: " + gpNew.getWEBSVR_MAX_EX_BYTES());
1009 assert(gpNew.timestamp != 0);
1010 }
1011
1012 logger.log("PLEASE WAIT: fetching AllExhibitProperties from server... (may take several of your Earth minutes)");
1013 final long fetchStart = System.currentTimeMillis();
1014 final AllExhibitProperties aepNew = t.getAllExhibitProperties(aep, true);
1015 final long fetchEnd = System.currentTimeMillis();
1016 logger.log("Finished fetching AEP... ("+((fetchEnd-fetchStart+500)/1000)+"s)");
1017 lastAEPUpdatePoll = System.currentTimeMillis(); // Postpone next poll...
1018 if(aepNew != null)
1019 {
1020 // Reject all-zeroes AEP as probably master in process of reloading.
1021 if(aepNew.aeid.length == 0)
1022 { throw new IOException("Ignoring empty AEP from server: assumed transient during start-up"); }
1023
1024 // Save new AEP *before* we try to cache it,
1025 // since cacheing may fail due to JNLP restrictions.
1026 aep = aepNew;
1027
1028 // Now attempt to cache the AEP locally.
1029 logger.log("Cacheing AEP locally...");
1030 try
1031 {
1032 cacheAEP(aepNew);
1033 logger.log("Cached AEP successfully.");
1034 }
1035 catch(final Exception e)
1036 {
1037 logger.log("Failed to cache AEP locally: " + e.getMessage());
1038 }
1039 }
1040 }
1041 }
1042 catch(final InterruptedIOException e)
1043 {
1044 // The status remains unchanged because
1045 // InterruptedIOException is an invitation to retry soon.
1046 logger.log("RPC interrupted, will retry: " + e.getMessage());
1047 }
1048 catch(final IOException e)
1049 {
1050 // This status is unknown rather than bad,
1051 // as other things than a bad authorisation
1052 // such as a temporary server overload
1053 // may cause an IOException.
1054 connectionGood = null;
1055 logger.log("Connection to server status unknown: " + e.getMessage());
1056 //e.printStackTrace();
1057 }
1058 }
1059
1060 // Save any internal state if it has changed...
1061 final byte[] persistentDataIfChanged = props.getPersistentDataIfChanged();
1062 if(null != persistentDataIfChanged)
1063 {
1064 if(IsDebug.isDebug) { logger.log("Properties data to save (bytes): " + persistentDataIfChanged.length); }
1065 saveProperties();
1066 }
1067 }
1068 catch(final Exception e)
1069 {
1070 // Absorb any exceptions to avoid killing the timer.
1071 logger.log("Unexpected exception: " + e.getMessage());
1072 e.printStackTrace();
1073 }
1074 }
1075
1076 /**Number of bytes of expansion room we allow for one growth in the AEP; non-negative.
1077 * We can do this so that we do not ask the JNLP/user too often
1078 * for expanded storage space.
1079 * <p>
1080 * Should typically allow for a few days' growth as new exhibits are added.
1081 */
1082 private static final int AEP_EXPANSION_BYTES = 30001;
1083
1084 /**Save/cache local copy of AEP in persistent store.
1085 * This is only possible if the PersistenceService is available,
1086 * else this call is ignored.
1087 * <p>
1088 * This save may be prevented on grounds of size, etc, by the JNLP runtime.
1089 * <p>
1090 * This should NOT be called by more than one thread at once.
1091 *
1092 * @param aepToSave AEP to save; usually not null
1093 */
1094 private void cacheAEP(final AllExhibitProperties aepToSave)
1095 throws IOException
1096 {
1097 // Do nothing if not running in JWS/JNLP environment.
1098 if((bs == null) || (ps == null)) { return; }
1099
1100 final URL aepURL = makeAEPURL(bs);
1101
1102 // Start with a reasonable size buffer to write into...
1103 int initialBufSize = 65536;
1104 // Try to guess a new larger buffer size from any existing store size...
1105 try { initialBufSize = Math.max(initialBufSize, (int) (AEP_EXPANSION_BYTES + ps.get(aepURL).getLength())); }
1106 catch(final IOException e) { /* Ignore missing store error. */ }
1107
1108 final ByteArrayOutputStream baos = new ByteArrayOutputStream(initialBufSize);
1109 final ObjectOutputStream ois = new ObjectOutputStream(new GZIPOutputStream(baos));
1110 ois.writeObject(aepToSave);
1111 ois.close();
1112
1113 // OK, how big is the compressed serialised AEP?
1114 final int len = baos.size();
1115 if(IsDebug.isDebug) { logger.log("AEP save size (bytes) is: " + len); }
1116
1117 // Create entry for AEP if not yet extant.
1118 try { ps.create(aepURL, len + AEP_EXPANSION_BYTES); /* Enough for current AEP plus a margin for growth... */ }
1119 catch(final IOException e) { } // Ignore errors.
1120
1121 // Now try to actually persist the data
1122 // by making sure that the entry has enough space granted
1123 // and then overwriting the entry.
1124 final FileContents fc = ps.get(aepURL);
1125 final long available = fc.getMaxLength();
1126 if(available < len)
1127 {
1128 // Currently not enough space available for current AEP...
1129 // so ask for more, in fact lots more than we currently need
1130 // to allow room for expansion without pestering the user too much.
1131 final long granted = fc.setMaxLength(AEP_EXPANSION_BYTES + 2 * len);
1132 if(len > granted)
1133 { throw new IOException("could not get enough space to cache AEP ("+granted+" vs "+len+")"); }
1134 }
1135 final OutputStream os = fc.getOutputStream(true);
1136 try { baos.writeTo(os); }
1137 finally { os.close(); }
1138 }
1139
1140 /**Returns true if this (short) exhibit name is valid and unique.
1141 * The name has to be syntactically valid,
1142 * and the short name (and thus the whole name that contains it)
1143 * not currently present in:
1144 * </ul>
1145 * <li>The Gallery exhibit set (the main AEP).
1146 * <li>The uploading-files list.
1147 * <li>The selected-files list.
1148 * </ul>
1149 */
1150 boolean shortNameValidAndUnique(final String newShortName)
1151 {
1152 if(!ExhibitName.validNameFinalComponentSyntax(newShortName)) { return(false); }
1153 if(selectedFiles.shortNameInUse(newShortName)) { return(false); }
1154 if(uploadingFiles.shortNameInUse(newShortName)) { return(false); }
1155 if(aep.aeid.getFullName(newShortName) != null) { return(false); }
1156
1157 // Seems OK.
1158 return(true);
1159 }
1160
1161 /**If our name is not unique then increment the number-in-series (if non-zero) until we get a unique name.
1162 * This checks the (short) name against the AEP and other databases.
1163 */
1164 void uibIncNumberIfNotZeroWhileNotUnique(final UploadInfoBean uib)
1165 {
1166 while(uib.getNumber() != 0)
1167 {
1168 final String shortName = uib.getName(false);
1169 if(shortNameValidAndUnique(shortName))
1170 { break; /* Seems that we now have a unique name! */ }
1171 uib.incNumberIfNonZero();
1172 }
1173 }
1174
1175
1176 /**Propose/select/add selected files for upload.
1177 * Takes one or more files somehow selected by the user
1178 * (eg with some sort of GUI front end)
1179 * and a base exhibit name:
1180 * <ul>
1181 * <li>validates/guesses the files' types
1182 * <li>sorts the files into name order
1183 * <li>adds them to the table of names where the user can adjust details
1184 * </ul>
1185 * before asking for the files to be uploaded.
1186 * <p>
1187 * Duplicate entries may be silently discarded.
1188 * <p>
1189 * Package-visible to be callable from GUI classes.
1190 *
1191 * @param autoGuessFormat if true then the routine may try to guess the
1192 * type/format of files that do not match the type implied by the name
1193 * @param files one (or more if the number-in-series is not zero) files
1194 * that the user has selected for upload;
1195 * never null not zero-size no containing nulls
1196 * @param uib initialised with the first/desired name;
1197 * never null and must be in a state where enoughValidUniqueInfo()==true
1198 *
1199 * @return null if all is well, else an error message (and no files added)
1200 */
1201 String addSelectedFiles(final boolean autoGuessFormat,
1202 final FileContents files[],
1203 final UploadInfoBean uib)
1204 {
1205 if((files == null) || (files.length < 1) ||
1206 (uib == null) || !uib.enoughValidUniqueInfo())
1207 { throw new IllegalArgumentException(); }
1208
1209 final String baseExhibitName = uib.getFullName();
1210 final String extensionComponent = ExhibitName.getExtensionComponent(baseExhibitName).toString();
1211 final ExhibitMIME.ExhibitTypeParameters etp = ExhibitMIME.getExhibitType(extensionComponent);
1212
1213 try
1214 {
1215 // Sort files into order.
1216 // We use a case-insensitive ordering by name.
1217 Arrays.sort(files, new Comparator<FileContents>(){
1218 /**Compares its two arguments for order. */
1219 public final int compare(final FileContents fc1, final FileContents fc2)
1220 {
1221 try { return(fc1.getName().compareToIgnoreCase(fc2.getName())); }
1222 catch(final IOException e)
1223 {
1224 final String message = "INTERNAL ERROR: failed to sort file(s) for upload: " + e.getMessage();
1225 logger.log(message);
1226 throw new Error(message, e);
1227 }
1228 }
1229 });
1230
1231 // Validate all the files offered.
1232 // They must be readable and have a correct magic number
1233 // and file-extension if not auto-guessing...
1234 // Deal with them is file-name order.
1235 for(int i = 0; i < files.length; ++i)
1236 {
1237 final FileContents fc = files[i];
1238
1239 // Skip this local file if already in selected-files list.
1240 final String name = fc.getName();
1241 if(selectedFiles.localFilenameInUse(name))
1242 { continue; }
1243
1244 // Is the file readable?
1245 if(!fc.canRead())
1246 { return("Cannot read file: " + name); }
1247
1248 // Is the file a sensible length?
1249 if(fc.getLength() < 1)
1250 { return("File is zero-length: " + name); }
1251
1252 // Has file got correct extension (ignoring case).
1253 final boolean correctExt = extensionComponent.equalsIgnoreCase(
1254 FileTools.getExtension(name));
1255
1256 // Has the file got the correct extension AND magic number?
1257 // It is an error not to if "auto"/guess mode is not selected.
1258 final int longestMagicBytes = ExhibitMIME.getLongestMagicBytes();
1259 final InputStream is = new BufferedInputStream(fc.getInputStream(),
1260 longestMagicBytes);
1261 try
1262 {
1263 uibIncNumberIfNotZeroWhileNotUnique(uib); // Move to next unique exhibit name if need be.
1264
1265 is.mark(longestMagicBytes); // Allow rewind to start of file.
1266 final boolean isFileTypeOK =
1267 correctExt &&
1268 ExhibitMIME.magicOK(etp, is);
1269 if(isFileTypeOK)
1270 {
1271 // Magic is fine (and there *is* a magic number)
1272 // and file extension is fine (ignoring case);
1273 // thus file can be added with suggested name
1274 // and the default/suggested extension.
1275 uib.setSuffix(etp.dotSuffixForInputFile);
1276 selectedFiles.add(new SelectedFileDetails(fc, uib.getFullName(), uib.getDescription(), null));
1277 }
1278 else if(!autoGuessFormat)
1279 { return("File may be corrupt or wrong type (try 'auto' mode): " + name); }
1280 else
1281 {
1282 // First try to guess by file extension...
1283 ExhibitMIME.ExhibitTypeParameters etpGuessed =
1284 ExhibitMIME.getInputFileType(name.toLowerCase());
1285 is.reset(); // Rewind ready to check magic...
1286 final boolean guessedOKFromExtension =
1287 (etpGuessed != null) &&
1288 ExhibitMIME.magicOK(etpGuessed, is);
1289
1290 // If the file extension does not seem to be right
1291 // then try to guess just from the file magic number,
1292 // ie from the file content rather than its name.
1293 if(!guessedOKFromExtension)
1294 {
1295 is.reset(); // Rewind ready to check magic...
1296 etpGuessed = ExhibitMIME.guessTypeFromMagic(is);
1297 }
1298
1299 // Failed...
1300 if(etpGuessed == null)
1301 { return("File type is unrecognised: " + name); }
1302
1303 // Need to adjust the exhibit type for this file.
1304 uib.setSuffix("." + etpGuessed.suffixForInputFile);
1305 selectedFiles.add(new SelectedFileDetails(fc, uib.getFullName(), uib.getDescription(), null));
1306 }
1307 }
1308 finally { is.close(); }
1309 }
1310 }
1311 catch(final Exception e)
1312 {
1313 e.printStackTrace();
1314 logger.log("Failed to open file(s) for upload: " + e.getMessage());
1315
1316 // Reset the name to the original in case of error.
1317 uib.setFullName(baseExhibitName);
1318
1319 return("Cannot upload: " + e.getMessage());
1320 }
1321
1322 return(null); // OK!
1323 }
1324
1325
1326
1327
1328 /**Save persistent properties (if possible).
1329 * If we have PersistenceService then
1330 * try to create/allocate space to save (ignoring errors if already there)
1331 * and save the data.
1332 */
1333 private void saveProperties()
1334 {
1335 if((ps != null) && (bs != null))
1336 {
1337 try
1338 {
1339 final URL propsURL = makePropsURL(bs);
1340
1341 // Try to create a space for persisting the data
1342 // if not already present (ignoring error if it is).
1343 try { ps.create(propsURL, UploaderProps.MAX_PERS_BYTES); }
1344 catch(final IOException e) { }
1345
1346 // Now try to save our properties data.
1347 final FileContents fc = ps.get(propsURL);
1348 final OutputStream os = fc.getOutputStream(true);
1349 try { os.write(props.getPersistentData()); }
1350 finally { os.close(); }
1351
1352 if(IsDebug.isDebug) { logger.log("Saved properties..."); }
1353 }
1354 catch(final IOException e)
1355 {
1356 e.printStackTrace();
1357 logger.log("Cannot save properties: " + e.getMessage());
1358 }
1359 }
1360 }
1361
1362
1363 /**Perform any activity required to shut down cleanly, eg save state.
1364 * This should try to avoid taking a long time.
1365 * <p>
1366 * Package-visible so as to be directly usable by the main GUI class.
1367 */
1368 void shutdown()
1369 {
1370 // Save any state...
1371 saveProperties();
1372
1373 // Shut down the tunnel last.
1374 if(tunnel != null) { tunnel.destroy(); }
1375 }
1376 }