001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
004 *
005 * This program 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 * This program 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 */
018package org.syncany.operations.down;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.List;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import org.syncany.database.FileVersion.FileType;
028import org.syncany.operations.down.actions.DeleteFileSystemAction;
029import org.syncany.operations.down.actions.FileSystemAction;
030import org.syncany.operations.down.actions.NewFileSystemAction;
031import org.syncany.operations.down.actions.RenameFileSystemAction;
032
033/**
034 * Sorts file system actions according to their natural order to prevent scenarios 
035 * in which a non-empty directory is deleted, ...
036 * 
037 * Sorting necessary:
038 * 1. Delete file/symlink actions must happen first
039 * 2. New folder actions must happen after delete file/symlink actions, and must be sorted by path, shortest first (within new actions)
040 * 3. New file/symlink actions must happen after new folder actions 
041 * 4. Rename file/symlink actions must happen after new folder actions
042 * 5. Delete folder actions must happen last, sorted by path, longest first (within delete actions)
043 *    a. except if the deleted folder has a name clash with another action (rename/new)
044 *       in that case, it must happen before the other action
045 * 
046 * No sorting necessary:
047 * - Change file/symlink actions can happen anytime
048 * - Set attributes actions can happen anytime
049 * 
050 * Cannot happen:
051 * - Rename folder
052 * - Change folder
053 * 
054 * 
055 * TODO [medium] NewSymlinkFileSystemAction → NewFile (has no content)
056 */
057public class FileSystemActionComparator  {
058        private static final Logger logger = Logger.getLogger(FileSystemActionComparator.class.getSimpleName());
059        
060        private static final Object[][] TARGET_ORDER =  new Object[][] {
061                new Object[] { DeleteFileSystemAction.class, FileType.FILE }, 
062                new Object[] { DeleteFileSystemAction.class, FileType.SYMLINK }, 
063                new Object[] { NewFileSystemAction.class, FileType.FOLDER },
064                new Object[] { NewFileSystemAction.class, FileType.FILE },
065                new Object[] { NewFileSystemAction.class, FileType.SYMLINK },
066                new Object[] { RenameFileSystemAction.class, FileType.FILE },
067                new Object[] { RenameFileSystemAction.class, FileType.SYMLINK },
068                new Object[] { DeleteFileSystemAction.class, FileType.FOLDER } 
069        };
070        
071        private InternalFileSystemActionComparator internalComparator = new InternalFileSystemActionComparator();
072                        
073        public void sort(List<FileSystemAction> actions) {
074                Collections.sort(actions, internalComparator);
075                postCompareSort(actions);
076                
077                if (logger.isLoggable(Level.INFO)) {
078                        logger.log(Level.INFO, "   Sorted actions:");
079                        
080                        for (FileSystemAction action : actions) {
081                                logger.log(Level.INFO, "   + "+action);
082                        }
083                }
084        }
085        
086        /**
087         * Fixes the case in which a folder has been swapped with a file (case 5a, see above)
088         * 
089         * New/Renamed file system actions must happen *after* the file was deleted. This method
090         * moves new/renamed actions below deleted folder actions. 
091         * 
092         * DEL  FILE     file1.jpg      
093         * NEW  FOLDER   folder1
094         * NEW  FILE     folder1/fileinfolder1.jpg                       ← No Issue, but needs to be checked
095         * NEW  FILE     folder2                                         ←←← Issue, because folder2 is deleted below!
096         * NEW  SYMLINK  folder1/symlinkinfolder1.jpg                    ← No Issue, but needs to be checked
097         * REN  FILE     folder2/fileinfolder2.jpg → folder1/x2.jpg
098         * REN  SYMLINK  folder2/symlinkinfolder2.jpg → folder1/sym2
099         * DEL  FOLDER   folder2
100         *                                                               ←←← New position of "NEW FILE folder2"!
101         */
102        public void postCompareSort(List<FileSystemAction> actions) {
103                List<FileSystemAction> fixedActions = new ArrayList<FileSystemAction>(actions);
104                
105                int i = 0;
106                
107                while (i < fixedActions.size()) {
108                        FileSystemAction currentAction = fixedActions.get(i);
109                        
110                        if (currentAction instanceof DeleteFileSystemAction && currentAction.getType() == FileType.FOLDER) {
111                                break;
112                        }
113                        
114                        //System.out.println("testing "+currentAction);
115                        if ((currentAction instanceof NewFileSystemAction || currentAction instanceof RenameFileSystemAction)
116                                        && (currentAction.getType() == FileType.FILE || currentAction.getType() == FileType.SYMLINK)) {
117                                        
118                                int conflictingDeleteActionIndex = getPathConflictingWithDeleteActionIndex(currentAction, fixedActions);
119                                
120                                if (conflictingDeleteActionIndex >= 0) {
121                                        logger.log(Level.INFO, "     --> match, conflict ["+i+"]: "+currentAction);
122                                        logger.log(Level.INFO, "                    with ["+conflictingDeleteActionIndex+"]: "+fixedActions.get(conflictingDeleteActionIndex));
123                                        
124                                        fixedActions.remove(i);
125                                        fixedActions.add(conflictingDeleteActionIndex, currentAction);
126                                        
127                                        i--; // fix counter!
128                                }
129                        }
130                        
131                        i++;
132                }               
133                
134                // Replace
135                actions.clear();
136                actions.addAll(fixedActions);
137        }
138                        
139        private int getPathConflictingWithDeleteActionIndex(FileSystemAction currentAction, List<FileSystemAction> deleteFolderActions) {
140                for (int i=0; i<deleteFolderActions.size(); i++) {
141                        FileSystemAction action = deleteFolderActions.get(i);
142
143                        if (action instanceof DeleteFileSystemAction && action.getType() == FileType.FOLDER) {                  
144                                if (action.getFile2().getPath().equals(currentAction.getFile2().getPath())) {
145                                        return i;
146                                }
147                        }
148                }
149                
150                return -1;
151        }
152
153        private class InternalFileSystemActionComparator implements Comparator<FileSystemAction> {
154                @Override
155                public int compare(FileSystemAction a1, FileSystemAction a2) {
156                        int a1Position = determinePosition(a1);
157                        int a2Position = determinePosition(a2);
158                        
159                        if (a1Position > a2Position) {
160                                return 1;
161                        }
162                        else if (a1Position < a2Position) {
163                                return -1;
164                        }
165                        
166                        return compareByFullName(a1, a2);
167                }
168                                
169                private int compareByFullName(FileSystemAction a1, FileSystemAction a2) {
170                        // For deleted, do the longest path first
171                        if (a1.getClass().equals(DeleteFileSystemAction.class)) {
172                                return -1 * a1.getFile2().getPath().compareTo(a2.getFile2().getPath());
173                        }
174                        
175                        // For new folder actions, do the shortest path first
176                        else if (a1.getClass().equals(NewFileSystemAction.class)) {
177                                return a1.getFile2().getPath().compareTo(a2.getFile2().getPath());
178                        }
179                        
180                        return 0;
181                }
182
183                private int determinePosition(FileSystemAction a) {
184                        for (int i=0; i<TARGET_ORDER.length; i++) {
185                                Class<?> targetClass = (Class<?>) TARGET_ORDER[i][0];
186                                FileType targetFileType = (FileType) TARGET_ORDER[i][1];
187                                
188                                if (a.getClass().equals(targetClass) && a.getType() == targetFileType) {                                        
189                                        return i;
190                                }
191                        }
192                        
193                        return -1;
194                }
195        }
196        
197}