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

Last change on this file since 7956 was 7951, checked in by Don-vip, 9 years ago

fix some Sonar issues

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