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

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

sonar - squid:S1166 - Exception handlers should preserve the original exceptions

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