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

Last change on this file was 18801, checked in by taylor.smock, 9 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

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