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

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

sonar - squid:S1450 - Private fields only used as local variables in methods should become local variables

  • Property svn:eol-style set to native
File size: 16.4 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 final Object layersLock = new Object();
97 private final Deque<File> deletedLayers = new LinkedList<>();
98
99 private final File autosaveDir = new File(Main.pref.getUserDataDirectory(), AUTOSAVE_DIR);
100 private final File deletedLayersDir = new File(Main.pref.getUserDataDirectory(), DELETED_LAYERS_DIR);
101
102 /**
103 * Replies the autosave directory.
104 * @return the autosave directory
105 * @since 10299
106 */
107 public final Path getAutosaveDir() {
108 return autosaveDir.toPath();
109 }
110
111 /**
112 * Starts the autosave background task.
113 */
114 public void schedule() {
115 if (PROP_INTERVAL.get() > 0) {
116
117 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
118 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
119 return;
120 }
121 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
122 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
123 return;
124 }
125
126 File[] files = deletedLayersDir.listFiles();
127 if (files != null) {
128 for (File f: files) {
129 deletedLayers.add(f); // FIXME: sort by mtime
130 }
131 }
132
133 new Timer(true).schedule(this, 1000L, PROP_INTERVAL.get() * 1000L);
134 Main.getLayerManager().addLayerChangeListener(this, true);
135 }
136 }
137
138 private static String getFileName(String layerName, int index) {
139 String result = layerName;
140 for (char illegalCharacter : ILLEGAL_CHARACTERS) {
141 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)),
142 '&' + String.valueOf((int) illegalCharacter) + ';');
143 }
144 if (index != 0) {
145 result = result + '_' + index;
146 }
147 return result;
148 }
149
150 private void setLayerFileName(AutosaveLayerInfo layer) {
151 int index = 0;
152 while (true) {
153 String filename = getFileName(layer.layer.getName(), index);
154 boolean foundTheSame = false;
155 for (AutosaveLayerInfo info: layersInfo) {
156 if (info != layer && filename.equals(info.layerFileName)) {
157 foundTheSame = true;
158 break;
159 }
160 }
161
162 if (!foundTheSame) {
163 layer.layerFileName = filename;
164 return;
165 }
166
167 index++;
168 }
169 }
170
171 protected File getNewLayerFile(AutosaveLayerInfo layer, Date now, int startIndex) {
172 int index = startIndex;
173 while (true) {
174 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s",
175 layer.layerFileName, now, index == 0 ? "" : ('_' + Integer.toString(index)));
176 File result = new File(autosaveDir, filename + '.' + Main.pref.get("autosave.extension", "osm"));
177 try {
178 if (index > PROP_INDEX_LIMIT.get())
179 throw new IOException("index limit exceeded");
180 if (result.createNewFile()) {
181 createNewPidFile(autosaveDir, filename);
182 return result;
183 } else {
184 Main.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
185 }
186 } catch (IOException e) {
187 Main.error(e, tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()));
188 return null;
189 }
190 index++;
191 }
192 }
193
194 private static void createNewPidFile(File autosaveDir, String filename) {
195 File pidFile = new File(autosaveDir, filename+".pid");
196 try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) {
197 ps.println(ManagementFactory.getRuntimeMXBean().getName());
198 } catch (IOException | SecurityException t) {
199 Main.error(t);
200 }
201 }
202
203 private void savelayer(AutosaveLayerInfo info) {
204 if (!info.layer.getName().equals(info.layerName)) {
205 setLayerFileName(info);
206 info.layerName = info.layer.getName();
207 }
208 if (changedDatasets.remove(info.layer.data)) {
209 File file = getNewLayerFile(info, new Date(), 0);
210 if (file != null) {
211 info.backupFiles.add(file);
212 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
213 }
214 }
215 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
216 File oldFile = info.backupFiles.remove();
217 if (Utils.deleteFile(oldFile, marktr("Unable to delete old backup file {0}"))) {
218 Utils.deleteFile(getPidFile(oldFile), marktr("Unable to delete old backup file {0}"));
219 }
220 }
221 }
222
223 @Override
224 public void run() {
225 synchronized (layersLock) {
226 try {
227 for (AutosaveLayerInfo info: layersInfo) {
228 savelayer(info);
229 }
230 changedDatasets.clear();
231 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) {
232 GuiHelper.runInEDT(this::displayNotification);
233 }
234 } catch (RuntimeException t) {
235 // Don't let exception stop time thread
236 Main.error("Autosave failed:");
237 Main.error(t);
238 }
239 }
240 }
241
242 protected void displayNotification() {
243 new Notification(tr("Your work has been saved automatically."))
244 .setDuration(Notification.TIME_SHORT)
245 .show();
246 }
247
248 @Override
249 public void layerOrderChanged(LayerOrderChangeEvent e) {
250 // Do nothing
251 }
252
253 private void registerNewlayer(OsmDataLayer layer) {
254 synchronized (layersLock) {
255 layer.data.addDataSetListener(datasetAdapter);
256 layersInfo.add(new AutosaveLayerInfo(layer));
257 }
258 }
259
260 @Override
261 public void layerAdded(LayerAddEvent e) {
262 if (e.getAddedLayer() instanceof OsmDataLayer) {
263 registerNewlayer((OsmDataLayer) e.getAddedLayer());
264 }
265 }
266
267 @Override
268 public void layerRemoving(LayerRemoveEvent e) {
269 if (e.getRemovedLayer() instanceof OsmDataLayer) {
270 synchronized (layersLock) {
271 OsmDataLayer osmLayer = (OsmDataLayer) e.getRemovedLayer();
272 osmLayer.data.removeDataSetListener(datasetAdapter);
273 Iterator<AutosaveLayerInfo> it = layersInfo.iterator();
274 while (it.hasNext()) {
275 AutosaveLayerInfo info = it.next();
276 if (info.layer == osmLayer) {
277
278 savelayer(info);
279 File lastFile = info.backupFiles.pollLast();
280 if (lastFile != null) {
281 moveToDeletedLayersFolder(lastFile);
282 }
283 for (File file: info.backupFiles) {
284 if (Utils.deleteFile(file)) {
285 Utils.deleteFile(getPidFile(file));
286 }
287 }
288
289 it.remove();
290 }
291 }
292 }
293 }
294 }
295
296 @Override
297 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
298 changedDatasets.add(event.getDataset());
299 }
300
301 protected File getPidFile(File osmFile) {
302 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid"));
303 }
304
305 /**
306 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM.
307 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance.
308 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM
309 */
310 public List<File> getUnsavedLayersFiles() {
311 List<File> result = new ArrayList<>();
312 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER);
313 if (files == null)
314 return result;
315 for (File file: files) {
316 if (file.isFile()) {
317 boolean skipFile = false;
318 File pidFile = getPidFile(file);
319 if (pidFile.exists()) {
320 try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) {
321 String jvmId = reader.readLine();
322 if (jvmId != null) {
323 String pid = jvmId.split("@")[0];
324 skipFile = jvmPerfDataFileExists(pid);
325 }
326 } catch (IOException | SecurityException t) {
327 Main.error(t);
328 }
329 }
330 if (!skipFile) {
331 result.add(file);
332 }
333 }
334 }
335 return result;
336 }
337
338 private static boolean jvmPerfDataFileExists(final String jvmId) {
339 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name"));
340 if (jvmDir.exists() && jvmDir.canRead()) {
341 File[] files = jvmDir.listFiles((FileFilter) file -> file.getName().equals(jvmId) && file.isFile());
342 return files != null && files.length == 1;
343 }
344 return false;
345 }
346
347 /**
348 * Recover the unsaved layers and open them asynchronously.
349 * @return A future that can be used to wait for the completion of this task.
350 */
351 public Future<?> recoverUnsavedLayers() {
352 List<File> files = getUnsavedLayersFiles();
353 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
354 final Future<?> openFilesFuture = Main.worker.submit(openFileTsk);
355 return Main.worker.submit(() -> {
356 try {
357 // Wait for opened tasks to be generated.
358 openFilesFuture.get();
359 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
360 moveToDeletedLayersFolder(f);
361 }
362 } catch (InterruptedException | ExecutionException e) {
363 Main.error(e);
364 }
365 });
366 }
367
368 /**
369 * Move file to the deleted layers directory.
370 * If moving does not work, it will try to delete the file directly.
371 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
372 * some files in the deleted layers directory will be removed.
373 *
374 * @param f the file, usually from the autosave dir
375 */
376 private void moveToDeletedLayersFolder(File f) {
377 File backupFile = new File(deletedLayersDir, f.getName());
378 File pidFile = getPidFile(f);
379
380 if (backupFile.exists()) {
381 deletedLayers.remove(backupFile);
382 Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}"));
383 }
384 if (f.renameTo(backupFile)) {
385 deletedLayers.add(backupFile);
386 Utils.deleteFile(pidFile);
387 } else {
388 Main.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
389 // we cannot move to deleted folder, so just try to delete it directly
390 if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) {
391 Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}"));
392 }
393 }
394 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
395 File next = deletedLayers.remove();
396 if (next == null) {
397 break;
398 }
399 Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}"));
400 }
401 }
402
403 /**
404 * Mark all unsaved layers as deleted. They are still preserved in the deleted layers folder.
405 */
406 public void discardUnsavedLayers() {
407 for (File f: getUnsavedLayersFiles()) {
408 moveToDeletedLayersFolder(f);
409 }
410 }
411}
Note: See TracBrowser for help on using the repository browser.