001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.job;
020    
021    import com.mucommander.file.AbstractFile;
022    import com.mucommander.file.AbstractRWArchiveFile;
023    import com.mucommander.file.util.FileSet;
024    import com.mucommander.text.Translator;
025    import com.mucommander.ui.dialog.file.FileCollisionDialog;
026    import com.mucommander.ui.dialog.file.ProgressDialog;
027    import com.mucommander.ui.dialog.file.RenameDialog;
028    import com.mucommander.ui.main.MainFrame;
029    
030    import java.io.IOException;
031    
032    /**
033     * This class is the parent class of {@link com.mucommander.job.CopyJob} and {@link com.mucommander.job.MoveJob} and
034     * allows them to share methods and fields.
035     *
036     * @author Maxence Bernard, Mariusz Jakubowski
037     * @see com.mucommander.job.CopyJob
038     * @see com.mucommander.job.MoveJob
039     */
040    public abstract class AbstractCopyJob extends TransferFileJob {
041        
042        /** Base destination folder */
043        protected AbstractFile baseDestFolder;
044        
045        /** New filename in destination */
046        protected String newName;
047    
048        /** Default choice when encountering an existing file */
049        protected int defaultFileExistsAction = FileCollisionDialog.ASK_ACTION;
050        
051        /** Title used for error dialogs */
052        protected String errorDialogTitle;
053        
054        protected boolean append;
055        
056        /** The archive that contains the destination files (may be null) */
057        protected AbstractRWArchiveFile archiveToOptimize;
058    
059        /** True when an archive is being optimized */
060        protected boolean isOptimizingArchive;
061    
062        /**
063         * Creates a new <code>AbstractCopyJob</code>.
064         *
065         * @param progressDialog dialog which shows this job's progress
066         * @param mainFrame mainFrame this job has been triggered by
067         * @param files files which are going to be copied
068         * @param destFolder destination folder where the files will be copied
069         * @param newName the new filename in the destination folder, can be <code>null</code> in which case the original filename will be used.
070         * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values
071         */
072        public AbstractCopyJob(ProgressDialog progressDialog, MainFrame mainFrame,
073                FileSet files, AbstractFile destFolder, String newName, int fileExistsAction) {
074            super(progressDialog, mainFrame, files);
075    
076            this.baseDestFolder = destFolder;
077            this.newName = newName;        
078            this.defaultFileExistsAction = fileExistsAction;
079        }
080    
081        /**
082         * Creates a destination file given a destination folder and a new file name.
083         * @param destFolder a destination folder
084         * @param destFileName a destination file name
085         * @return the destination file or null if it cannot be created
086         */
087        protected AbstractFile createDestinationFile(AbstractFile destFolder,
088                String destFileName) {
089            AbstractFile destFile;
090            do {    // Loop for retry
091                try {
092                    destFile = destFolder.getDirectChild(destFileName);
093                    break;
094                }
095                catch(IOException e) {
096                    // Destination file couldn't be instanciated
097    
098                    int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_write_file", destFileName));
099                    // Retry loops
100                    if(ret==RETRY_ACTION)
101                        continue;
102                    // Cancel or close dialog return false
103                    return null;
104                    // Skip continues
105                }
106            } while(true);
107            return destFile;
108        }
109        
110        /**
111         * Checks if there is a file collision (file exists in the destination).
112         * If there is no collision this method returns destFile.
113         * If there is a collision this method returns: <ul>
114         *  <li>null if a user cancelled the transfer 
115         *  <li>null if a user skipped the file
116         *  <li>destFile if a user resumed the transfer (and sets append flag)
117         *  <li>destFile if a user has chosen to overwrite the file
118         *  <li>new file if a user renamed the file
119         *  </ul>
120         * @param file a source file
121         * @param destFolder a destination folder
122         * @param destFile a destination file
123         * @param allowCaseVariation if true,
124         * @return destFile the new destination file
125         */
126        protected AbstractFile checkForCollision(AbstractFile file, AbstractFile destFolder, AbstractFile destFile, boolean allowCaseVariation) {
127            append = false;
128            while (true) {
129                // Check for file collisions (file exists in the destination, destination subfolder of source, ...)
130                // if a default action hasn't been specified
131                int collision = FileCollisionChecker.checkForCollision(file, destFile);
132                
133                // If allowCaseVariation is true and both files are equal, test if the destination filename is a variation
134                // of the original filename with a different case. If that is the case, do not warn about the source and
135                // destination being the same.
136                if(allowCaseVariation && collision==FileCollisionChecker.SAME_SOURCE_AND_DESTINATION) {
137                    String sourceFileName = file.getName();
138                    String destFileName = destFile.getName();
139                    if(sourceFileName.equalsIgnoreCase(destFileName) && !sourceFileName.equals(destFileName))
140                        break;
141                }
142                
143                // Handle collision, asking the user what to do or using a default action to resolve the collision 
144                if(collision != FileCollisionChecker.NO_COLLOSION) {
145                    int choice;
146                    // Use default action if one has been set, if not show up a dialog
147                    if(defaultFileExistsAction==FileCollisionDialog.ASK_ACTION) {
148                        FileCollisionDialog dialog = new FileCollisionDialog(progressDialog, mainFrame, collision, file, destFile, true, true);
149                        choice = waitForUserResponse(dialog);
150                        // If 'apply to all' was selected, this choice will be used for any other files (user will not be asked again)
151                        if(dialog.applyToAllSelected())
152                            defaultFileExistsAction = choice;
153                    }
154                    else
155                        choice = defaultFileExistsAction;
156        
157                    // Cancel, skip or close dialog
158                    if (choice==-1 || choice== FileCollisionDialog.CANCEL_ACTION) {
159                        interrupt();
160                        return null;
161                    }
162                    // Skip file
163                    else if (choice== FileCollisionDialog.SKIP_ACTION) {
164                        return null;
165                    }
166                    // Append to file (resume file copy)
167                    else if (choice== FileCollisionDialog.RESUME_ACTION) {
168                        append = true;
169                        break;
170                    }
171                    // Overwrite file
172                    else if (choice== FileCollisionDialog.OVERWRITE_ACTION) {
173                        // Do nothing, simply continue
174                        break;
175                    }
176                    //  Overwrite file if destination is older
177                    else if (choice== FileCollisionDialog.OVERWRITE_IF_OLDER_ACTION) {
178                        // Overwrite if file is newer (stricly)
179                        if(file.getDate()<=destFile.getDate())
180                            return null;
181                        break;
182                    } else if (choice == FileCollisionDialog.RENAME_ACTION) {
183                        setPaused(true);
184                        RenameDialog dlg = new RenameDialog(mainFrame, destFile);
185                        setPaused(false);
186                        String destFileName = dlg.getNewName();
187                        if (destFileName != null) {
188                            destFile = createDestinationFile(destFolder, destFileName);
189                        } else {
190                            // turn on FileCollisionDialog, so we don't loop indefinitely
191                            defaultFileExistsAction = FileCollisionDialog.ASK_ACTION;
192                        }
193                        // continue with collision checking
194                        continue;
195                    }
196                }
197                break;    // no collision
198            }
199            return destFile;
200        }
201        
202        /**
203         * Optimizes the given writable archive file and notifies the user in case of an error.
204         *
205         * @param rwArchiveFile the writable archive file to optimize
206         */
207        protected void optimizeArchive(AbstractRWArchiveFile rwArchiveFile) {
208            isOptimizingArchive = true;
209    
210            while(true) {
211                try {
212                    archiveToOptimize = rwArchiveFile;
213                    archiveToOptimize.optimizeArchive();
214    
215                    break;
216                }
217                catch(IOException e) {
218                    if(showErrorDialog(errorDialogTitle, Translator.get("error_while_optimizing_archive", rwArchiveFile.getName()))==RETRY_ACTION)
219                        continue;
220    
221                    break;
222                }
223            }
224    
225            isOptimizingArchive = false;
226        }
227    
228    }