source: josm/trunk/src/org/openstreetmap/josm/data/AutosaveTask.java @ 11905

Last change on this file since 11905 was 11905, checked in by Don-vip, 2 years ago

sonar - squid:S2301 - Public methods should not contain selector arguments

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