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

Last change on this file since 6540 was 6539, checked in by jmt12, 20 years ago

Heres a bunch of other changed files. If it wasn't a Friday afternoon I might be bothered finding out what I actually changed in them. Such changes include: a new option or three on preferences, a bug fix for the GDM classes, several changes to CDM to allow for G2.39 configuration files, a fix to Codec to allow for quotes in format strings and more work on CommandTokenizer to allow for stupid, stupid, stupid collectionextra's starting with speech marks then a new line. Plus other stuff. And things. Peace Out.

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