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