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

Last change on this file since 4469 was 4469, checked in by kjdon, 21 years ago

commented out job_status stuff - not used by anything, changed file_status to a JLabel instead of a SmudgyLabel, and removed all teh tool tips from file_status - they only said the same thing anyway.

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