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

Revision 4852, 12.4 KB checked in by bastiK, 3 weeks ago (diff)

fixed #6252 - Phantom "unsaved" layers in 4021, 4032

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/plain
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, true /* no backup with appended ~ */);
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.