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

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

sonar - squid:S2325 - "private" methods that don't access instance data should be "static"

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