001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.io.session;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.Utils.equal;
006    
007    import java.io.BufferedInputStream;
008    import java.io.File;
009    import java.io.FileInputStream;
010    import java.io.FileNotFoundException;
011    import java.io.IOException;
012    import java.io.InputStream;
013    import java.lang.reflect.InvocationTargetException;
014    import java.net.URI;
015    import java.net.URISyntaxException;
016    import java.util.ArrayList;
017    import java.util.Collections;
018    import java.util.Enumeration;
019    import java.util.HashMap;
020    import java.util.LinkedHashMap;
021    import java.util.List;
022    import java.util.Map;
023    import java.util.Map.Entry;
024    import java.util.TreeMap;
025    import java.util.zip.ZipEntry;
026    import java.util.zip.ZipException;
027    import java.util.zip.ZipFile;
028    
029    import javax.swing.JOptionPane;
030    import javax.swing.SwingUtilities;
031    import javax.xml.parsers.DocumentBuilder;
032    import javax.xml.parsers.DocumentBuilderFactory;
033    import javax.xml.parsers.ParserConfigurationException;
034    
035    import org.openstreetmap.josm.Main;
036    import org.openstreetmap.josm.gui.ExtendedDialog;
037    import org.openstreetmap.josm.gui.layer.Layer;
038    import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
039    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
040    import org.openstreetmap.josm.io.IllegalDataException;
041    import org.openstreetmap.josm.tools.MultiMap;
042    import org.openstreetmap.josm.tools.Utils;
043    import org.w3c.dom.Document;
044    import org.w3c.dom.Element;
045    import org.w3c.dom.Node;
046    import org.w3c.dom.NodeList;
047    import org.xml.sax.SAXException;
048    
049    /**
050     * Reads a .jos session file and loads the layers in the process.
051     *
052     */
053    public class SessionReader {
054    
055        private static Map<String, Class<? extends SessionLayerImporter>> sessionLayerImporters = new HashMap<String, Class<? extends SessionLayerImporter>>();
056        static {
057            registerSessionLayerImporter("osm-data", OsmDataSessionImporter.class);
058            registerSessionLayerImporter("imagery", ImagerySessionImporter.class);
059            registerSessionLayerImporter("tracks", GpxTracksSessionImporter.class);
060            registerSessionLayerImporter("geoimage", GeoImageSessionImporter.class);
061        }
062    
063        public static void registerSessionLayerImporter(String layerType, Class<? extends SessionLayerImporter> importer) {
064            sessionLayerImporters.put(layerType, importer);
065        }
066    
067        public static SessionLayerImporter getSessionLayerImporter(String layerType) {
068            Class<? extends SessionLayerImporter> importerClass = sessionLayerImporters.get(layerType);
069            if (importerClass == null)
070                return null;
071            SessionLayerImporter importer = null;
072            try {
073                importer = importerClass.newInstance();
074            } catch (InstantiationException e) {
075                throw new RuntimeException(e);
076            } catch (IllegalAccessException e) {
077                throw new RuntimeException(e);
078            }
079            return importer;
080        }
081    
082        private File sessionFile;
083        private boolean zip; /* true, if session file is a .joz file; false if it is a .jos file */
084        private ZipFile zipFile;
085        private List<Layer> layers = new ArrayList<Layer>();
086        private List<Runnable> postLoadTasks = new ArrayList<Runnable>();
087    
088        /**
089         * @return list of layers that are later added to the mapview
090         */
091        public List<Layer> getLayers() {
092            return layers;
093        }
094    
095        /**
096         * @return actions executed in EDT after layers have been added (message dialog, etc.)
097         */
098        public List<Runnable> getPostLoadTasks() {
099            return postLoadTasks;
100        }
101    
102        public class ImportSupport {
103    
104            private String layerName;
105            private int layerIndex;
106            private List<LayerDependency> layerDependencies;
107    
108            public ImportSupport(String layerName, int layerIndex, List<LayerDependency> layerDependencies) {
109                this.layerName = layerName;
110                this.layerIndex = layerIndex;
111                this.layerDependencies = layerDependencies;
112            }
113    
114            /**
115             * Path of the file inside the zip archive.
116             * Used as alternative return value for getFile method.
117             */
118            private String inZipPath;
119    
120            /**
121             * Add a task, e.g. a message dialog, that should
122             * be executed in EDT after all layers have been added.
123             */
124            public void addPostLayersTask(Runnable task) {
125                postLoadTasks.add(task);
126            }
127    
128            /**
129             * Return an InputStream for a URI from a .jos/.joz file.
130             *
131             * The following forms are supported:
132             *
133             * - absolute file (both .jos and .joz):
134             *         "file:///home/user/data.osm"
135             *         "file:/home/user/data.osm"
136             *         "file:///C:/files/data.osm"
137             *         "file:/C:/file/data.osm"
138             *         "/home/user/data.osm"
139             *         "C:\files\data.osm"          (not a URI, but recognized by File constructor on Windows systems)
140             * - standalone .jos files:
141             *     - relative uri:
142             *         "save/data.osm"
143             *         "../project2/data.osm"
144             * - for .joz files:
145             *     - file inside zip archive:
146             *         "layers/01/data.osm"
147             *     - relativ to the .joz file:
148             *         "../save/data.osm"           ("../" steps out of the archive)
149             *
150             * @throws IOException Thrown when no Stream can be opened for the given URI, e.g. when the linked file has been deleted.
151             */
152            public InputStream getInputStream(String uriStr) throws IOException {
153                File file = getFile(uriStr);
154                if (file != null) {
155                    try {
156                        return new BufferedInputStream(new FileInputStream(file));
157                    } catch (FileNotFoundException e) {
158                        throw new IOException(tr("File ''{0}'' does not exist.", file.getPath()));
159                    }
160                } else if (inZipPath != null) {
161                    ZipEntry entry = zipFile.getEntry(inZipPath);
162                    if (entry != null) {
163                        InputStream is = zipFile.getInputStream(entry);
164                        return is;
165                    }
166                }
167                throw new IOException(tr("Unable to locate file  ''{0}''.", uriStr));
168            }
169    
170            /**
171             * Return a File for a URI from a .jos/.joz file.
172             *
173             * Returns null if the URI points to a file inside the zip archive.
174             * In this case, inZipPath will be set to the corresponding path.
175             */
176            public File getFile(String uriStr) throws IOException {
177                inZipPath = null;
178                try {
179                    URI uri = new URI(uriStr);
180                    if ("file".equals(uri.getScheme()))
181                        // absolute path
182                        return new File(uri);
183                    else if (uri.getScheme() == null) {
184                        // Check if this is an absolute path without 'file:' scheme part.
185                        // At this point, (as an exception) platform dependent path separator will be recognized.
186                        // (This form is discouraged, only for users that like to copy and paste a path manually.)
187                        File file = new File(uriStr);
188                        if (file.isAbsolute())
189                            return file;
190                        else {
191                            // for relative paths, only forward slashes are permitted
192                            if (isZip()) {
193                                if (uri.getPath().startsWith("../")) {
194                                    // relative to session file - "../" step out of the archive
195                                    String relPath = uri.getPath().substring(3);
196                                    return new File(sessionFile.toURI().resolve(relPath));
197                                } else {
198                                    // file inside zip archive
199                                    inZipPath = uriStr;
200                                    return null;
201                                }
202                            } else
203                                return new File(sessionFile.toURI().resolve(uri));
204                        }
205                    } else
206                        throw new IOException(tr("Unsupported scheme ''{0}'' in URI ''{1}''.", uri.getScheme(), uriStr));
207                } catch (URISyntaxException e) {
208                    throw new IOException(e);
209                }
210            }
211    
212            /**
213             * Returns true if we are reading from a .joz file.
214             */
215            public boolean isZip() {
216                return zip;
217            }
218    
219            /**
220             * Name of the layer that is currently imported.
221             */
222            public String getLayerName() {
223                return layerName;
224            }
225    
226            /**
227             * Index of the layer that is currently imported.
228             */
229            public int getLayerIndex() {
230                return layerIndex;
231            }
232    
233            /**
234             * Dependencies - maps the layer index to the importer of the given
235             * layer. All the dependent importers have loaded completely at this point.
236             */
237            public List<LayerDependency> getLayerDependencies() {
238                return layerDependencies;
239            }
240        }
241    
242        public static class LayerDependency {
243            private Integer index;
244            private Layer layer;
245            private SessionLayerImporter importer;
246    
247            public LayerDependency(Integer index, Layer layer, SessionLayerImporter importer) {
248                this.index = index;
249                this.layer = layer;
250                this.importer = importer;
251            }
252    
253            public SessionLayerImporter getImporter() {
254                return importer;
255            }
256    
257            public Integer getIndex() {
258                return index;
259            }
260    
261            public Layer getLayer() {
262                return layer;
263            }
264        }
265    
266        private void error(String msg) throws IllegalDataException {
267            throw new IllegalDataException(msg);
268        }
269    
270        private void parseJos(Document doc, ProgressMonitor progressMonitor) throws IllegalDataException {
271            Element root = doc.getDocumentElement();
272            if (!equal(root.getTagName(), "josm-session")) {
273                error(tr("Unexpected root element ''{0}'' in session file", root.getTagName()));
274            }
275            String version = root.getAttribute("version");
276            if (!"0.1".equals(version)) {
277                error(tr("Version ''{0}'' of session file is not supported. Expected: 0.1", version));
278            }
279    
280            NodeList layersNL = root.getElementsByTagName("layers");
281            if (layersNL.getLength() == 0) return;
282    
283            Element layersEl = (Element) layersNL.item(0);
284    
285            MultiMap<Integer, Integer> deps = new MultiMap<Integer, Integer>();
286            Map<Integer, Element> elems = new HashMap<Integer, Element>();
287    
288            NodeList nodes = layersEl.getChildNodes();
289    
290            for (int i=0; i<nodes.getLength(); ++i) {
291                Node node = nodes.item(i);
292                if (node.getNodeType() == Node.ELEMENT_NODE) {
293                    Element e = (Element) node;
294                    if (equal(e.getTagName(), "layer")) {
295    
296                        if (!e.hasAttribute("index")) {
297                            error(tr("missing mandatory attribute ''index'' for element ''layer''"));
298                        }
299                        Integer idx = null;
300                        try {
301                            idx = Integer.parseInt(e.getAttribute("index"));
302                        } catch (NumberFormatException ex) {}
303                        if (idx == null) {
304                            error(tr("unexpected format of attribute ''index'' for element ''layer''"));
305                        }
306                        if (elems.containsKey(idx)) {
307                            error(tr("attribute ''index'' ({0}) for element ''layer'' must be unique", Integer.toString(idx)));
308                        }
309                        elems.put(idx, e);
310    
311                        deps.putVoid(idx);
312                        String depStr = e.getAttribute("depends");
313                        if (depStr != null) {
314                            for (String sd : depStr.split(",")) {
315                                Integer d = null;
316                                try {
317                                    d = Integer.parseInt(sd);
318                                } catch (NumberFormatException ex) {}
319                                if (d != null) {
320                                    deps.put(idx, d);
321                                }
322                            }
323                        }
324                    }
325                }
326            }
327    
328            List<Integer> sorted = Utils.topologicalSort(deps);
329            final Map<Integer, Layer> layersMap = new TreeMap<Integer, Layer>(Collections.reverseOrder());
330            final Map<Integer, SessionLayerImporter> importers = new HashMap<Integer, SessionLayerImporter>();
331            final Map<Integer, String> names = new HashMap<Integer, String>();
332    
333            progressMonitor.setTicksCount(sorted.size());
334            LAYER: for (int idx: sorted) {
335                Element e = elems.get(idx);
336                if (e == null) {
337                    error(tr("missing layer with index {0}", idx));
338                }
339                if (!e.hasAttribute("name")) {
340                    error(tr("missing mandatory attribute ''name'' for element ''layer''"));
341                }
342                String name = e.getAttribute("name");
343                names.put(idx, name);
344                if (!e.hasAttribute("type")) {
345                    error(tr("missing mandatory attribute ''type'' for element ''layer''"));
346                }
347                String type = e.getAttribute("type");
348                SessionLayerImporter imp = getSessionLayerImporter(type);
349                if (imp == null) {
350                    CancelOrContinueDialog dialog = new CancelOrContinueDialog();
351                    dialog.show(
352                            tr("Unable to load layer"),
353                            tr("Cannot load layer of type ''{0}'' because no suitable importer was found.", type),
354                            JOptionPane.WARNING_MESSAGE,
355                            progressMonitor
356                            );
357                    if (dialog.isCancel()) {
358                        progressMonitor.cancel();
359                        return;
360                    } else {
361                        continue;
362                    }
363                } else {
364                    importers.put(idx, imp);
365                    List<LayerDependency> depsImp = new ArrayList<LayerDependency>();
366                    for (int d : deps.get(idx)) {
367                        SessionLayerImporter dImp = importers.get(d);
368                        if (dImp == null) {
369                            CancelOrContinueDialog dialog = new CancelOrContinueDialog();
370                            dialog.show(
371                                    tr("Unable to load layer"),
372                                    tr("Cannot load layer {0} because it depends on layer {1} which has been skipped.", idx, d),
373                                    JOptionPane.WARNING_MESSAGE,
374                                    progressMonitor
375                                    );
376                            if (dialog.isCancel()) {
377                                progressMonitor.cancel();
378                                return;
379                            } else {
380                                continue LAYER;
381                            }
382                        }
383                        depsImp.add(new LayerDependency(d, layersMap.get(d), dImp));
384                    }
385                    ImportSupport support = new ImportSupport(name, idx, depsImp);
386                    Layer layer = null;
387                    Exception exception = null;
388                    try {
389                        layer = imp.load(e, support, progressMonitor.createSubTaskMonitor(1, false));
390                    } catch (IllegalDataException ex) {
391                        exception = ex;
392                    } catch (IOException ex) {
393                        exception = ex;
394                    }
395                    if (exception != null) {
396                        exception.printStackTrace();
397                        CancelOrContinueDialog dialog = new CancelOrContinueDialog();
398                        dialog.show(
399                                tr("Error loading layer"),
400                                tr("<html>Could not load layer {0} ''{1}''.<br>Error is:<br>{2}</html>", idx, name, exception.getMessage()),
401                                JOptionPane.ERROR_MESSAGE,
402                                progressMonitor
403                                );
404                        if (dialog.isCancel()) {
405                            progressMonitor.cancel();
406                            return;
407                        } else {
408                            continue;
409                        }
410                    }
411    
412                    if (layer == null) throw new RuntimeException();
413                    layersMap.put(idx, layer);
414                }
415                progressMonitor.worked(1);
416            }
417    
418            layers = new ArrayList<Layer>();
419            for (Entry<Integer, Layer> e : layersMap.entrySet()) {
420                Layer l = e.getValue();
421                if (l == null) {
422                    continue;
423                }
424                l.setName(names.get(e.getKey()));
425                layers.add(l);
426            }
427        }
428    
429        /**
430         * Show Dialog when there is an error for one layer.
431         * Ask the user whether to cancel the complete session loading or just to skip this layer.
432         *
433         * This is expected to run in a worker thread (PleaseWaitRunnable), so invokeAndWait is
434         * needed to block the current thread and wait for the result of the modal dialog from EDT.
435         */
436        private static class CancelOrContinueDialog {
437    
438            private boolean cancel;
439    
440            public void show(final String title, final String message, final int icon, final ProgressMonitor progressMonitor) {
441                try {
442                    SwingUtilities.invokeAndWait(new Runnable() {
443                        @Override public void run() {
444                            ExtendedDialog dlg = new ExtendedDialog(
445                                    Main.parent,
446                                    title,
447                                    new String[] { tr("Cancel"), tr("Skip layer and continue") }
448                                    );
449                            dlg.setButtonIcons(new String[] {"cancel", "dialogs/next"});
450                            dlg.setIcon(icon);
451                            dlg.setContent(message);
452                            dlg.showDialog();
453                            cancel = dlg.getValue() != 2;
454                        }
455                    });
456                } catch (InvocationTargetException ex) {
457                    throw new RuntimeException(ex);
458                } catch (InterruptedException ex) {
459                    throw new RuntimeException(ex);
460                }
461            }
462    
463            public boolean isCancel() {
464                return cancel;
465            }
466        }
467    
468        public void loadSession(File sessionFile, boolean zip, ProgressMonitor progressMonitor) throws IllegalDataException, IOException {
469            if (progressMonitor == null) {
470                progressMonitor = NullProgressMonitor.INSTANCE;
471            }
472            this.sessionFile = sessionFile;
473            this.zip = zip;
474    
475            InputStream josIS = null;
476    
477            if (zip) {
478                try {
479                    zipFile = new ZipFile(sessionFile);
480                    ZipEntry josEntry = null;
481                    Enumeration<? extends ZipEntry> entries = zipFile.entries();
482                    while (entries.hasMoreElements()) {
483                        ZipEntry entry = entries.nextElement();
484                        if (entry.getName().toLowerCase().endsWith(".jos")) {
485                            josEntry = entry;
486                            break;
487                        }
488                    }
489                    if (josEntry == null) {
490                        error(tr("expected .jos file inside .joz archive"));
491                    }
492                    josIS = zipFile.getInputStream(josEntry);
493                } catch (ZipException ze) {
494                    throw new IOException(ze);
495                }
496            } else {
497                try {
498                    josIS = new FileInputStream(sessionFile);
499                } catch (FileNotFoundException ex) {
500                    throw new IOException(ex);
501                }
502            }
503    
504            try {
505                DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
506                builderFactory.setValidating(false);
507                builderFactory.setNamespaceAware(true);
508                DocumentBuilder builder = builderFactory.newDocumentBuilder();
509                Document document = builder.parse(josIS);
510                parseJos(document, progressMonitor);
511            } catch (SAXException e) {
512                throw new IllegalDataException(e);
513            } catch (ParserConfigurationException e) {
514                throw new IOException(e);
515            }
516        }
517    
518    }