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 | */
|
---|
27 | package org.greenstone.gatherer.file;
|
---|
28 |
|
---|
29 | import java.io.*;
|
---|
30 | import java.util.*;
|
---|
31 | import javax.swing.*;
|
---|
32 | import javax.swing.event.*;
|
---|
33 | import javax.swing.tree.*;
|
---|
34 | import org.greenstone.gatherer.Configuration;
|
---|
35 | import org.greenstone.gatherer.Dictionary;
|
---|
36 | import org.greenstone.gatherer.Gatherer;
|
---|
37 | import org.greenstone.gatherer.file.FileJob;
|
---|
38 | import org.greenstone.gatherer.file.FileNode;
|
---|
39 | import org.greenstone.gatherer.gui.LongProgressBar;
|
---|
40 | import org.greenstone.gatherer.gui.tree.DragTree;
|
---|
41 | import org.greenstone.gatherer.msm.GDMManager;
|
---|
42 | import org.greenstone.gatherer.undo.UndoManager;
|
---|
43 | import org.greenstone.gatherer.util.ArrayTools;
|
---|
44 | import org.greenstone.gatherer.util.DragComponent;
|
---|
45 | import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
|
---|
46 | import 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 | */
|
---|
52 | public 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 | }
|
---|