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