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

Last change on this file since 3966 was 3839, checked in by jttt, 13 years ago

Fix #5893 Null Pointer Exception while loding custom presets at start - JOSM hangs on splash screen

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/plain
File size: 12.4 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.File;
7import java.io.IOException;
8import java.util.ArrayList;
9import java.util.Date;
10import java.util.Deque;
11import java.util.HashSet;
12import java.util.Iterator;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Set;
16import java.util.Timer;
17import java.util.TimerTask;
18import java.util.regex.Pattern;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
22import org.openstreetmap.josm.data.osm.DataSet;
23import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
24import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
25import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
26import org.openstreetmap.josm.data.preferences.BooleanProperty;
27import org.openstreetmap.josm.data.preferences.IntegerProperty;
28import org.openstreetmap.josm.gui.MapView;
29import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
30import org.openstreetmap.josm.gui.layer.Layer;
31import org.openstreetmap.josm.gui.layer.OsmDataLayer;
32import org.openstreetmap.josm.io.OsmExporter;
33
34/**
35 * Saves data layers periodically so they can be recovered in case of a crash.
36 *
37 * There are 2 directories
38 * - autosave dir: copies of the currently open data layers are saved here every
39 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
40 * files are removed. If this dir is non-empty on start, JOSM assumes
41 * that it crashed last time.
42 * - deleted layers dir: "secondary archive" - when autosaved layers are restored
43 * they are copied to this directory. We cannot keep them in the autosave folder,
44 * but just deleting it would be dangerous: Maybe a feature inside the file
45 * caused JOSM to crash. If the data is valuable, the user can still try to
46 * open with another versions of JOSM or fix the problem manually.
47 *
48 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
49 */
50public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
51
52 private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' };
53 private static final String AUTOSAVE_DIR = "autosave";
54 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
55
56 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
57 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
58 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
59 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60);
60 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
61
62 private static class AutosaveLayerInfo {
63 OsmDataLayer layer;
64 String layerName;
65 String layerFileName;
66 final Deque<File> backupFiles = new LinkedList<File>();
67 }
68
69 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
70 private Set<DataSet> changedDatasets = new HashSet<DataSet>();
71 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>();
72 private Timer timer;
73 private final Object layersLock = new Object();
74 private final Deque<File> deletedLayers = new LinkedList<File>();
75
76 private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR);
77 private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR);
78
79 public void schedule() {
80 if (PROP_INTERVAL.get() > 0) {
81
82 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
83 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
84 return;
85 }
86 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
87 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
88 return;
89 }
90
91 for (File f: deletedLayersDir.listFiles()) {
92 deletedLayers.add(f); // FIXME: sort by mtime
93 }
94
95 timer = new Timer(true);
96 timer.schedule(this, 1000, PROP_INTERVAL.get() * 1000);
97 MapView.addLayerChangeListener(this);
98 if (Main.isDisplayingMapView()) {
99 for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) {
100 registerNewlayer(l);
101 }
102 }
103 }
104 }
105
106 private String getFileName(String layerName, int index) {
107 String result = layerName;
108 for (int i=0; i<ILLEGAL_CHARACTERS.length; i++) {
109 result = result.replaceAll(Pattern.quote(String.valueOf(ILLEGAL_CHARACTERS[i])),
110 '&' + String.valueOf((int)ILLEGAL_CHARACTERS[i]) + ';');
111 }
112 if (index != 0) {
113 result = result + '_' + index;
114 }
115 return result;
116 }
117
118 private void setLayerFileName(AutosaveLayerInfo layer) {
119 int index = 0;
120 while (true) {
121 String filename = getFileName(layer.layer.getName(), index);
122 boolean foundTheSame = false;
123 for (AutosaveLayerInfo info: layersInfo) {
124 if (info != layer && filename.equals(info.layerFileName)) {
125 foundTheSame = true;
126 break;
127 }
128 }
129
130 if (!foundTheSame) {
131 layer.layerFileName = filename;
132 return;
133 }
134
135 index++;
136 }
137 }
138
139 private File getNewLayerFile(AutosaveLayerInfo layer) {
140 int index = 0;
141 Date now = new Date();
142 while (true) {
143 File result = new File(autosaveDir, String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%3$s.osm", layer.layerFileName, now, index == 0?"":"_" + index));
144 try {
145 if (result.createNewFile())
146 return result;
147 else {
148 System.out.println(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
149 if (index > PROP_INDEX_LIMIT.get())
150 throw new IOException("index limit exceeded");
151 }
152 } catch (IOException e) {
153 System.err.println(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()));
154 return null;
155 }
156 index++;
157 }
158 }
159
160 private void savelayer(AutosaveLayerInfo info) throws IOException {
161 if (!info.layer.getName().equals(info.layerName)) {
162 setLayerFileName(info);
163 info.layerName = info.layer.getName();
164 }
165 if (changedDatasets.contains(info.layer.data)) {
166 File file = getNewLayerFile(info);
167 if (file != null) {
168 info.backupFiles.add(file);
169 new OsmExporter().exportData(file, info.layer);
170 }
171 }
172 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
173 File oldFile = info.backupFiles.remove();
174 if (!oldFile.delete()) {
175 System.out.println(tr("Unable to delete old backup file {0}", oldFile.getAbsolutePath()));
176 }
177 }
178 }
179
180 @Override
181 public void run() {
182 synchronized (layersLock) {
183 try {
184 for (AutosaveLayerInfo info: layersInfo) {
185 savelayer(info);
186 }
187 changedDatasets.clear();
188 } catch (Throwable t) {
189 // Don't let exception stop time thread
190 System.err.println("Autosave failed: ");
191 t.printStackTrace();
192 }
193 }
194 }
195
196 @Override
197 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
198 // Do nothing
199 }
200
201 private void registerNewlayer(OsmDataLayer layer) {
202 synchronized (layersLock) {
203 layer.data.addDataSetListener(datasetAdapter);
204 AutosaveLayerInfo info = new AutosaveLayerInfo();
205 info.layer = layer;
206 layersInfo.add(info);
207 }
208 }
209
210 @Override
211 public void layerAdded(Layer newLayer) {
212 if (newLayer instanceof OsmDataLayer) {
213 registerNewlayer((OsmDataLayer) newLayer);
214 }
215 }
216
217 @Override
218 public void layerRemoved(Layer oldLayer) {
219 if (oldLayer instanceof OsmDataLayer) {
220 synchronized (layersLock) {
221 OsmDataLayer osmLayer = (OsmDataLayer) oldLayer;
222 osmLayer.data.removeDataSetListener(datasetAdapter);
223 Iterator<AutosaveLayerInfo> it = layersInfo.iterator();
224 while (it.hasNext()) {
225 AutosaveLayerInfo info = it.next();
226 if (info.layer == osmLayer) {
227
228 try {
229 savelayer(info);
230 File lastFile = info.backupFiles.pollLast();
231 if (lastFile != null) {
232 moveToDeletedLayersFolder(lastFile);
233 }
234 for (File file: info.backupFiles) {
235 file.delete();
236 }
237 } catch (IOException e) {
238 System.err.println(tr("Error while creating backup of removed layer: {0}", e.getMessage()));
239 }
240
241 it.remove();
242 }
243 }
244 }
245 }
246 }
247
248 @Override
249 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
250 changedDatasets.add(event.getDataset());
251 }
252
253 public List<File> getUnsavedLayersFiles() {
254 List<File> result = new ArrayList<File>();
255 File[] files = autosaveDir.listFiles();
256 if (files == null)
257 return result;
258 for (File file: files) {
259 if (file.isFile()) {
260 result.add(file);
261 }
262 }
263 return result;
264 }
265
266 public void recoverUnsavedLayers() {
267 List<File> files = getUnsavedLayersFiles();
268 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
269 Main.worker.submit(openFileTsk);
270 Main.worker.submit(new Runnable() {
271 public void run() {
272 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
273 moveToDeletedLayersFolder(f);
274 }
275 }
276 });
277 }
278
279 /**
280 * Move file to the deleted layers directory.
281 * If moving does not work, it will try to delete the file directly.
282 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
283 * some files in the deleted layers directory will be removed.
284 *
285 * @param f the file, usually from the autosave dir
286 */
287 private void moveToDeletedLayersFolder(File f) {
288 File backupFile = new File(deletedLayersDir, f.getName());
289
290 if (backupFile.exists()) {
291 deletedLayers.remove(backupFile);
292 if (!backupFile.delete()) {
293 System.err.println(String.format("Warning: Could not delete old backup file %s", backupFile));
294 }
295 }
296 if (f.renameTo(backupFile)) {
297 deletedLayers.add(backupFile);
298 } else {
299 System.err.println(String.format("Warning: Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
300 // we cannot move to deleted folder, so just try to delete it directly
301 if (!f.delete()) {
302 System.err.println(String.format("Warning: Could not delete backup file %s", f));
303 }
304 }
305 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
306 File next = deletedLayers.remove();
307 if (next == null) {
308 break;
309 }
310 if (!next.delete()) {
311 System.err.println(String.format("Warning: Could not delete archived backup file %s", next));
312 }
313 }
314 }
315
316 public void dicardUnsavedLayers() {
317 for (File f: getUnsavedLayersFiles()) {
318 moveToDeletedLayersFolder(f);
319 }
320 }
321}
Note: See TracBrowser for help on using the repository browser.