001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.data;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.io.File;
007    import java.io.IOException;
008    import java.util.ArrayList;
009    import java.util.Date;
010    import java.util.Deque;
011    import java.util.HashSet;
012    import java.util.Iterator;
013    import java.util.LinkedList;
014    import java.util.List;
015    import java.util.Set;
016    import java.util.Timer;
017    import java.util.TimerTask;
018    import java.util.regex.Pattern;
019    
020    import org.openstreetmap.josm.Main;
021    import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
022    import org.openstreetmap.josm.data.osm.DataSet;
023    import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
024    import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
025    import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
026    import org.openstreetmap.josm.data.preferences.BooleanProperty;
027    import org.openstreetmap.josm.data.preferences.IntegerProperty;
028    import org.openstreetmap.josm.gui.MapView;
029    import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
030    import org.openstreetmap.josm.gui.layer.Layer;
031    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
032    import org.openstreetmap.josm.io.OsmExporter;
033    
034    /**
035     * Saves data layers periodically so they can be recovered in case of a crash.
036     *
037     * There are 2 directories
038     *  - autosave dir: copies of the currently open data layers are saved here every
039     *      PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
040     *      files are removed. If this dir is non-empty on start, JOSM assumes
041     *      that it crashed last time.
042     *  - deleted layers dir: "secondary archive" - when autosaved layers are restored
043     *      they are copied to this directory. We cannot keep them in the autosave folder,
044     *      but just deleting it would be dangerous: Maybe a feature inside the file
045     *      caused JOSM to crash. If the data is valuable, the user can still try to
046     *      open with another versions of JOSM or fix the problem manually.
047     *
048     *      The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
049     */
050    public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
051    
052        private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' };
053        private static final String AUTOSAVE_DIR = "autosave";
054        private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
055    
056        public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
057        public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
058        public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
059        public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60);
060        public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
061    
062        private static class AutosaveLayerInfo {
063            OsmDataLayer layer;
064            String layerName;
065            String layerFileName;
066            final Deque<File> backupFiles = new LinkedList<File>();
067        }
068    
069        private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
070        private Set<DataSet> changedDatasets = new HashSet<DataSet>();
071        private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>();
072        private Timer timer;
073        private final Object layersLock = new Object();
074        private final Deque<File> deletedLayers = new LinkedList<File>();
075    
076        private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR);
077        private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR);
078    
079        public void schedule() {
080            if (PROP_INTERVAL.get() > 0) {
081    
082                if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
083                    System.out.println(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
084                    return;
085                }
086                if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
087                    System.out.println(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
088                    return;
089                }
090    
091                for (File f: deletedLayersDir.listFiles()) {
092                    deletedLayers.add(f); // FIXME: sort by mtime
093                }
094    
095                timer = new Timer(true);
096                timer.schedule(this, 1000, PROP_INTERVAL.get() * 1000);
097                MapView.addLayerChangeListener(this);
098                if (Main.isDisplayingMapView()) {
099                    for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) {
100                        registerNewlayer(l);
101                    }
102                }
103            }
104        }
105    
106        private String getFileName(String layerName, int index) {
107            String result = layerName;
108            for (int i=0; i<ILLEGAL_CHARACTERS.length; i++) {
109                result = result.replaceAll(Pattern.quote(String.valueOf(ILLEGAL_CHARACTERS[i])),
110                        '&' + String.valueOf((int)ILLEGAL_CHARACTERS[i]) + ';');
111            }
112            if (index != 0) {
113                result = result + '_' + index;
114            }
115            return result;
116        }
117    
118        private void setLayerFileName(AutosaveLayerInfo layer) {
119            int index = 0;
120            while (true) {
121                String filename = getFileName(layer.layer.getName(), index);
122                boolean foundTheSame = false;
123                for (AutosaveLayerInfo info: layersInfo) {
124                    if (info != layer && filename.equals(info.layerFileName)) {
125                        foundTheSame = true;
126                        break;
127                    }
128                }
129    
130                if (!foundTheSame) {
131                    layer.layerFileName = filename;
132                    return;
133                }
134    
135                index++;
136            }
137        }
138    
139        private File getNewLayerFile(AutosaveLayerInfo layer) {
140            int index = 0;
141            Date now = new Date();
142            while (true) {
143                File result = new File(autosaveDir, String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%3$s.osm", layer.layerFileName, now, index == 0?"":"_" + index));
144                try {
145                    if (result.createNewFile())
146                        return result;
147                    else {
148                        System.out.println(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
149                        if (index > PROP_INDEX_LIMIT.get())
150                            throw new IOException("index limit exceeded");
151                    }
152                } catch (IOException e) {
153                    System.err.println(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()));
154                    return null;
155                }
156                index++;
157            }
158        }
159    
160        private void savelayer(AutosaveLayerInfo info) throws IOException {
161            if (!info.layer.getName().equals(info.layerName)) {
162                setLayerFileName(info);
163                info.layerName = info.layer.getName();
164            }
165            if (changedDatasets.contains(info.layer.data)) {
166                File file = getNewLayerFile(info);
167                if (file != null) {
168                    info.backupFiles.add(file);
169                    new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
170                }
171            }
172            while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
173                File oldFile = info.backupFiles.remove();
174                if (!oldFile.delete()) {
175                    System.out.println(tr("Unable to delete old backup file {0}", oldFile.getAbsolutePath()));
176                }
177            }
178        }
179    
180        @Override
181        public void run() {
182            synchronized (layersLock) {
183                try {
184                    for (AutosaveLayerInfo info: layersInfo) {
185                        savelayer(info);
186                    }
187                    changedDatasets.clear();
188                } catch (Throwable t) {
189                    // Don't let exception stop time thread
190                    System.err.println("Autosave failed: ");
191                    t.printStackTrace();
192                }
193            }
194        }
195    
196        @Override
197        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
198            // Do nothing
199        }
200    
201        private void registerNewlayer(OsmDataLayer layer) {
202            synchronized (layersLock) {
203                layer.data.addDataSetListener(datasetAdapter);
204                AutosaveLayerInfo info = new AutosaveLayerInfo();
205                info.layer = layer;
206                layersInfo.add(info);
207            }
208        }
209    
210        @Override
211        public void layerAdded(Layer newLayer) {
212            if (newLayer instanceof OsmDataLayer) {
213                registerNewlayer((OsmDataLayer) newLayer);
214            }
215        }
216    
217        @Override
218        public void layerRemoved(Layer oldLayer) {
219            if (oldLayer instanceof OsmDataLayer) {
220                synchronized (layersLock) {
221                    OsmDataLayer osmLayer = (OsmDataLayer) oldLayer;
222                    osmLayer.data.removeDataSetListener(datasetAdapter);
223                    Iterator<AutosaveLayerInfo> it = layersInfo.iterator();
224                    while (it.hasNext()) {
225                        AutosaveLayerInfo info = it.next();
226                        if (info.layer == osmLayer) {
227    
228                            try {
229                                savelayer(info);
230                                File lastFile = info.backupFiles.pollLast();
231                                if (lastFile != null) {
232                                    moveToDeletedLayersFolder(lastFile);
233                                }
234                                for (File file: info.backupFiles) {
235                                    file.delete();
236                                }
237                            } catch (IOException e) {
238                                System.err.println(tr("Error while creating backup of removed layer: {0}", e.getMessage()));
239                            }
240    
241                            it.remove();
242                        }
243                    }
244                }
245            }
246        }
247    
248        @Override
249        public void processDatasetEvent(AbstractDatasetChangedEvent event) {
250            changedDatasets.add(event.getDataset());
251        }
252    
253        public List<File> getUnsavedLayersFiles() {
254            List<File> result = new ArrayList<File>();
255            File[] files = autosaveDir.listFiles();
256            if (files == null)
257                return result;
258            for (File file: files) {
259                if (file.isFile()) {
260                    result.add(file);
261                }
262            }
263            return result;
264        }
265    
266        public void recoverUnsavedLayers() {
267            List<File> files = getUnsavedLayersFiles();
268            final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
269            Main.worker.submit(openFileTsk);
270            Main.worker.submit(new Runnable() {
271                public void run() {
272                    for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
273                        moveToDeletedLayersFolder(f);
274                    }
275                }
276            });
277        }
278    
279        /**
280         * Move file to the deleted layers directory.
281         * If moving does not work, it will try to delete the file directly.
282         * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
283         * some files in the deleted layers directory will be removed.
284         *
285         * @param f the file, usually from the autosave dir
286         */
287        private void moveToDeletedLayersFolder(File f) {
288            File backupFile = new File(deletedLayersDir, f.getName());
289    
290            if (backupFile.exists()) {
291                deletedLayers.remove(backupFile);
292                if (!backupFile.delete()) {
293                    System.err.println(String.format("Warning: Could not delete old backup file %s", backupFile));
294                }
295            }
296            if (f.renameTo(backupFile)) {
297                deletedLayers.add(backupFile);
298            } else {
299                System.err.println(String.format("Warning: Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
300                // we cannot move to deleted folder, so just try to delete it directly
301                if (!f.delete()) {
302                    System.err.println(String.format("Warning: Could not delete backup file %s", f));
303                }
304            }
305            while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
306                File next = deletedLayers.remove();
307                if (next == null) {
308                    break;
309                }
310                if (!next.delete()) {
311                    System.err.println(String.format("Warning: Could not delete archived backup file %s", next));
312                }
313            }
314        }
315    
316        public void dicardUnsavedLayers() {
317            for (File f: getUnsavedLayersFiles()) {
318                moveToDeletedLayersFolder(f);
319            }
320        }
321    }