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

Last change on this file since 6035 was 5847, checked in by mdewsnip, 21 years ago

A much improved workspace tree that only refreshes when it really needs to (and only refreshes what it really needs to). This should prevent the five second plus refreshes on slow machines.

I'm planning to tidy up the collection tree in a similar way, when I get time.

  • Property svn:keywords set to Author Date Id Revision
File size: 37.3 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.Dictionary;
35import org.greenstone.gatherer.Gatherer;
36import org.greenstone.gatherer.file.FileJob;
37import org.greenstone.gatherer.file.FileNode;
38import org.greenstone.gatherer.gui.LongProgressBar;
39import org.greenstone.gatherer.gui.tree.DragTree;
40import org.greenstone.gatherer.msm.GDMManager;
41import org.greenstone.gatherer.undo.UndoManager;
42import org.greenstone.gatherer.util.ArrayTools;
43import org.greenstone.gatherer.util.DragComponent;
44import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
45import org.greenstone.gatherer.util.Utility;
46
47/** 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.
48 * @author John Thompson, Greenstone Digital Library, University of Waikato
49 * @version 2.3
50 */
51public class FileQueue
52 extends Thread
53 implements TreeSelectionListener {
54 /** When someone requests the movement queue to be dumped this cancel flag is set to true. */
55 private boolean cancel_action = false;
56 /** A temporary mapping from currently existing FileNode folder to their equivelent FileNode folder within the undo managers tree. */
57 private HashMap completed_folder_mappings = new HashMap();
58 /** true to cause this file queue to return from run() as soon as there are no jobs left on the queue. Useful for undo jobs which must occur before a specific action. */
59 private boolean return_immediately = false;
60 /** We are only allowed to wait under specific circumstances. */
61 /* private boolean wait_allowed = true; */
62 /** true if the user has selected yes to all from a file 'clash' dialog. */
63 private boolean yes_to_all = false;
64 /** A temporary mapping from currently existing FileNodes to the potential FileNode folder within the undo managers tree. */
65 private HashMap recycle_folder_mappings = new HashMap();
66 /** A label explaining the current moving files status. */
67 private JLabel file_status = null;
68 /** A list containing a queue of waiting movement jobs. */
69 //private LinkedList queue;
70 private ArrayList queue;
71 /** A progress bar which shows how many bytes, out of the total size of bytes, has been moved. */
72 private LongProgressBar progress = null;
73 /** The last piece of text shown on the file status label, just incase we are displaying a very temporary message. */
74 private String previous = null;
75 /** Constructor.
76 * @param return_immediately true to cause this file queue to return from run() as soon as there are no jobs left on the queue.
77 * @see org.greenstone.gatherer.Configuration
78 * @see org.greenstone.gatherer.gui.Coloring
79 * @see org.greenstone.gatherer.gui.LongProgressBar
80 */
81 public FileQueue(boolean return_immediately) {
82 this.return_immediately = return_immediately;
83 this.queue = new ArrayList();//LinkedList();
84 String args[] = new String[2];
85 args[0] = "0";
86 args[1] = "0";
87 file_status = new JLabel();
88 Dictionary.setText(file_status, "FileActions.Selected", args);
89 progress = new LongProgressBar();
90 progress.setBackground(Gatherer.config.getColor("coloring.collection_tree_background", false));
91 progress.setForeground(Gatherer.config.getColor("coloring.collection_tree_foreground", false));
92 progress.setString(Dictionary.get("FileActions.No_Activity"));
93 progress.setStringPainted(true);
94 args = null;
95 }
96
97 /** Requeue an existing job into the queue.
98 * @param job A previously created FileJob.
99 */
100 synchronized public void addJob(FileJob job, int position) {
101 job.done = true; // Ensure that the requeued job is marked as done.
102 queue.add(position, job);
103 notify();
104 }
105
106 /** 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).
107 * @param id A long id unique to all jobs created by a single action.
108 * @param source The DragComponent source of this file, most likely a DragTree.
109 * @param child The FileNode you wish to mode.
110 * @param target The DragComponent to move the file to, again most likely a DragTree.
111 * @param parent The files new FileNode parent within the target.
112 * @param type The type of this movement as an int, either COPY or DELETE.
113 * @param undo true if this job should generate undo jobs, false for redo ones.
114 * @param undoable true if this job can generate undo or redo jobs at all, false otherwise.
115 */
116 public void addJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type, boolean undo, boolean undoable, boolean folder_level) {
117 addJob(id, source, child, target, parent, type, undo, undoable, folder_level, -1);
118 }
119
120 synchronized public void addJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type, boolean undo, boolean undoable, boolean folder_level, int position) {
121 FileJob job = new FileJob(id, source, child, target, parent, type, undo, undoable);
122 job.folder_level = folder_level;
123 ///ystem.err.println("Adding job: " + job);
124 if(position != -1 && position <= queue.size() + 1) {
125 queue.add(position, job);
126 }
127 else {
128 queue.add(job);
129 }
130 notify();
131 }
132
133 /** Calculates the total deep file size of the selected file nodes.
134 * @param files a FileNode[] of selected files
135 * @return true if a cancel was signalled, false otherwise
136 * @see org.greenstone.gatherer.file.FileManager.Task#run()
137 */
138 public boolean calculateSize(FileNode[] files) {
139 progress.reset();
140 progress.setString(Dictionary.get("FileActions.Calculating_Size"));
141 progress.setIndeterminate(true);
142 Vector remaining = new Vector();
143 for(int i = 0; !cancel_action && i < files.length; i++) {
144 remaining.add(files[i]);
145 }
146 while(!cancel_action && remaining.size() > 0) {
147 FileNode node = (FileNode)remaining.remove(0);
148 if(node.isLeaf()) {
149 progress.addMaximum(node.getFile().length());
150 }
151 else {
152 for(int i = 0; !cancel_action && i < node.getChildCount(); i++) {
153 remaining.add(node.getChildAt(i));
154 }
155 }
156 }
157 progress.setString(Dictionary.get("FileActions.No_Activity"));
158 progress.setIndeterminate(false);
159 // Now we return if calculation was cancelled so that the FileManagers Task can skip the addJob phase correctly.
160 if(cancel_action) {
161 cancel_action = false; // reset
162 return true;
163 }
164 else {
165 return false;
166 }
167 }
168
169 /** This method is called to cancel the job queue at the next available moment. */
170 public void cancelAction() {
171 cancel_action = true;
172 }
173 /** Access to the file state label. */
174 public JLabel getFileStatus() {
175 return file_status;
176 }
177
178 /** Access to the progress bar. */
179 public LongProgressBar getProgress() {
180 return progress;
181 }
182 /** Prevent the progress bar updating momentarily, while its size is re-adjusted. */
183 public void pause() {
184 progress.setIndeterminate(true);
185 }
186 /** 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.
187 * @see org.greenstone.gatherer.Gatherer
188 * @see org.greenstone.gatherer.collection.CollectionManager
189 * @see org.greenstone.gatherer.file.FileJob
190 * @see org.greenstone.gatherer.file.FileNode
191 * @see org.greenstone.gatherer.gui.LongProgressBar
192 * @see org.greenstone.gatherer.msm.MetadataSetManager
193 * @see org.greenstone.gatherer.undo.UndoManager
194 * @see org.greenstone.gatherer.util.Utility
195 */
196 public void run() {
197 super.setName("FileQueue");
198 while(!Gatherer.self.exit) {
199 try {
200 // Retrieve the next job
201 int position = queue.size() - 1;
202 FileJob job = removeJob(position);
203 if (job != null) {
204 ///ystem.err.println("Found job: " + job);
205 // The user can cancel this individual action at several places, so keep track if the state is 'ready' for the next step.
206 boolean ready = true;
207 FileNode origin_node = job.getOrigin();
208 FileNode destination_node = job.getDestination();
209 FileSystemModel source_model = (FileSystemModel)job.source.getTreeModel();
210 FileSystemModel target_model = (FileSystemModel)job.target.getTreeModel();
211 if(destination_node == null) {
212 // 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.
213 destination_node = (FileNode) target_model.getRoot();
214 }
215 // Extract common job details.
216 File source_file = origin_node.getFile();
217 File target_file = null;
218 // Determine the target file for a copy or move.
219 if(job.type == FileJob.COPY || job.type == FileJob.MOVE) {
220 //target_file = new File(destination_node.getFile(), source_file.getName());
221 // 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
222 target_file = new File(destination_node.getFile(), origin_node.toString());
223 }
224 // 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.
225 if((job.type == FileJob.COPY || job.type == FileJob.MOVE) && !job.done) {
226 ///ystem.err.println("Copy/Move: " + origin_node);
227 FileNode new_node = null;
228 // 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).
229 if(target_file.exists()) {
230 // We've previously been told
231 if(yes_to_all) {
232 // Remove the old file and tree entry.
233 target_file.delete();
234 ready = true;
235 }
236 else {
237 ///atherer.println("Opps! This filename already exists. Give the user some options.");
238 Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("FileActions.Yes_To_All"), Dictionary.get("General.No"), Dictionary.get("General.Cancel") };
239 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]);
240 switch(result) {
241 case 1: // Yes To All
242 yes_to_all = true;
243 case 0: // Yes
244 // Remove the old file and tree entry.
245 if(destination_node != null) {
246 TreePath destination_path = new TreePath(destination_node.getPath());
247 FileNode temp_target_node = new FileNode(target_file, target_model, true);
248 TreePath target_path = destination_path.pathByAddingChild(temp_target_node);
249 SynchronizedTreeModelTools.removeNodeFromParent(target_model, target_model.getNode(target_path));
250 target_path = null;
251 temp_target_node = null;
252 destination_path = null;
253 }
254 target_file.delete();
255 ready = true;
256 break;
257 case 3: // No To All
258 cancel_action = true;
259 case 2: // No
260 default:
261 ready = false;
262 // Increment progress by size of potentially copied file
263 progress.addValue(origin_node.getFile().length());
264 }
265 }
266 }
267 // 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.
268 if(ready) {
269 // update status area
270 String args[] = new String[1];
271 args[0] = "" + (queue.size() + 1) + "";
272 if(job.type == FileJob.COPY) {
273 args[0] = Utility.formatPath("FileActions.Copying", source_file.getAbsolutePath(), file_status.getSize().width);
274 file_status.setText(Dictionary.get("FileActions.Copying", args));
275 }
276 else {
277 args[0] = Utility.formatPath("FileActions.Moving", source_file.getAbsolutePath(), file_status.getSize().width);
278 file_status.setText(Dictionary.get("FileActions.Moving", args));
279 }
280 args = null;
281
282 // If source is a file
283 if(source_file.isFile()) {
284 // 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?
285 try {
286 copyFile(source_file, target_file, progress);
287 }
288 // 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.
289 catch(FileNotFoundException fnf_exception) {
290 Gatherer.printStackTrace(fnf_exception);
291 cancel_action = true;
292 // Show warning.
293 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);
294 // Force refresh of source folder.
295 source_model.refresh(new TreePath(((FileNode)origin_node.getParent()).getPath()));
296 }
297 catch(FileAlreadyExistsException fae_exception) {
298 Gatherer.printStackTrace(fae_exception);
299 cancel_action = true;
300 // Show warning.
301 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);
302 // Nothing else can be done by the Gatherer.
303 }
304 catch(InsufficientSpaceException is_exception) {
305 Gatherer.printStackTrace(is_exception);
306 cancel_action = true;
307 // Show warning. The message body of the expection explains how much more space is required for this file copy.
308 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Insufficient_Space_Message", is_exception.getMessage()), Dictionary.get("FileActions.Insufficient_Space_Title"), JOptionPane.ERROR_MESSAGE);
309 // 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.
310 }
311 catch(UnknownFileErrorException ufe_exception) {
312 Gatherer.printStackTrace(ufe_exception);
313 cancel_action = true;
314 // Show warning
315 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Unknown_File_Error_Message"), Dictionary.get("FileActions.Unknown_File_Error_Title"), JOptionPane.ERROR_MESSAGE);
316 // Nothing else we can do.
317 }
318 catch(WriteNotPermittedException wnp_exception) {
319 Gatherer.printStackTrace(wnp_exception);
320 cancel_action = true;
321 // Show warning
322 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);
323 // Nothing else we can do.
324 }
325 catch(IOException exception) {
326 // Can't really do much about this.
327 Gatherer.printStackTrace(exception);
328 }
329 // If not cancelled
330 if(!cancel_action) {
331 // Step one is to create a dummy FileNode. Its important it has the correct structure so getPath works.
332 FileNode new_record = new FileNode(target_file);
333 SynchronizedTreeModelTools.insertNodeInto(target_model, destination_node, new_record);
334 new_node = new_record;
335
336 // create undo job
337 if(job.undoable) {
338 job.undoable = false;
339 if(job.type == FileJob.COPY) {
340 // A copy is undone with a delete, so it doesn't really matter where the file originally came from (we're not moving it back there, but into the recycle bin). You may also notice we don't make use of the target parent record. This is because no undo action needs this information, and even if it did it could simply ask for records parent!
341 Gatherer.c_man.undo.addUndo(job.ID(), UndoManager.FILE_COPY, null, null, job.target, new_record, job.undo);
342 }
343 else {
344 // Movements however do need a source and source parent so the file can be moved back to the correct place.
345 Gatherer.c_man.undo.addUndo(job.ID(), UndoManager.FILE_MOVE, job.source, (FileNode)origin_node.getParent(), job.target, new_record, job.undo);
346 }
347 }
348 new_record = null;
349 }
350 }
351 // Else
352 else if(source_file.isDirectory()) {
353 // create new record
354 FileNode directory_record = new FileNode(target_file);
355 ///ystem.err.println("Directory record = " + directory_record + " (" + target_file.getAbsolutePath() + ")");
356 SynchronizedTreeModelTools.insertNodeInto(target_model, destination_node, directory_record);
357 // Why is this not happening eh?
358 directory_record.setParent(destination_node);
359 if(!target_file.exists()) {
360 // make the directory
361 target_file.mkdirs();
362 new_node = directory_record;
363 // create undo job
364 if(job.undoable) {
365 job.undoable = false;
366 if(job.type == FileJob.COPY) {
367 // A copy is undone with a delete, so it doesn't really matter where the file originally came from (we're not moving it back there, but into the recycle bin). You may also notice we don't make use of the target parent record. This is because no undo action needs this information, and even if it did it could simply ask for records parent!
368 Gatherer.c_man.undo.addUndo(job.ID(), UndoManager.FILE_COPY, null, null, job.target, directory_record, job.undo);
369 }
370 else {
371 // Movements however do need a source and source parent so the file can be moved back to the correct place.
372 Gatherer.c_man.undo.addUndo(job.ID(), UndoManager.FILE_MOVE, job.source, (FileNode)origin_node.getParent(), job.target, directory_record, job.undo);
373 }
374 }
375 }
376 // Else inform the users that a directory already exists and files will be copied into it
377 //else {
378 // JOptionPane.showMessageDialog(null, Dictionary.get("FileActions.Directory_Exists", target_file.toString()), Dictionary.get("General.Warning"), JOptionPane.WARNING_MESSAGE);
379 //}
380 // 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.
381 FileNode child_record = null;
382 // 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.
383 // -- Starting queue ...[a]
384 // remove(position) = 'a' ...
385 // add(position, 'b') ...[b]
386 // add(position, 'c') ...[c][b]
387 // add(position, 'd') ...[d][c][b]
388 // Next loop
389 // remove(position) = 'b' ...[d][c]
390 for(int i = 0; i < origin_node.getChildCount(); i++) {
391 child_record = (FileNode) origin_node.getChildAt(i);
392 addJob(job.ID(), job.source, child_record, job.target, directory_record, job.type, job.undo, false, false, position);
393 }
394 child_record = null;
395 directory_record = null;
396 }
397 // The file wasn't found!
398 else {
399 cancel_action = true;
400 // Show warning.
401 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);
402 // Force refresh of source folder.
403 source_model.refresh(new TreePath(((FileNode)origin_node.getParent()).getPath()));
404 }
405
406 // We can't have been cancelled, and we must have created a new FileNode during the above phase, before we can handle metadata.
407 if(!cancel_action && new_node != null) {
408 /* Time to handle any existing metadata. */
409 // If the directory came from inside our collection...
410 if (job.source.toString().equals("Collection")) {
411 ///ystem.err.println("Move within collection...");
412 GDMManager gdm = Gatherer.c_man.getCollection().gdm;
413 // we just retrieve the metadata attached to the origin node...
414 ArrayList existing_metadata = gdm.getMetadataOnly(source_file);
415 //Gatherer.println("Existing metadata for " + origin_node + ": " + gdm.toString(existing_metadata));
416 // then assign this remainder to the new folder.
417 ///ystem.err.println("New metadata: " + gdm.toString(existing_metadata));
418 gdm.addMetadata(new_node, existing_metadata);
419 existing_metadata = null;
420 gdm = null;
421 }
422 // If it came from the recycle bin retrieve the metadata from there, once again remembering to account for inherited metadata
423 else if (job.source.toString().equals("Undo")) {
424 GDMManager gdm = Gatherer.c_man.getCollection().gdm;
425 // Retrieve metadata from the recycle bin
426 ArrayList existing_metadata = Gatherer.c_man.undo.getMetadata(source_file);
427 // then assign this remainder to the new folder.
428 gdm.addMetadata(new_node, existing_metadata);
429 existing_metadata = null;
430 gdm = null;
431 }
432 // Otherwise if it came from the workspace use the MSMs parsers to search for folder level metadata (such as metadata.xml or marc records).
433 else if (job.source.toString().equals("Workspace")) {
434 cancel_action = !Gatherer.c_man.getCollection().msm.searchForMetadata(new_node, origin_node, job.folder_level);
435 }
436 }
437 new_node = null;
438
439 }
440 }
441 // 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.
442 if(!cancel_action && ready && (job.type == FileJob.DELETE || job.type == FileJob.MOVE)) {
443 ///ystem.err.print("Delete/Move: " + origin_node + " -> ");
444 // If the source is an empty directory or a file. Don't do anything to the root node of a tree.
445 File[] child_list = source_file.listFiles();
446 if(source_file.isFile() || (child_list != null && (child_list.length == 0 || (child_list.length == 1 && child_list[0].getName().equals(Utility.METADATA_XML))) && origin_node.getParent() != null)) {
447 ///ystem.err.println("File or empty directory.");
448 // Delete any metadata.xml still in the directory.
449 if(child_list != null && child_list.length == 1) {
450 child_list[0].delete();
451 }
452
453 ///ystem.err.println("Origin is file or is directory and is empty.");
454 // update status area
455 String args[] = new String[1];
456 args[0] = "" + (queue.size() + 1) + "";
457 //job_status.setText(Dictionary.get("FileActions.Jobs", args));
458 args[0] = Utility.formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width);
459 file_status.setText(Dictionary.get("FileActions.Deleting", args));
460 args = null;
461
462 // Remove its metadata
463 ArrayList metadatum = null;
464 if(job.source == Gatherer.c_man.undo) {
465 Gatherer.c_man.undo.addMetadata(target_file, metadatum);
466 }
467 else {
468 metadatum = Gatherer.c_man.getCollection().gdm.removeMetadata(origin_node.getFile());
469 }
470 // determine its parent node
471 FileNode parent_record = (FileNode)origin_node.getParent();
472 // Remove from model
473 SynchronizedTreeModelTools.removeNodeFromParent(source_model, origin_node);
474 // If we are deleting
475 File recycled_file = null;
476 FileNode recycled_parent = null;
477 if(job.type == FileJob.DELETE) {
478
479 /** If we ever decide to reenable undo then this code will have to be seriously improved.
480 // See if this record already has a previous recycle folder noted in the recycle folder mappings.
481 recycled_parent = (FileNode) recycle_folder_mappings.get(origin_node);
482 if(recycled_parent != null) {
483 recycle_folder_mappings.remove(origin_node);
484 }
485 else {
486 recycled_parent = (FileNode) Gatherer.c_man.undo.getTreeModel().getRoot();
487 }
488 // This may be a directory which we have already added previously.
489 if(completed_folder_mappings.containsKey(origin_node)) {
490 FileNode recycled_record = (FileNode) completed_folder_mappings.get(origin_node);
491 // Replace the temporary directory record in the undo tree with this one.
492 SynchronizedTreeModelTools.replaceNode(Gatherer.c_man.undo.getTreeModel(), recycled_record, origin_node);
493 origin_node.setFile(recycled_record.getFile());
494 }
495 else {
496 // copy the file to the recycle bin
497 recycled_file = new File(recycled_parent.getFile(), origin_node.toString());
498 // If the file already exists, delete it.
499 if(recycled_file.exists()) {
500 recycled_file.delete();
501 }
502 recycled_file.deleteOnExit();
503 copyFile(source_file, recycled_file, progress);
504 origin_node.setFile(recycled_file);
505 // Add the node to the appropriate place in the UndoManagers tree model. Unfortunately this one does have the possibility of altering the GUI (if the removeNodeFromParent above hasn't occured yet), so we must put it on the queue.
506 SynchronizedTreeModelTools.insertNodeInto(Gatherer.c_man.undo.getTreeModel(), recycled_parent, origin_node);
507 }
508 Gatherer.c_man.undo.addMetadata(source_file, metadatum);
509 **/
510 }
511 // delete the source file
512 Utility.delete(source_file);
513 /**
514 // create undo job as necessary. File move would have been handled above.
515 if(job.undoable) {
516 job.undoable = false;
517 // The target is null, indicating that we moved this file to the recycle bin. The UndoManager will replace null with 'this'.
518 Gatherer.c_man.undo.addUndo(job.ID(), UndoManager.FILE_DELETE, job.source, parent_record, null, origin_node, job.undo);
519 }
520 recycled_parent = null;
521 recycled_file = null;
522 **/
523 //metadatum = null;
524 }
525 // Else the source is a directory and it has children remaining
526 else if(child_list != null && child_list.length > 0) {
527 ///ystem.err.print("Nonempty directory -> ");
528 ///ystem.err.println("Directory is non-empty. Remove children first.");
529 FileNode recycle_folder_record = null;
530 // Don't worry about all this for true file move actions.
531 if(job.type == FileJob.DELETE) {
532 /** Again, if we reenable undo then this code must be debugged.
533 // See if this record already has a previous recycle folder noted in the recycle folder mappings.
534 FileNode parent = (FileNode) recycle_folder_mappings.get(origin_node);
535 if(parent != null) {
536 recycle_folder_mappings.remove(origin_node);
537 }
538 else {
539 parent = (FileNode) Gatherer.c_man.undo.getTreeModel().getRoot();
540 }
541 // We must add the folder node to our undo manager tree now, so we'll have something to add children to. We use a copy of the directory file record.
542 File recycle_folder = new File(parent.getFile(), origin_node.toString());
543 recycle_folder.deleteOnExit();
544 recycle_folder.mkdirs();
545 recycle_folder_record = new FileNode(recycle_folder);
546 // Add this node to the undo tree model.
547 SynchronizedTreeModelTools.insertNodeInto(Gatherer.c_man.undo.getTreeModel(), parent, recycle_folder_record);
548 // We add an entry to the complete mappings to ensure this directory isn't added again
549 completed_folder_mappings.put(origin_node, recycle_folder_record);
550 ///ystem.err.println("Added completed directories mapping " + origin_node);
551 **/
552 // 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.
553 ///ystem.err.println("Directory has " + origin_node.getChildCount() + " children.");
554 ///ystem.err.println("Directory actually has " + child_list.length + " children.");
555 origin_node.unmap();
556 origin_node.map();
557 ///ystem.err.println("Directory has " + origin_node.getChildCount() + " children.");
558 ///ystem.err.println("Directory actually has " + child_list.length + " children.");
559 for(int i = 0; i < origin_node.getChildCount(); i++) {
560 FileNode child_record = (FileNode) origin_node.getChildAt(i);
561 addJob(job.ID(), job.source, child_record, job.target, destination_node, FileJob.DELETE, job.undo, false, false, position);
562 //if(recycle_folder_record != null) {
563 // recycle_folder_mappings.put(child_record, recycle_folder_record);
564 //}
565 }
566
567 }
568 // 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.
569 // One special case. Do not requeue root nodes. Don't requeue jobs marked as done.
570 if(origin_node.getParent() != null && !job.done) {
571 System.err.println("Requeuing: " + origin_node.getFile().getAbsolutePath());
572 job.type = FileJob.DELETE; // You only requeue jobs that are deletes, as directories must be inspected before children, but deleted after.
573 addJob(job, position);
574 }
575 else {
576 System.err.println("I've already done this job twice. I refuse to requeue it again!!!");
577 }
578 }
579 }
580 job = null;
581 source_file = null;
582 target_file = null;
583 origin_node = null;
584 // We can only break out of the while loop if we are out of files, or if the action was cancelled.
585 if(cancel_action) {
586 // Empty queue
587 clearJobs();
588 cancel_action = false;
589 }
590 // Debugging pause.
591 ///ystem.err.println("Job complete.");
592 }
593 else {
594 synchronized(this) {
595 ///ystem.err.println("Queue size = " + queue.size());
596 // Force the trees to refresh (but only if there is something on screen!)
597 if(Gatherer.g_man != null) {
598 Gatherer.g_man.refreshTrees(DragTree.COLLECTION_CONTENTS_CHANGED);
599 }
600 // Reset status area
601 //job_status.setText(Dictionary.get("No_Selection"));
602 file_status.setText(Dictionary.get("FileActions.No_Activity"));
603 progress.reset();
604 progress.setString(Dictionary.get("FileActions.No_Activity"));
605 yes_to_all = false;
606 completed_folder_mappings.clear();
607 recycle_folder_mappings.clear();
608 // Now wait if applicable.
609 if(return_immediately) {
610 return;
611 }
612 ///ystem.err.println("Waiting");
613 wait();
614 }
615 }
616 }
617 catch (Exception error) {
618 Gatherer.printStackTrace(error);
619 }
620 }
621 }
622 /** A second lock is necessary to prevent the thread waiting, because its notify occured momentarily before it waited. If the flag is set then the thread can't wait, but loops around. This may chew processor time so should only be used if you are certain files are about to be placed on the queue.
623 * @param wait_allowed The new state of the wait_allowed flag, as a boolean.
624 */
625 /* private void setWaitAllowed(boolean wait_allowed) {
626 this.wait_allowed = wait_allowed;
627 } */
628 /** Restore the progress bar so that it updates normally.
629 * @see org.greenstone.gatherer.gui.LongProgressBar
630 */
631 /* synchronized private void unpause() {
632 progress.setIndeterminate(false);
633 if(previous != null) {
634 progress.setString(previous);
635 previous = null;
636 }
637 } */
638
639 /** Called when the user makes some selection in one of the trees we are listening to. From this we update the status details. */
640 public void valueChanged(TreeSelectionEvent event) {
641 JTree tree = (JTree) event.getSource();
642 if(tree.getSelectionCount() > 0) {
643 TreePath selection[] = tree.getSelectionPaths();
644 int file_count = 0;
645 int dir_count = 0;
646 for(int i = 0; i < selection.length; i++) {
647 TreeNode record = (TreeNode) selection[i].getLastPathComponent();
648 if(record.isLeaf()) {
649 file_count++;
650 }
651 else {
652 dir_count++;
653 }
654 record = null;
655 }
656 selection = null;
657 // String args[] = new String[2];
658 // args[0] = "" + file_count + "";
659 // args[1] = "" + dir_count + "";
660 // job_status.setText(get("Selected", args));
661 // args = null;
662 }
663 tree = null;
664 }
665
666 synchronized private void clearJobs() {
667 queue.clear();
668 }
669
670 /** Copy a file from the source location to the destination location.
671 * @param source The source File.
672 * @param destination The destination File.
673 * @see org.greenstone.gatherer.Gatherer
674 */
675 public void copyFile(File source, File destination, LongProgressBar progress)
676 throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, UnknownFileErrorException, WriteNotPermittedException {
677 if(source.isDirectory()) {
678 destination.mkdirs();
679 }
680 else {
681 // Check if the origin file exists.
682 if(!source.exists()) {
683 System.err.println("Couldn't find the source file.");
684 throw(new FileNotFoundException());
685 }
686 // Check if the destination file does not exist.
687 if(destination.exists()) {
688 throw(new FileAlreadyExistsException());
689 }
690 File dirs = destination.getParentFile();
691 dirs.mkdirs();
692 // Copy the file.
693 FileInputStream f_in = new FileInputStream(source);
694 FileOutputStream f_out = null;
695 // This may throw a file not found exception, but this translates to a WriteNotPermittedException, in this case
696 try {
697 f_out = new FileOutputStream(destination);
698 }
699 catch (FileNotFoundException exception) {
700 throw new WriteNotPermittedException(exception.toString());
701 }
702 byte data[] = new byte[Utility.BUFFER_SIZE];
703 int data_size = 0;
704 while((data_size = f_in.read(data, 0, Utility.BUFFER_SIZE)) != -1 && !cancel_action) {
705 long destination_size = destination.length();
706 try {
707 f_out.write(data, 0, data_size);
708 }
709 // 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.
710 catch (IOException io_exception) {
711 if(destination_size + (long) data_size > destination.length()) {
712 // Determine the difference (which I guess is in bytes).
713 long difference = (destination_size + (long) data_size) - destination.length();
714 // Transform that into a human readable string.
715 String message = Utility.formatFileLength(difference);
716 throw(new InsufficientSpaceException(message));
717 }
718 else {
719 throw(io_exception);
720 }
721 }
722 if(progress != null) {
723 progress.addValue(data_size);
724 }
725 }
726 // Flush and close the streams to ensure all bytes are written.
727 f_in.close();
728 f_out.close();
729 // We have now, in theory, produced an exact copy of the source file. Check this by comparing sizes.
730 if(!destination.exists() || (!cancel_action && source.length() != destination.length())) {
731 throw(new UnknownFileErrorException());
732 }
733 // If we were cancelled, ensure that none of the destination file exists.
734 if(cancel_action) {
735 destination.delete();
736 }
737 }
738 }
739
740
741 private FileJob removeJob(int position) {
742 FileJob job = null;
743 if(queue.size() > 0) {
744 job = (FileJob) queue.remove(position);
745 }
746 return job;
747 }
748}
Note: See TracBrowser for help on using the repository browser.