source: trunk/gli/src/org/greenstone/gatherer/file/FileQueue.java@ 11073

Last change on this file since 11073 was 11073, checked in by mdewsnip, 18 years ago

Renamed LongProgressBar to GProgressBar, and tidied up a great deal. Removed the BigInteger sillyness and used longs instead, so should be slightly more efficient. This will now be used for all progress bars in the GLI, as it sets indeterminate/string/value safely (should avoid NPEs when setting indeterminate).

  • Property svn:keywords set to Author Date Id Revision
File size: 31.6 KB
Line 
1/**
2 *#########################################################################
3 *
4 * A component of the Gatherer application, part of the Greenstone digital
5 * library suite from the New Zealand Digital Library Project at the
6 * University of Waikato, New Zealand.
7 *
8 * Author: John Thompson, Greenstone Digital Library, University of Waikato
9 *
10 * Copyright (C) 1999 New Zealand Digital Library Project
11 *
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
25 *########################################################################
26 */
27package org.greenstone.gatherer.file;
28
29import java.io.*;
30import java.util.*;
31import javax.swing.*;
32import javax.swing.event.*;
33import javax.swing.tree.*;
34import org.greenstone.gatherer.Configuration;
35import org.greenstone.gatherer.DebugStream;
36import org.greenstone.gatherer.Dictionary;
37import org.greenstone.gatherer.Gatherer;
38import org.greenstone.gatherer.collection.CollectionTreeNode;
39import org.greenstone.gatherer.gui.GProgressBar;
40import org.greenstone.gatherer.gui.tree.DragTree;
41import org.greenstone.gatherer.metadata.MetadataValue;
42import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
43import org.greenstone.gatherer.util.DragComponent;
44import org.greenstone.gatherer.util.StaticStrings;
45import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
46import org.greenstone.gatherer.util.Utility;
47
48/** A threaded object which processes a queue of file actions such as copying and movement. It also handles updating the various trees involved so they are an accurate representation of the file system they are meant to match.
49 * @author John Thompson, Greenstone Digital Library, University of Waikato
50 * @version 2.3
51 */
52public class FileQueue
53 extends Thread
54{
55 /** When someone requests the movement queue to be dumped this cancel flag is set to true. */
56 private boolean cancel_action = false;
57 /** The button which controls the stopping of the file queue. */
58 private JButton stop_button = null;
59 /** true if the user has selected yes to all from a file 'clash' dialog. */
60 private boolean yes_to_all = false;
61 /** A label explaining the current moving files status. */
62 private JLabel file_status = null;
63 /** A list containing a queue of waiting movement jobs. */
64 private ArrayList queue = null;
65 /** A progress bar which shows how many bytes, out of the total size of bytes, has been moved. */
66 private GProgressBar progress = null;
67
68
69 /** Constructor.
70 */
71 public FileQueue() {
72 DebugStream.println("FileQueue started.");
73 this.queue = new ArrayList();
74 file_status = new JLabel();
75 Dictionary.setText(file_status, "FileActions.No_Activity");
76 progress = new GProgressBar();
77 progress.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
78 progress.setForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
79 progress.setString(Dictionary.get("FileActions.No_Activity"));
80 progress.setStringPainted(true);
81 }
82
83 /** Requeue an existing job into the queue.
84 * @param job A previously created FileJob.
85 */
86 synchronized private void addJob(FileJob job, int position) {
87 job.done = true; // Ensure that the requeued job is marked as done.
88 queue.add(position, job);
89 notify();
90 }
91
92 /** Add a new job to the queue, specifiying as many arguments as is necessary to complete this type of job (ie delete needs no target information).
93 * @param id A long id unique to all jobs created by a single action.
94 * @param source The DragComponent source of this file, most likely a DragTree.
95 * @param child The FileNode you wish to mode.
96 * @param target The DragComponent to move the file to, again most likely a DragTree.
97 * @param parent The files new FileNode parent within the target.
98 * @param type The type of this movement as an int, either COPY or DELETE.
99 */
100 public void addJob(long id, DragComponent source, FileNode[] children, DragComponent target, FileNode parent, byte type, boolean folder_level)
101 {
102 // Queue the sub-job(s) (this may fail if we are asked to delete a read only file)
103 for (int i = 0; i < children.length; i++) {
104 addJob(id, source, children[i], target, parent, type, folder_level, -1);
105 }
106 }
107
108 synchronized private void addJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type, boolean folder_level, int position) {
109 FileJob job = new FileJob(id, source, child, target, parent, type);
110 job.folder_level = folder_level;
111 DebugStream.println("Adding job: " + job);
112 if(position != -1 && position <= queue.size() + 1) {
113 queue.add(position, job);
114 }
115 else {
116 queue.add(job);
117 }
118 notify();
119 }
120
121 /** Calculates the total deep file size of the selected file nodes.
122 * @param files a FileNode[] of selected files
123 * @return true if a cancel was signalled, false otherwise
124 * @see org.greenstone.gatherer.file.FileManager.Task#run()
125 */
126 public boolean calculateSize(FileNode[] files)
127 {
128 file_status.setText(Dictionary.get("FileActions.Calculating_Size"));
129 progress.setString(Dictionary.get("FileActions.Calculating_Size"));
130
131 // Calculate the total file size of all the selected file nodes
132 Vector remaining = new Vector();
133 for (int i = 0; !cancel_action && i < files.length; i++) {
134 remaining.add(files[i]);
135 }
136 while (!cancel_action && remaining.size() > 0) {
137 FileNode node = (FileNode) remaining.remove(0);
138 if (node.isLeaf()) {
139 progress.addMaximum(node.getFile().length());
140 }
141 else {
142 for (int i = 0; !cancel_action && i < node.getChildCount(); i++) {
143 remaining.add(node.getChildAt(i));
144 }
145 }
146 }
147
148 // Now we return if calculation was cancelled so that the FileManagers Task can skip the addJob phase correctly.
149 if (cancel_action) {
150 cancel_action = false; // reset
151 return true;
152 }
153 else {
154 return false;
155 }
156 }
157
158 /** This method is called to cancel the job queue at the next available moment. */
159 public void cancelAction() {
160 cancel_action = true;
161 }
162
163
164 private int countFolderDepth(File file)
165 {
166 int depth = 0;
167 while (file != null) {
168 depth++;
169 file = file.getParentFile();
170 }
171 return depth;
172 }
173
174
175 /** Format the given filename path string so that it is no longer than the given width. If it is wider replace starting directories with ...
176 * @param key The key <strong>String</Strong> used to retrieve a phrase from the dictionary for this item.
177 * @param raw The raw filename path <strong>String</strong>.
178 * @param width The maximum width as an <i>int</i>.
179 * @return A path <strong>String</strong> no longer than width.
180 */
181 private String formatPath(String key, String raw, int width)
182 {
183 JLabel label = new JLabel(Dictionary.get(key, raw));
184 int position = -1;
185 while(label.getPreferredSize().width > width && (position = raw.indexOf(File.separator)) != -1) {
186 raw = "..." + raw.substring(position + 1);
187 label.setText(Dictionary.get(key, raw));
188 }
189 if(raw.indexOf(File.separator) == -1 && raw.startsWith("...")) {
190 raw = raw.substring(3);
191 }
192 return raw;
193 }
194
195
196 /** Access to the file state label. */
197 public JLabel getFileStatus() {
198 return file_status;
199 }
200
201 /** Access to the progress bar. */
202 public GProgressBar getProgressBar() {
203 return progress;
204 }
205
206
207 /** The run method exists in every thread, and here it is used to work its way through the queue of Jobs. If no jobs are waiting and it cans, it waits until a job arrives. If a job is present then it is either COPIED or DELETED, with the records being copied or removed as necessary, and directories being recursed through. Finally the user can press cancel to cause the loop to prematurely dump the job queue then wait.
208 * @see org.greenstone.gatherer.Gatherer
209 * @see org.greenstone.gatherer.collection.CollectionManager
210 * @see org.greenstone.gatherer.file.FileJob
211 * @see org.greenstone.gatherer.file.FileNode
212 * @see org.greenstone.gatherer.gui.GProgressBar
213 * @see org.greenstone.gatherer.util.Utility
214 */
215 public void run()
216 {
217 super.setName("FileQueue");
218
219 while (!Gatherer.exit) {
220 try {
221 // Retrieve the next job
222 int position = queue.size() - 1;
223 FileJob job = null;
224 if (position >= 0) {
225 job = (FileJob) queue.remove(position);
226 }
227
228 if (job != null) {
229 ///ystem.err.println("Found job: " + job);
230 // Enabled stop button
231 stop_button.setEnabled(true);
232 // The user can cancel this individual action at several places, so keep track if the state is 'ready' for the next step.
233 boolean ready = true;
234 FileNode origin_node = job.getOrigin();
235 FileNode destination_node = job.getDestination();
236 FileSystemModel source_model = (FileSystemModel)job.source.getTreeModel();
237 FileSystemModel target_model = (FileSystemModel)job.target.getTreeModel();
238 if(destination_node == null) {
239 // Retrieve the root node of the target model instead. A delete, or course, has no target file so all deleted files are added to the root of the Recycle Bin model.
240 destination_node = (FileNode) target_model.getRoot();
241 }
242
243 // Extract common job details.
244 File source_file = origin_node.getFile();
245 File target_file = null;
246 // Determine the target file for a copy or move.
247 if (job.type == FileJob.COPY || job.type == FileJob.MOVE) {
248 // use the name of the filenode instead of the name of the file - these should be the same except for the collection directories where we want the collection name to be used, not 'import' which is the underlying name
249 target_file = new File(destination_node.getFile(), origin_node.toString());
250 }
251 // To copy a file, copy it then add any metadata found at the source. If this file was already in our collection then we must ensure the lastest version of its metadata.xml has been saved to disk. To copy a directory simply create the directory at the destination, then add all of its children files as new jobs.
252 if((job.type == FileJob.COPY || job.type == FileJob.MOVE) && !job.done) {
253 ///ystem.err.println("Copy/Move: " + origin_node);
254
255 // The number one thing to check is whether we are in a cyclic loop. The easiest way is just to check how deep we are
256 int max_folder_depth = Configuration.getInt("general.max_folder_depth", Configuration.COLLECTION_SPECIFIC);
257 boolean continue_over_depth = false;
258 if (countFolderDepth(source_file) > max_folder_depth) {
259 Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("General.No"), Dictionary.get("FileActions.Increase_Depth") };
260 String args[] = { String.valueOf(max_folder_depth), source_file.getAbsolutePath() };
261 int result = JOptionPane.showOptionDialog(Gatherer.g_man, Utility.formatHTMLWidth(Dictionary.get("FileActions.Possible_Cyclic_Path", args), 80), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[1]);
262 args = null;
263 options = null;
264 switch(result) {
265 case 0: // Yes
266 continue_over_depth = true;
267 break;
268 case 2: // Continue and increase depth
269 continue_over_depth = true;
270 Configuration.setInt("general.max_folder_depth", Configuration.COLLECTION_SPECIFIC, (max_folder_depth + 1));
271 break;
272 }
273 }
274 else {
275 continue_over_depth = true;
276 }
277
278 if(continue_over_depth) {
279 FileNode new_node = null;
280 // Check if file exists, and action as necessary. Be aware the user can choose to cancel the action all together (where upon ready becomes false).
281 if(target_file.exists()) {
282 // We've previously been told
283 if(yes_to_all) {
284 // Remove the old file and tree entry.
285 target_file.delete();
286 ready = true;
287 }
288 else {
289 ///atherer.println("Opps! This filename already exists. Give the user some options.");
290 Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("FileActions.Yes_To_All"), Dictionary.get("General.No"), Dictionary.get("General.Cancel") };
291 int result = JOptionPane.showOptionDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Exists", target_file.getName()), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
292 switch(result) {
293 case 1: // Yes To All
294 yes_to_all = true;
295 case 0: // Yes
296 // Remove the old file and tree entry.
297 if(destination_node != null) {
298 TreePath destination_path = new TreePath(destination_node.getPath());
299 CollectionTreeNode temp_target_node = new CollectionTreeNode(target_file); // !!! , target_model, true);
300 TreePath target_path = destination_path.pathByAddingChild(temp_target_node);
301 SynchronizedTreeModelTools.removeNodeFromParent(target_model, target_model.getNode(target_path));
302 target_path = null;
303 temp_target_node = null;
304 destination_path = null;
305 }
306 target_file.delete();
307 ready = true;
308 break;
309 case 3: // No To All
310 cancel_action = true;
311 case 2: // No
312 default:
313 ready = false;
314 // Increment progress by size of potentially copied file
315 progress.addValue(origin_node.getFile().length());
316 }
317 }
318 }
319 // We proceed with the copy/move if the ready flag is still set. If it is that means there is no longer any existing file of the same name.
320 if(ready) {
321 // update status area
322 String args[] = new String[1];
323 args[0] = "" + (queue.size() + 1) + "";
324 if(job.type == FileJob.COPY) {
325 args[0] = formatPath("FileActions.Copying", source_file.getAbsolutePath(), file_status.getSize().width);
326 file_status.setText(Dictionary.get("FileActions.Copying", args));
327 }
328 else {
329 args[0] = formatPath("FileActions.Moving", source_file.getAbsolutePath(), file_status.getSize().width);
330 file_status.setText(Dictionary.get("FileActions.Moving", args));
331 }
332 args = null;
333
334 // If source is a file
335 if(source_file.isFile()) {
336 // copy the file. If anything goes wrong the copy file should throw the appropriate exception. No matter what exception is thrown (bar an IOException) we display some message, perhaps take some action, then cancel the remainder of the pending file jobs. No point in being told your out of hard drive space for each one of six thousand files eh?
337 try {
338 copyFile(source_file, target_file, progress);
339 }
340 // If we can't find the source file, then the most likely reason is that the file system has changed since the last time it was mapped. Warn the user that the requested file can't be found, then force a refresh of the source folder involved.
341 catch(FileNotFoundException fnf_exception) {
342 DebugStream.printStackTrace(fnf_exception);
343 cancel_action = true;
344 // Show warning.
345 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Not_Found_Message", source_file.getName()), Dictionary.get("FileActions.File_Not_Found_Title"), JOptionPane.ERROR_MESSAGE);
346 // Force refresh of source folder.
347 source_model.refresh(new TreePath(((FileNode)origin_node.getParent()).getPath()));
348 }
349 catch(FileAlreadyExistsException fae_exception) {
350 DebugStream.printStackTrace(fae_exception);
351 cancel_action = true;
352 // Show warning.
353 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Already_Exists_Message", target_file.getName()), Dictionary.get("FileActions.File_Already_Exists_Title"), JOptionPane.ERROR_MESSAGE);
354 // Nothing else can be done by the Gatherer.
355 }
356 catch(InsufficientSpaceException is_exception) {
357 DebugStream.printStackTrace(is_exception);
358 cancel_action = true;
359 // Show warning. The message body of the expection explains how much more space is required for this file copy.
360 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Insufficient_Space_Message", is_exception.getMessage()), Dictionary.get("FileActions.Insufficient_Space_Title"), JOptionPane.ERROR_MESSAGE);
361 // Nothing else can be done by the Gatherer. In fact if we are really out of space I'm not even sure we can quit safely.
362 }
363 catch (ReadNotPermittedException rnp_exception) {
364 if (DebugStream.isDebuggingEnabled()) {
365 DebugStream.printStackTrace(rnp_exception);
366 }
367 cancel_action = true;
368 // Show warning
369 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Read_Not_Permitted_Message", source_file.getAbsolutePath()), Dictionary.get("FileActions.Write_Not_Permitted_Title"), JOptionPane.ERROR_MESSAGE);
370 // Nothing else we can do.
371 }
372 catch(UnknownFileErrorException ufe_exception) {
373 DebugStream.printStackTrace(ufe_exception);
374 cancel_action = true;
375 // Show warning
376 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Unknown_File_Error_Message"), Dictionary.get("FileActions.Unknown_File_Error_Title"), JOptionPane.ERROR_MESSAGE);
377 // Nothing else we can do.
378 }
379 catch(WriteNotPermittedException wnp_exception) {
380 if (DebugStream.isDebuggingEnabled()) {
381 DebugStream.printStackTrace(wnp_exception);
382 }
383 cancel_action = true;
384 // Show warning
385 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Write_Not_Permitted_Message", target_file.getAbsolutePath()), Dictionary.get("FileActions.Write_Not_Permitted_Title"), JOptionPane.ERROR_MESSAGE);
386 // Nothing else we can do.
387 }
388 catch(IOException exception) {
389 // Can't really do much about this.
390 DebugStream.printStackTrace(exception);
391 }
392 // If not cancelled
393 if (!cancel_action) {
394 // Create a dummy FileNode with the correct structure (so getPath works)
395 new_node = new CollectionTreeNode(target_file);
396 SynchronizedTreeModelTools.insertNodeInto(target_model, destination_node, new_node);
397 }
398 }
399 // Else
400 else if(source_file.isDirectory()) {
401 // create new record
402 CollectionTreeNode directory_record = new CollectionTreeNode(target_file);
403 SynchronizedTreeModelTools.insertNodeInto(target_model, destination_node, directory_record);
404 // Why is this not happening eh?
405 directory_record.setParent(destination_node);
406 if(!target_file.exists()) {
407 // make the directory
408 target_file.mkdirs();
409 new_node = directory_record;
410 }
411
412 // Queue non-filtered child files for copying. If this directory already existed, the child records will have to generate the undo jobs, as we don't want to entirely delete this directory if it already existed.
413 FileNode child_record = null;
414 // In order to have a sane copy proceedure (rather than always copying last file first as it used to) we always add the child node at the position the parent was removed from. Consider the file job 'a' at the end of the queue which generates three new jobs 'b', 'c' and 'd'. The resulting flow should look like this.
415 // -- Starting queue ...[a]
416 // remove(position) = 'a' ...
417 // add(position, 'b') ...[b]
418 // add(position, 'c') ...[c][b]
419 // add(position, 'd') ...[d][c][b]
420 // Next loop
421 // remove(position) = 'b' ...[d][c]
422 //for(int i = 0; i < origin_node.getChildCount(); i++) {
423 for (int i=origin_node.getChildCount()-1; i>=0; i--) {
424 child_record = (FileNode) origin_node.getChildAt(i);
425 addJob(job.ID(), job.source, child_record, job.target, directory_record, job.type, false, position);
426 }
427 child_record = null;
428 directory_record = null;
429 }
430 // The file wasn't found!
431 else {
432 cancel_action = true;
433 // Show warning.
434 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Not_Found_Message", source_file.getName()), Dictionary.get("FileActions.File_Not_Found_Title"), JOptionPane.ERROR_MESSAGE);
435 // Force refresh of source folder.
436 source_model.refresh(new TreePath(((FileNode)origin_node.getParent()).getPath()));
437 }
438
439 // If we haven't been cancelled and we created a new FileNode during the above phase, now is the time to deal with metadata
440 if (!cancel_action && new_node != null) {
441 // If the file came from inside our collection...
442 if (job.source.toString().equals("Collection")) {
443 // Get the non-folder level metadata assigned to the origin node...
444 ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
445 // ...and remove it from the original node and assign it to the new folder
446 MetadataXMLFileManager.removeMetadata((CollectionTreeNode) origin_node, assigned_metadata);
447 MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_node, assigned_metadata);
448 }
449 // If it came from the workspace search for metadata assigned to the file
450 else if (job.source.toString().equals("Workspace")) {
451 ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(origin_node.getFile());
452 MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_node, assigned_metadata);
453 }
454
455 if (job.type == FileJob.COPY && new_node.getFile().isFile()) {
456 Gatherer.c_man.fireFileAddedToCollection(new_node.getFile());
457 }
458 }
459 new_node = null;
460 }
461 }
462 }
463 // If we haven't been cancelled, and we've been asked to delete a directory/file, or perhaps as part of a move, we delete the file. This involves removing any existing metadata and then copying the file to the recycled bin (for a delete only), then deleting the file. When deleting a directory record from the tree (or from the filesystem for that matter) we must ensure that all of the descendant records have already been removed. If we fail to do this the delete will fail, or you will be bombarded with hundreds of 'Parent node of null not allowed' error messages. Also be aware that if the user has cancelled just this action, because of say a name clash, then we shouldn't do any deleting of any sort dammit.
464 if(!cancel_action && ready && (job.type == FileJob.DELETE || job.type == FileJob.MOVE)) {
465 // Update the progress bar for this job
466 if (source_file.isFile()) {
467 progress.addValue(source_file.length());
468 }
469
470 // If the source is a file or an empty directory (but not the root node of a tree)
471 File[] child_list = source_file.listFiles();
472 if (source_file.isFile() || (child_list != null && child_list.length == 0 && origin_node.getParent() != null)) {
473 // Update status area
474 String args[] = new String[1];
475 args[0] = formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width);
476 file_status.setText(Dictionary.get("FileActions.Deleting", args));
477
478 // If it is a metadata.xml file, we must unload it
479 if (source_file.getName().equals(StaticStrings.METADATA_XML)) {
480 MetadataXMLFileManager.unloadMetadataXMLFile(source_file);
481 }
482
483 // Remove the metadata assigned directly to the file
484 ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(origin_node.getFile());
485 MetadataXMLFileManager.removeMetadata((CollectionTreeNode) origin_node, assigned_metadata);
486
487 // Remove from model
488 FileNode parent_record = (FileNode) origin_node.getParent();
489 if (parent_record != null) {
490 SynchronizedTreeModelTools.removeNodeFromParent(source_model, origin_node);
491 }
492
493 // Delete the source file
494 if (!Utility.delete(source_file)) {
495 // Show message that we couldn't delete
496 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Not_Deleted_Message", source_file.getName()), Dictionary.get("FileActions.File_Not_Deleted_Title"), JOptionPane.ERROR_MESSAGE);
497 }
498 }
499 // Else the source is a directory and it has children remaining
500 else if(child_list != null && child_list.length > 0) {
501 // Don't worry about all this for true file move actions.
502 if(job.type == FileJob.DELETE) {
503 // queue all of its children, (both filtered and non-filtered), but for deleting only. Don't queue jobs for a current move event, as they would be queued as part of copying. I have no idea way, per sec, however the children within the origin node are always invalid during deletion (there are several copies of some nodes?!?). I'll check that each child is only added once.
504 origin_node.refresh();
505 for(int i = 0; i < origin_node.size(); i++) {
506 FileNode child_record = (FileNode) origin_node.getChildAtUnfiltered(i);
507 ///atherer.println("Queuing: " + child_record);
508 addJob(job.ID(), job.source, child_record, job.target, destination_node, FileJob.DELETE, false, position);
509 }
510 }
511 // Requeue a delete job -after- the children have been dealt with. Remember I've reversed the direction of the queue so sooner is later. Te-he. Also have to remember that we have have followed this path to get here for a move job: Copy Directory -> Queue Child Files -> Delete Directory (must occur after child files) -> Queue Directory.
512 // One special case. Do not requeue root nodes. Don't requeue jobs marked as done.
513 if(origin_node.getParent() != null && !job.done) {
514 ///atherer.println("Requeuing: " + origin_node.getFile().getAbsolutePath());
515 job.type = FileJob.DELETE; // You only requeue jobs that are deletes, as directories must be inspected before children, but deleted after.
516 addJob(job, position);
517 }
518 else {
519 DebugStream.println("I've already done this job twice. I refuse to requeue it again!");
520 }
521 }
522 }
523 job = null;
524 source_file = null;
525 target_file = null;
526 origin_node = null;
527
528 // We only break out of the while loop if we are out of files or the action was cancelled
529 if (cancel_action) {
530 // Empty queue
531 clearJobs();
532 cancel_action = false;
533 }
534 }
535 else { // job == null
536 // Disable stop button
537 if (stop_button != null) {
538 stop_button.setEnabled(false);
539 }
540 synchronized(this) {
541 // Force both workspace and collection trees to refresh
542 if (Gatherer.g_man != null) {
543 Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED);
544 Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED);
545 }
546
547 // Reset status area
548 file_status.setText(Dictionary.get("FileActions.No_Activity"));
549 progress.reset();
550 progress.setString(Dictionary.get("FileActions.No_Activity"));
551 yes_to_all = false;
552 try {
553 wait();
554 }
555 catch (InterruptedException exception) {}
556 }
557 }
558 }
559 catch (Exception error) {
560 DebugStream.printStackTrace(error);
561 }
562 }
563 }
564
565
566 /** Register the button that will be responsible for stopping executing file actions.
567 * @param stop_button a JButton
568 */
569 public void registerStopButton(JButton stop_button) {
570 this.stop_button = stop_button;
571 }
572
573
574 synchronized private void clearJobs() {
575 queue.clear();
576 }
577
578 /** Copy the contents from the source directory to the destination
579 * directory.
580 * @param source The source directory
581 * @param destination The destination directory
582 * @param progress A progress bar to monitor copying progress
583 * @see org.greenstone.gatherer.Gatherer
584 */
585 public void copyDirectoryContents(File source, File destination, GProgressBar progress)
586 throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException
587 {
588 if (!source.isDirectory()) return;
589 // check that dest dirs exist
590 destination.mkdirs();
591
592 File [] src_files = source.listFiles();
593 if (src_files.length == 0) return; // nothing to copy
594 for (int i=0; i<src_files.length; i++) {
595 File f = src_files[i];
596 String f_name = f.getName();
597 File new_file = new File(destination, f_name);
598 if (f.isDirectory()) {
599 copyDirectoryContents(f, new_file, progress);
600 } else if (f.isFile()) {
601 copyFile(f, new_file, progress);
602 }
603 }
604
605 }
606
607 /** Copy a file from the source location to the destination location.
608 * @param source The source File.
609 * @param destination The destination File.
610 * @see org.greenstone.gatherer.Gatherer
611 */
612 public void copyFile(File source, File destination, GProgressBar progress)
613 throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException {
614 if(source.isDirectory()) {
615 destination.mkdirs();
616 }
617 else {
618 // Check if the origin file exists.
619 if (!source.exists()) {
620 DebugStream.println("Couldn't find the source file.");
621 throw(new FileNotFoundException());
622 }
623
624 // Make sure the destination file does not exist.
625 if (destination.exists()) {
626 throw(new FileAlreadyExistsException());
627 }
628
629 // Open an input stream to the source file
630 FileInputStream f_in = null;
631 try {
632 f_in = new FileInputStream(source);
633 }
634 catch (FileNotFoundException exception) {
635 // A FileNotFoundException translates into a ReadNotPermittedException in this case
636 throw new ReadNotPermittedException(exception.toString());
637 }
638
639 // Create an necessary directories for the target file
640 File dirs = destination.getParentFile();
641 dirs.mkdirs();
642
643 // Open an output stream to the target file
644 FileOutputStream f_out = null;
645 try {
646 f_out = new FileOutputStream(destination);
647 }
648 catch (FileNotFoundException exception) {
649 // A FileNotFoundException translates into a WriteNotPermittedException in this case
650 throw new WriteNotPermittedException(exception.toString());
651 }
652
653 // Copy the file
654 byte data[] = new byte[Utility.BUFFER_SIZE];
655 int data_size = 0;
656 while((data_size = f_in.read(data, 0, Utility.BUFFER_SIZE)) != -1 && !cancel_action) {
657 long destination_size = destination.length();
658 try {
659 f_out.write(data, 0, data_size);
660 }
661 // If an IO exception occurs, we can do some maths to determine if the number of bytes written to the file was less than expected. If so we assume a InsufficientSpace exception. If not we just throw the exception again.
662 catch (IOException io_exception) {
663 if(destination_size + (long) data_size > destination.length()) {
664 // Determine the difference (which I guess is in bytes).
665 long difference = (destination_size + (long) data_size) - destination.length();
666 // Transform that into a human readable string.
667 String message = Utility.formatFileLength(difference);
668 throw(new InsufficientSpaceException(message));
669 }
670 else {
671 throw(io_exception);
672 }
673 }
674 if(progress != null) {
675 progress.addValue(data_size);
676 }
677 }
678 // Flush and close the streams to ensure all bytes are written.
679 f_in.close();
680 f_out.close();
681 // We have now, in theory, produced an exact copy of the source file. Check this by comparing sizes.
682 if(!destination.exists() || (!cancel_action && source.length() != destination.length())) {
683 throw(new UnknownFileErrorException());
684 }
685 // If we were cancelled, ensure that none of the destination file exists.
686 if(cancel_action) {
687 destination.delete();
688 }
689 }
690 }
691}
Note: See TracBrowser for help on using the repository browser.