001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.BufferedReader; 007import java.io.File; 008import java.io.FileFilter; 009import java.io.FileInputStream; 010import java.io.IOException; 011import java.io.InputStreamReader; 012import java.io.PrintStream; 013import java.lang.management.ManagementFactory; 014import java.util.ArrayList; 015import java.util.Date; 016import java.util.Deque; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Set; 022import java.util.Timer; 023import java.util.TimerTask; 024import java.util.regex.Pattern; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask; 028import org.openstreetmap.josm.data.osm.DataSet; 029import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 030import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; 031import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; 032import org.openstreetmap.josm.data.preferences.BooleanProperty; 033import org.openstreetmap.josm.data.preferences.IntegerProperty; 034import org.openstreetmap.josm.gui.MapView; 035import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 036import org.openstreetmap.josm.gui.Notification; 037import org.openstreetmap.josm.gui.layer.Layer; 038import org.openstreetmap.josm.gui.layer.OsmDataLayer; 039import org.openstreetmap.josm.gui.util.GuiHelper; 040import org.openstreetmap.josm.io.OsmExporter; 041import org.openstreetmap.josm.io.OsmImporter; 042import org.openstreetmap.josm.tools.Utils; 043 044/** 045 * Saves data layers periodically so they can be recovered in case of a crash. 046 * 047 * There are 2 directories 048 * - autosave dir: copies of the currently open data layers are saved here every 049 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding 050 * files are removed. If this dir is non-empty on start, JOSM assumes 051 * that it crashed last time. 052 * - deleted layers dir: "secondary archive" - when autosaved layers are restored 053 * they are copied to this directory. We cannot keep them in the autosave folder, 054 * but just deleting it would be dangerous: Maybe a feature inside the file 055 * caused JOSM to crash. If the data is valuable, the user can still try to 056 * open with another versions of JOSM or fix the problem manually. 057 * 058 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files. 059 */ 060public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener { 061 062 private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' }; 063 private static final String AUTOSAVE_DIR = "autosave"; 064 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers"; 065 066 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true); 067 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1); 068 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5); 069 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60); 070 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000); 071 /** Defines if a notification should be displayed after each autosave */ 072 public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false); 073 074 private static class AutosaveLayerInfo { 075 OsmDataLayer layer; 076 String layerName; 077 String layerFileName; 078 final Deque<File> backupFiles = new LinkedList<File>(); 079 } 080 081 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this); 082 private final Set<DataSet> changedDatasets = new HashSet<DataSet>(); 083 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>(); 084 private Timer timer; 085 private final Object layersLock = new Object(); 086 private final Deque<File> deletedLayers = new LinkedList<File>(); 087 088 private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR); 089 private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR); 090 091 public void schedule() { 092 if (PROP_INTERVAL.get() > 0) { 093 094 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) { 095 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath())); 096 return; 097 } 098 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) { 099 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath())); 100 return; 101 } 102 103 for (File f: deletedLayersDir.listFiles()) { 104 deletedLayers.add(f); // FIXME: sort by mtime 105 } 106 107 timer = new Timer(true); 108 timer.schedule(this, 1000, PROP_INTERVAL.get() * 1000); 109 MapView.addLayerChangeListener(this); 110 if (Main.isDisplayingMapView()) { 111 for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) { 112 registerNewlayer(l); 113 } 114 } 115 } 116 } 117 118 private String getFileName(String layerName, int index) { 119 String result = layerName; 120 for (char illegalCharacter : ILLEGAL_CHARACTERS) { 121 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)), 122 '&' + String.valueOf((int) illegalCharacter) + ';'); 123 } 124 if (index != 0) { 125 result = result + '_' + index; 126 } 127 return result; 128 } 129 130 private void setLayerFileName(AutosaveLayerInfo layer) { 131 int index = 0; 132 while (true) { 133 String filename = getFileName(layer.layer.getName(), index); 134 boolean foundTheSame = false; 135 for (AutosaveLayerInfo info: layersInfo) { 136 if (info != layer && filename.equals(info.layerFileName)) { 137 foundTheSame = true; 138 break; 139 } 140 } 141 142 if (!foundTheSame) { 143 layer.layerFileName = filename; 144 return; 145 } 146 147 index++; 148 } 149 } 150 151 private File getNewLayerFile(AutosaveLayerInfo layer) { 152 int index = 0; 153 Date now = new Date(); 154 while (true) { 155 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%3$s", layer.layerFileName, now, index == 0?"":"_" + index); 156 File result = new File(autosaveDir, filename+".osm"); 157 try { 158 if (result.createNewFile()) { 159 try { 160 File pidFile = new File(autosaveDir, filename+".pid"); 161 PrintStream ps = new PrintStream(pidFile); 162 ps.println(ManagementFactory.getRuntimeMXBean().getName()); 163 Utils.close(ps); 164 } catch (Throwable t) { 165 Main.error(t); 166 } 167 return result; 168 } else { 169 Main.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath())); 170 if (index > PROP_INDEX_LIMIT.get()) 171 throw new IOException("index limit exceeded"); 172 } 173 } catch (IOException e) { 174 Main.error(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage())); 175 return null; 176 } 177 index++; 178 } 179 } 180 181 private void savelayer(AutosaveLayerInfo info) throws IOException { 182 if (!info.layer.getName().equals(info.layerName)) { 183 setLayerFileName(info); 184 info.layerName = info.layer.getName(); 185 } 186 if (changedDatasets.remove(info.layer.data)) { 187 File file = getNewLayerFile(info); 188 if (file != null) { 189 info.backupFiles.add(file); 190 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */); 191 } 192 } 193 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) { 194 File oldFile = info.backupFiles.remove(); 195 if (!oldFile.delete()) { 196 Main.warn(tr("Unable to delete old backup file {0}", oldFile.getAbsolutePath())); 197 } else { 198 getPidFile(oldFile).delete(); 199 } 200 } 201 } 202 203 @Override 204 public void run() { 205 synchronized (layersLock) { 206 try { 207 for (AutosaveLayerInfo info: layersInfo) { 208 savelayer(info); 209 } 210 changedDatasets.clear(); 211 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) { 212 displayNotification(); 213 } 214 } catch (Throwable t) { 215 // Don't let exception stop time thread 216 Main.error("Autosave failed:"); 217 Main.error(t); 218 t.printStackTrace(); 219 } 220 } 221 } 222 223 protected void displayNotification() { 224 GuiHelper.runInEDT(new Runnable() { 225 @Override 226 public void run() { 227 new Notification(tr("Your work has been saved automatically.")) 228 .setDuration(Notification.TIME_SHORT) 229 .show(); 230 } 231 }); 232 } 233 234 @Override 235 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 236 // Do nothing 237 } 238 239 private void registerNewlayer(OsmDataLayer layer) { 240 synchronized (layersLock) { 241 layer.data.addDataSetListener(datasetAdapter); 242 AutosaveLayerInfo info = new AutosaveLayerInfo(); 243 info.layer = layer; 244 layersInfo.add(info); 245 } 246 } 247 248 @Override 249 public void layerAdded(Layer newLayer) { 250 if (newLayer instanceof OsmDataLayer) { 251 registerNewlayer((OsmDataLayer) newLayer); 252 } 253 } 254 255 @Override 256 public void layerRemoved(Layer oldLayer) { 257 if (oldLayer instanceof OsmDataLayer) { 258 synchronized (layersLock) { 259 OsmDataLayer osmLayer = (OsmDataLayer) oldLayer; 260 osmLayer.data.removeDataSetListener(datasetAdapter); 261 Iterator<AutosaveLayerInfo> it = layersInfo.iterator(); 262 while (it.hasNext()) { 263 AutosaveLayerInfo info = it.next(); 264 if (info.layer == osmLayer) { 265 266 try { 267 savelayer(info); 268 File lastFile = info.backupFiles.pollLast(); 269 if (lastFile != null) { 270 moveToDeletedLayersFolder(lastFile); 271 } 272 for (File file: info.backupFiles) { 273 if (file.delete()) { 274 getPidFile(file).delete(); 275 } 276 } 277 } catch (IOException e) { 278 Main.error(tr("Error while creating backup of removed layer: {0}", e.getMessage())); 279 } 280 281 it.remove(); 282 } 283 } 284 } 285 } 286 } 287 288 @Override 289 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 290 changedDatasets.add(event.getDataset()); 291 } 292 293 private final File getPidFile(File osmFile) { 294 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid")); 295 } 296 297 /** 298 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM. 299 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance. 300 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM 301 */ 302 public List<File> getUnsavedLayersFiles() { 303 List<File> result = new ArrayList<File>(); 304 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER); 305 if (files == null) 306 return result; 307 for (File file: files) { 308 if (file.isFile()) { 309 boolean skipFile = false; 310 File pidFile = getPidFile(file); 311 if (pidFile.exists()) { 312 try { 313 BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(pidFile))); 314 try { 315 String jvmId = reader.readLine(); 316 if (jvmId != null) { 317 String pid = jvmId.split("@")[0]; 318 skipFile = jvmPerfDataFileExists(pid); 319 } 320 } catch (Throwable t) { 321 Main.error(t); 322 } finally { 323 Utils.close(reader); 324 } 325 } catch (Throwable t) { 326 Main.error(t); 327 } 328 } 329 if (!skipFile) { 330 result.add(file); 331 } 332 } 333 } 334 return result; 335 } 336 337 private boolean jvmPerfDataFileExists(final String jvmId) { 338 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name")); 339 if (jvmDir.exists() && jvmDir.canRead()) { 340 File[] files = jvmDir.listFiles(new FileFilter() { 341 @Override 342 public boolean accept(File file) { 343 return file.getName().equals(jvmId) && file.isFile(); 344 } 345 }); 346 return files != null && files.length == 1; 347 } 348 return false; 349 } 350 351 public void recoverUnsavedLayers() { 352 List<File> files = getUnsavedLayersFiles(); 353 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files")); 354 Main.worker.submit(openFileTsk); 355 Main.worker.submit(new Runnable() { 356 @Override 357 public void run() { 358 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) { 359 moveToDeletedLayersFolder(f); 360 } 361 } 362 }); 363 } 364 365 /** 366 * Move file to the deleted layers directory. 367 * If moving does not work, it will try to delete the file directly. 368 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS, 369 * some files in the deleted layers directory will be removed. 370 * 371 * @param f the file, usually from the autosave dir 372 */ 373 private void moveToDeletedLayersFolder(File f) { 374 File backupFile = new File(deletedLayersDir, f.getName()); 375 File pidFile = getPidFile(f); 376 377 if (backupFile.exists()) { 378 deletedLayers.remove(backupFile); 379 if (!backupFile.delete()) { 380 Main.warn(String.format("Could not delete old backup file %s", backupFile)); 381 } 382 } 383 if (f.renameTo(backupFile)) { 384 deletedLayers.add(backupFile); 385 pidFile.delete(); 386 } else { 387 Main.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName())); 388 // we cannot move to deleted folder, so just try to delete it directly 389 if (!f.delete()) { 390 Main.warn(String.format("Could not delete backup file %s", f)); 391 } else { 392 pidFile.delete(); 393 } 394 } 395 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) { 396 File next = deletedLayers.remove(); 397 if (next == null) { 398 break; 399 } 400 if (!next.delete()) { 401 Main.warn(String.format("Could not delete archived backup file %s", next)); 402 } 403 } 404 } 405 406 public void dicardUnsavedLayers() { 407 for (File f: getUnsavedLayersFiles()) { 408 moveToDeletedLayersFolder(f); 409 } 410 } 411}