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

Last change on this file since 3779 was 3720, checked in by bastiK, 13 years ago

added missing svn:eol-style

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