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