source: josm/trunk/src/org/openstreetmap/josm/gui/layer/AutosaveTask.java @ 13114

Last change on this file since 13114 was 13114, checked in by Don-vip, 15 months ago

fix #11537 - Autosave also note layer

  • Property svn:eol-style set to native
File size: 19.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.BufferedReader;
8import java.io.File;
9import java.io.FileFilter;
10import java.io.IOException;
11import java.io.PrintStream;
12import java.lang.management.ManagementFactory;
13import java.nio.charset.StandardCharsets;
14import java.nio.file.Files;
15import java.nio.file.Path;
16import java.util.ArrayList;
17import java.util.Date;
18import java.util.Deque;
19import java.util.HashSet;
20import java.util.Iterator;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24import java.util.Timer;
25import java.util.TimerTask;
26import java.util.concurrent.ExecutionException;
27import java.util.concurrent.Future;
28import java.util.concurrent.TimeUnit;
29import java.util.regex.Pattern;
30
31import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
32import org.openstreetmap.josm.data.osm.DataSet;
33import org.openstreetmap.josm.data.osm.NoteData;
34import org.openstreetmap.josm.data.osm.NoteData.NoteDataUpdateListener;
35import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
36import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
37import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
38import org.openstreetmap.josm.data.preferences.BooleanProperty;
39import org.openstreetmap.josm.data.preferences.IntegerProperty;
40import org.openstreetmap.josm.gui.MainApplication;
41import org.openstreetmap.josm.gui.Notification;
42import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
43import org.openstreetmap.josm.gui.io.importexport.NoteImporter;
44import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
45import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
46import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
47import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
48import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
49import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
50import org.openstreetmap.josm.gui.util.GuiHelper;
51import org.openstreetmap.josm.spi.preferences.Config;
52import org.openstreetmap.josm.tools.Logging;
53import org.openstreetmap.josm.tools.Utils;
54
55/**
56 * Saves data and note layers periodically so they can be recovered in case of a crash.
57 *
58 * There are 2 directories
59 *  - autosave dir: copies of the currently open data layers are saved here every
60 *      PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
61 *      files are removed. If this dir is non-empty on start, JOSM assumes
62 *      that it crashed last time.
63 *  - deleted layers dir: "secondary archive" - when autosaved layers are restored
64 *      they are copied to this directory. We cannot keep them in the autosave folder,
65 *      but just deleting it would be dangerous: Maybe a feature inside the file
66 *      caused JOSM to crash. If the data is valuable, the user can still try to
67 *      open with another versions of JOSM or fix the problem manually.
68 *
69 *      The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
70 *
71 * @since  3378 (creation)
72 * @since 10386 (new LayerChangeListener interface)
73 */
74public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener, NoteDataUpdateListener {
75
76    private static final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
77    private static final String AUTOSAVE_DIR = "autosave";
78    private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
79
80    /**
81     * If autosave is enabled
82     */
83    public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
84    /**
85     * The number of files to store per layer
86     */
87    public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
88    /**
89     * How many deleted layers should be stored
90     */
91    public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
92    /**
93     * The autosave interval, in seconds
94     */
95    public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", (int) TimeUnit.MINUTES.toSeconds(5));
96    /**
97     * The maximum number of autosave files to store
98     */
99    public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
100    /**
101     * Defines if a notification should be displayed after each autosave
102     */
103    public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false);
104
105    protected static final class AutosaveLayerInfo<T extends AbstractModifiableLayer> {
106        private final T layer;
107        private String layerName;
108        private String layerFileName;
109        private final Deque<File> backupFiles = new LinkedList<>();
110
111        AutosaveLayerInfo(T layer) {
112            this.layer = layer;
113        }
114    }
115
116    private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
117    private final Set<DataSet> changedDatasets = new HashSet<>();
118    private final Set<NoteData> changedNoteData = new HashSet<>();
119    private final List<AutosaveLayerInfo<?>> layersInfo = new ArrayList<>();
120    private final Object layersLock = new Object();
121    private final Deque<File> deletedLayers = new LinkedList<>();
122
123    private final File autosaveDir = new File(Config.getDirs().getUserDataDirectory(true), AUTOSAVE_DIR);
124    private final File deletedLayersDir = new File(Config.getDirs().getUserDataDirectory(true), DELETED_LAYERS_DIR);
125
126    /**
127     * Replies the autosave directory.
128     * @return the autosave directory
129     * @since 10299
130     */
131    public final Path getAutosaveDir() {
132        return autosaveDir.toPath();
133    }
134
135    /**
136     * Starts the autosave background task.
137     */
138    public void schedule() {
139        if (PROP_INTERVAL.get() > 0) {
140
141            if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
142                Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
143                return;
144            }
145            if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
146                Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
147                return;
148            }
149
150            File[] files = deletedLayersDir.listFiles();
151            if (files != null) {
152                for (File f: files) {
153                    deletedLayers.add(f); // FIXME: sort by mtime
154                }
155            }
156
157            new Timer(true).schedule(this, TimeUnit.SECONDS.toMillis(1), TimeUnit.SECONDS.toMillis(PROP_INTERVAL.get()));
158            MainApplication.getLayerManager().addAndFireLayerChangeListener(this);
159        }
160    }
161
162    private static String getFileName(String layerName, int index) {
163        String result = layerName;
164        for (char illegalCharacter : ILLEGAL_CHARACTERS) {
165            result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)),
166                    '&' + String.valueOf((int) illegalCharacter) + ';');
167        }
168        if (index != 0) {
169            result = result + '_' + index;
170        }
171        return result;
172    }
173
174    private void setLayerFileName(AutosaveLayerInfo<?> layer) {
175        int index = 0;
176        while (true) {
177            String filename = getFileName(layer.layer.getName(), index);
178            boolean foundTheSame = false;
179            for (AutosaveLayerInfo<?> info: layersInfo) {
180                if (info != layer && filename.equals(info.layerFileName)) {
181                    foundTheSame = true;
182                    break;
183                }
184            }
185
186            if (!foundTheSame) {
187                layer.layerFileName = filename;
188                return;
189            }
190
191            index++;
192        }
193    }
194
195    protected File getNewLayerFile(AutosaveLayerInfo<?> layer, Date now, int startIndex) {
196        int index = startIndex;
197        while (true) {
198            String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s",
199                    layer.layerFileName, now, index == 0 ? "" : ('_' + Integer.toString(index)));
200            File result = new File(autosaveDir, filename + '.' +
201                    (layer.layer instanceof NoteLayer ?
202                            Config.getPref().get("autosave.notes.extension", "osn") :
203                            Config.getPref().get("autosave.extension", "osm")));
204            try {
205                if (index > PROP_INDEX_LIMIT.get())
206                    throw new IOException("index limit exceeded");
207                if (result.createNewFile()) {
208                    createNewPidFile(autosaveDir, filename);
209                    return result;
210                } else {
211                    Logging.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
212                }
213            } catch (IOException e) {
214                Logging.log(Logging.LEVEL_ERROR, tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()), e);
215                return null;
216            }
217            index++;
218        }
219    }
220
221    private static void createNewPidFile(File autosaveDir, String filename) {
222        File pidFile = new File(autosaveDir, filename+".pid");
223        try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) {
224            ps.println(ManagementFactory.getRuntimeMXBean().getName());
225        } catch (IOException | SecurityException t) {
226            Logging.error(t);
227        }
228    }
229
230    private void savelayer(AutosaveLayerInfo<?> info) {
231        if (!info.layer.getName().equals(info.layerName)) {
232            setLayerFileName(info);
233            info.layerName = info.layer.getName();
234        }
235        try {
236            if (info.layer instanceof OsmDataLayer) {
237                OsmDataLayer dataLayer = (OsmDataLayer) info.layer;
238                if (changedDatasets.remove(dataLayer.data)) {
239                    File file = getNewLayerFile(info, new Date(), 0);
240                    if (file != null) {
241                        info.backupFiles.add(file);
242                        new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
243                    }
244                }
245            } else if (info.layer instanceof NoteLayer) {
246                NoteLayer noteLayer = (NoteLayer) info.layer;
247                if (changedNoteData.remove(noteLayer.getNoteData())) {
248                    File file = getNewLayerFile(info, new Date(), 0);
249                    if (file != null) {
250                        info.backupFiles.add(file);
251                        new NoteExporter().exportData(file, info.layer);
252                    }
253                }
254            }
255        } catch (IOException e) {
256            Logging.error(e);
257        }
258        while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
259            File oldFile = info.backupFiles.remove();
260            if (Utils.deleteFile(oldFile, marktr("Unable to delete old backup file {0}"))) {
261                Utils.deleteFile(getPidFile(oldFile), marktr("Unable to delete old backup file {0}"));
262            }
263        }
264    }
265
266    @Override
267    public void run() {
268        synchronized (layersLock) {
269            try {
270                for (AutosaveLayerInfo<?> info: layersInfo) {
271                    savelayer(info);
272                }
273                changedDatasets.clear();
274                changedNoteData.clear();
275                if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) {
276                    GuiHelper.runInEDT(this::displayNotification);
277                }
278            } catch (RuntimeException t) { // NOPMD
279                // Don't let exception stop time thread
280                Logging.error("Autosave failed:");
281                Logging.error(t);
282            }
283        }
284    }
285
286    protected void displayNotification() {
287        new Notification(tr("Your work has been saved automatically."))
288        .setDuration(Notification.TIME_SHORT)
289        .show();
290    }
291
292    @Override
293    public void layerOrderChanged(LayerOrderChangeEvent e) {
294        // Do nothing
295    }
296
297    private void registerNewlayer(OsmDataLayer layer) {
298        synchronized (layersLock) {
299            layer.data.addDataSetListener(datasetAdapter);
300            layersInfo.add(new AutosaveLayerInfo<>(layer));
301        }
302    }
303
304    private void registerNewlayer(NoteLayer layer) {
305        synchronized (layersLock) {
306            layer.getNoteData().addNoteDataUpdateListener(this);
307            layersInfo.add(new AutosaveLayerInfo<>(layer));
308        }
309    }
310
311    @Override
312    public void layerAdded(LayerAddEvent e) {
313        if (e.getAddedLayer() instanceof OsmDataLayer) {
314            registerNewlayer((OsmDataLayer) e.getAddedLayer());
315        } else if (e.getAddedLayer() instanceof NoteLayer) {
316            registerNewlayer((NoteLayer) e.getAddedLayer());
317        }
318    }
319
320    @Override
321    public void layerRemoving(LayerRemoveEvent e) {
322        if (e.getRemovedLayer() instanceof OsmDataLayer) {
323            synchronized (layersLock) {
324                OsmDataLayer osmLayer = (OsmDataLayer) e.getRemovedLayer();
325                osmLayer.data.removeDataSetListener(datasetAdapter);
326                cleanupLayer(osmLayer);
327            }
328        } else if (e.getRemovedLayer() instanceof NoteLayer) {
329            synchronized (layersLock) {
330                NoteLayer noteLayer = (NoteLayer) e.getRemovedLayer();
331                noteLayer.getNoteData().removeNoteDataUpdateListener(this);
332                cleanupLayer(noteLayer);
333            }
334        }
335    }
336
337    private void cleanupLayer(AbstractModifiableLayer removedLayer) {
338        Iterator<AutosaveLayerInfo<?>> it = layersInfo.iterator();
339        while (it.hasNext()) {
340            AutosaveLayerInfo<?> info = it.next();
341            if (info.layer == removedLayer) {
342
343                savelayer(info);
344                File lastFile = info.backupFiles.pollLast();
345                if (lastFile != null) {
346                    moveToDeletedLayersFolder(lastFile);
347                }
348                for (File file: info.backupFiles) {
349                    if (Utils.deleteFile(file)) {
350                        Utils.deleteFile(getPidFile(file));
351                    }
352                }
353
354                it.remove();
355            }
356        }
357    }
358
359    @Override
360    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
361        changedDatasets.add(event.getDataset());
362    }
363
364    @Override
365    public void noteDataUpdated(NoteData data) {
366        changedNoteData.add(data);
367    }
368
369    @Override
370    public void selectedNoteChanged(NoteData noteData) {
371        // Do nothing
372    }
373
374    protected File getPidFile(File osmFile) {
375        return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid"));
376    }
377
378    /**
379     * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM.
380     * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance.
381     * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM
382     */
383    public List<File> getUnsavedLayersFiles() {
384        List<File> result = new ArrayList<>();
385        File[] files = autosaveDir.listFiles((FileFilter)
386                pathname -> OsmImporter.FILE_FILTER.accept(pathname) || NoteImporter.FILE_FILTER.accept(pathname));
387        if (files == null)
388            return result;
389        for (File file: files) {
390            if (file.isFile()) {
391                boolean skipFile = false;
392                File pidFile = getPidFile(file);
393                if (pidFile.exists()) {
394                    try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) {
395                        String jvmId = reader.readLine();
396                        if (jvmId != null) {
397                            String pid = jvmId.split("@")[0];
398                            skipFile = jvmPerfDataFileExists(pid);
399                        }
400                    } catch (IOException | SecurityException t) {
401                        Logging.error(t);
402                    }
403                }
404                if (!skipFile) {
405                    result.add(file);
406                }
407            }
408        }
409        return result;
410    }
411
412    private static boolean jvmPerfDataFileExists(final String jvmId) {
413        File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name"));
414        if (jvmDir.exists() && jvmDir.canRead()) {
415            File[] files = jvmDir.listFiles((FileFilter) file -> file.getName().equals(jvmId) && file.isFile());
416            return files != null && files.length == 1;
417        }
418        return false;
419    }
420
421    /**
422     * Recover the unsaved layers and open them asynchronously.
423     * @return A future that can be used to wait for the completion of this task.
424     */
425    public Future<?> recoverUnsavedLayers() {
426        List<File> files = getUnsavedLayersFiles();
427        final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
428        final Future<?> openFilesFuture = MainApplication.worker.submit(openFileTsk);
429        return MainApplication.worker.submit(() -> {
430            try {
431                // Wait for opened tasks to be generated.
432                openFilesFuture.get();
433                for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
434                    moveToDeletedLayersFolder(f);
435                }
436            } catch (InterruptedException | ExecutionException e) {
437                Logging.error(e);
438            }
439        });
440    }
441
442    /**
443     * Move file to the deleted layers directory.
444     * If moving does not work, it will try to delete the file directly.
445     * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
446     * some files in the deleted layers directory will be removed.
447     *
448     * @param f the file, usually from the autosave dir
449     */
450    private void moveToDeletedLayersFolder(File f) {
451        File backupFile = new File(deletedLayersDir, f.getName());
452        File pidFile = getPidFile(f);
453
454        if (backupFile.exists()) {
455            deletedLayers.remove(backupFile);
456            Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}"));
457        }
458        if (f.renameTo(backupFile)) {
459            deletedLayers.add(backupFile);
460            Utils.deleteFile(pidFile);
461        } else {
462            Logging.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
463            // we cannot move to deleted folder, so just try to delete it directly
464            if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) {
465                Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}"));
466            }
467        }
468        while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
469            File next = deletedLayers.remove();
470            if (next == null) {
471                break;
472            }
473            Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}"));
474        }
475    }
476
477    /**
478     * Mark all unsaved layers as deleted. They are still preserved in the deleted layers folder.
479     */
480    public void discardUnsavedLayers() {
481        for (File f: getUnsavedLayersFiles()) {
482            moveToDeletedLayersFolder(f);
483        }
484    }
485}
Note: See TracBrowser for help on using the repository browser.