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

Last change on this file since 12453 was 12374, checked in by michael2402, 7 years ago

Javadoc for the data package.

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