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

Last change on this file since 10364 was 10364, checked in by stoecker, 8 years ago

gsoc-core - patch by Michael Zangl - see #12953 - remove deprecation usage

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