001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.session;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GraphicsEnvironment;
007import java.io.BufferedInputStream;
008import java.io.File;
009import java.io.FileNotFoundException;
010import java.io.IOException;
011import java.io.InputStream;
012import java.lang.reflect.InvocationTargetException;
013import java.net.URI;
014import java.net.URISyntaxException;
015import java.nio.charset.StandardCharsets;
016import java.nio.file.Files;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Enumeration;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.TreeMap;
026import java.util.zip.ZipEntry;
027import java.util.zip.ZipException;
028import java.util.zip.ZipFile;
029
030import javax.swing.JOptionPane;
031import javax.swing.SwingUtilities;
032import javax.xml.parsers.ParserConfigurationException;
033
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.data.ViewportData;
036import org.openstreetmap.josm.data.coor.EastNorth;
037import org.openstreetmap.josm.data.coor.LatLon;
038import org.openstreetmap.josm.data.projection.Projection;
039import org.openstreetmap.josm.gui.ExtendedDialog;
040import org.openstreetmap.josm.gui.layer.Layer;
041import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
042import org.openstreetmap.josm.gui.progress.ProgressMonitor;
043import org.openstreetmap.josm.io.Compression;
044import org.openstreetmap.josm.io.IllegalDataException;
045import org.openstreetmap.josm.tools.CheckParameterUtil;
046import org.openstreetmap.josm.tools.JosmRuntimeException;
047import org.openstreetmap.josm.tools.Logging;
048import org.openstreetmap.josm.tools.MultiMap;
049import org.openstreetmap.josm.tools.Utils;
050import org.w3c.dom.Document;
051import org.w3c.dom.Element;
052import org.w3c.dom.Node;
053import org.w3c.dom.NodeList;
054import org.xml.sax.SAXException;
055
056/**
057 * Reads a .jos session file and loads the layers in the process.
058 * @since 4668
059 */
060public class SessionReader {
061
062    /**
063     * Data class for projection saved in the session file.
064     */
065    public static class SessionProjectionChoiceData {
066        private final String projectionChoiceId;
067        private final Collection<String> subPreferences;
068
069        /**
070         * Construct a new SessionProjectionChoiceData.
071         * @param projectionChoiceId projection choice id
072         * @param subPreferences parameters for the projection choice
073         */
074        public SessionProjectionChoiceData(String projectionChoiceId, Collection<String> subPreferences) {
075            this.projectionChoiceId = projectionChoiceId;
076            this.subPreferences = subPreferences;
077        }
078
079        /**
080         * Get the projection choice id.
081         * @return the projection choice id
082         */
083        public String getProjectionChoiceId() {
084            return projectionChoiceId;
085        }
086
087        /**
088         * Get the parameters for the projection choice
089         * @return parameters for the projection choice
090         */
091        public Collection<String> getSubPreferences() {
092            return subPreferences;
093        }
094    }
095
096    /**
097     * Data class for viewport saved in the session file.
098     */
099    public static class SessionViewportData {
100        private final LatLon center;
101        private final double meterPerPixel;
102
103        /**
104         * Construct a new SessionViewportData.
105         * @param center the lat/lon coordinates of the screen center
106         * @param meterPerPixel scale in meters per pixel
107         */
108        public SessionViewportData(LatLon center, double meterPerPixel) {
109            CheckParameterUtil.ensureParameterNotNull(center);
110            this.center = center;
111            this.meterPerPixel = meterPerPixel;
112        }
113
114        /**
115         * Get the lat/lon coordinates of the screen center.
116         * @return lat/lon coordinates of the screen center
117         */
118        public LatLon getCenter() {
119            return center;
120        }
121
122        /**
123         * Get the scale in meters per pixel.
124         * @return scale in meters per pixel
125         */
126        public double getScale() {
127            return meterPerPixel;
128        }
129
130        /**
131         * Convert this viewport data to a {@link ViewportData} object (with projected coordinates).
132         * @param proj the projection to convert from lat/lon to east/north
133         * @return the corresponding ViewportData object
134         */
135        public ViewportData getEastNorthViewport(Projection proj) {
136            EastNorth centerEN = proj.latlon2eastNorth(center);
137            // Get a "typical" distance in east/north units that
138            // corresponds to a couple of pixels. Shouldn't be too
139            // large, to keep it within projection bounds and
140            // not too small to avoid rounding errors.
141            double dist = 0.01 * proj.getDefaultZoomInPPD();
142            LatLon ll1 = proj.eastNorth2latlon(new EastNorth(centerEN.east() - dist, centerEN.north()));
143            LatLon ll2 = proj.eastNorth2latlon(new EastNorth(centerEN.east() + dist, centerEN.north()));
144            double meterPerEasting = ll1.greatCircleDistance(ll2) / dist / 2;
145            double scale = meterPerPixel / meterPerEasting; // unit: easting per pixel
146            return new ViewportData(centerEN, scale);
147        }
148    }
149
150    private static final Map<String, Class<? extends SessionLayerImporter>> sessionLayerImporters = new HashMap<>();
151
152    private URI sessionFileURI;
153    private boolean zip; // true, if session file is a .joz file; false if it is a .jos file
154    private ZipFile zipFile;
155    private List<Layer> layers = new ArrayList<>();
156    private int active = -1;
157    private final List<Runnable> postLoadTasks = new ArrayList<>();
158    private SessionViewportData viewport;
159    private SessionProjectionChoiceData projectionChoice;
160
161    static {
162        registerSessionLayerImporter("osm-data", OsmDataSessionImporter.class);
163        registerSessionLayerImporter("imagery", ImagerySessionImporter.class);
164        registerSessionLayerImporter("tracks", GpxTracksSessionImporter.class);
165        registerSessionLayerImporter("geoimage", GeoImageSessionImporter.class);
166        registerSessionLayerImporter("markers", MarkerSessionImporter.class);
167        registerSessionLayerImporter("osm-notes", NoteSessionImporter.class);
168    }
169
170    /**
171     * Register a session layer importer.
172     *
173     * @param layerType layer type
174     * @param importer importer for this layer class
175     */
176    public static void registerSessionLayerImporter(String layerType, Class<? extends SessionLayerImporter> importer) {
177        sessionLayerImporters.put(layerType, importer);
178    }
179
180    /**
181     * Returns the session layer importer for the given layer type.
182     * @param layerType layer type to import
183     * @return session layer importer for the given layer
184     */
185    public static SessionLayerImporter getSessionLayerImporter(String layerType) {
186        Class<? extends SessionLayerImporter> importerClass = sessionLayerImporters.get(layerType);
187        if (importerClass == null)
188            return null;
189        SessionLayerImporter importer = null;
190        try {
191            importer = importerClass.getConstructor().newInstance();
192        } catch (ReflectiveOperationException e) {
193            throw new JosmRuntimeException(e);
194        }
195        return importer;
196    }
197
198    /**
199     * @return list of layers that are later added to the mapview
200     */
201    public List<Layer> getLayers() {
202        return layers;
203    }
204
205    /**
206     * @return active layer, or {@code null} if not set
207     * @since 6271
208     */
209    public Layer getActive() {
210        // layers is in reverse order because of the way TreeMap is built
211        return (active >= 0 && active < layers.size()) ? layers.get(layers.size()-1-active) : null;
212    }
213
214    /**
215     * @return actions executed in EDT after layers have been added (message dialog, etc.)
216     */
217    public List<Runnable> getPostLoadTasks() {
218        return postLoadTasks;
219    }
220
221    /**
222     * Return the viewport (map position and scale).
223     * @return the viewport; can be null when no viewport info is found in the file
224     */
225    public SessionViewportData getViewport() {
226        return viewport;
227    }
228
229    /**
230     * Return the projection choice data.
231     * @return the projection; can be null when no projection info is found in the file
232     */
233    public SessionProjectionChoiceData getProjectionChoice() {
234        return projectionChoice;
235    }
236
237    /**
238     * A class that provides some context for the individual {@link SessionLayerImporter}
239     * when doing the import.
240     */
241    public class ImportSupport {
242
243        private final String layerName;
244        private final int layerIndex;
245        private final List<LayerDependency> layerDependencies;
246
247        /**
248         * Path of the file inside the zip archive.
249         * Used as alternative return value for getFile method.
250         */
251        private String inZipPath;
252
253        /**
254         * Constructs a new {@code ImportSupport}.
255         * @param layerName layer name
256         * @param layerIndex layer index
257         * @param layerDependencies layer dependencies
258         */
259        public ImportSupport(String layerName, int layerIndex, List<LayerDependency> layerDependencies) {
260            this.layerName = layerName;
261            this.layerIndex = layerIndex;
262            this.layerDependencies = layerDependencies;
263        }
264
265        /**
266         * Add a task, e.g. a message dialog, that should
267         * be executed in EDT after all layers have been added.
268         * @param task task to run in EDT
269         */
270        public void addPostLayersTask(Runnable task) {
271            postLoadTasks.add(task);
272        }
273
274        /**
275         * Return an InputStream for a URI from a .jos/.joz file.
276         *
277         * The following forms are supported:
278         *
279         * - absolute file (both .jos and .joz):
280         *         "file:///home/user/data.osm"
281         *         "file:/home/user/data.osm"
282         *         "file:///C:/files/data.osm"
283         *         "file:/C:/file/data.osm"
284         *         "/home/user/data.osm"
285         *         "C:\files\data.osm"          (not a URI, but recognized by File constructor on Windows systems)
286         * - standalone .jos files:
287         *     - relative uri:
288         *         "save/data.osm"
289         *         "../project2/data.osm"
290         * - for .joz files:
291         *     - file inside zip archive:
292         *         "layers/01/data.osm"
293         *     - relativ to the .joz file:
294         *         "../save/data.osm"           ("../" steps out of the archive)
295         * @param uriStr URI as string
296         * @return the InputStream
297         *
298         * @throws IOException Thrown when no Stream can be opened for the given URI, e.g. when the linked file has been deleted.
299         */
300        public InputStream getInputStream(String uriStr) throws IOException {
301            File file = getFile(uriStr);
302            if (file != null) {
303                try {
304                    return new BufferedInputStream(Compression.getUncompressedFileInputStream(file));
305                } catch (FileNotFoundException e) {
306                    throw new IOException(tr("File ''{0}'' does not exist.", file.getPath()), e);
307                }
308            } else if (inZipPath != null) {
309                ZipEntry entry = zipFile.getEntry(inZipPath);
310                if (entry != null) {
311                    return zipFile.getInputStream(entry);
312                }
313            }
314            throw new IOException(tr("Unable to locate file  ''{0}''.", uriStr));
315        }
316
317        /**
318         * Return a File for a URI from a .jos/.joz file.
319         *
320         * Returns null if the URI points to a file inside the zip archive.
321         * In this case, inZipPath will be set to the corresponding path.
322         * @param uriStr the URI as string
323         * @return the resulting File
324         * @throws IOException if any I/O error occurs
325         */
326        public File getFile(String uriStr) throws IOException {
327            inZipPath = null;
328            try {
329                URI uri = new URI(uriStr);
330                if ("file".equals(uri.getScheme()))
331                    // absolute path
332                    return new File(uri);
333                else if (uri.getScheme() == null) {
334                    // Check if this is an absolute path without 'file:' scheme part.
335                    // At this point, (as an exception) platform dependent path separator will be recognized.
336                    // (This form is discouraged, only for users that like to copy and paste a path manually.)
337                    File file = new File(uriStr);
338                    if (file.isAbsolute())
339                        return file;
340                    else {
341                        // for relative paths, only forward slashes are permitted
342                        if (isZip()) {
343                            if (uri.getPath().startsWith("../")) {
344                                // relative to session file - "../" step out of the archive
345                                String relPath = uri.getPath().substring(3);
346                                return new File(sessionFileURI.resolve(relPath));
347                            } else {
348                                // file inside zip archive
349                                inZipPath = uriStr;
350                                return null;
351                            }
352                        } else
353                            return new File(sessionFileURI.resolve(uri));
354                    }
355                } else
356                    throw new IOException(tr("Unsupported scheme ''{0}'' in URI ''{1}''.", uri.getScheme(), uriStr));
357            } catch (URISyntaxException | IllegalArgumentException e) {
358                throw new IOException(e);
359            }
360        }
361
362        /**
363         * Determines if we are reading from a .joz file.
364         * @return {@code true} if we are reading from a .joz file, {@code false} otherwise
365         */
366        public boolean isZip() {
367            return zip;
368        }
369
370        /**
371         * Name of the layer that is currently imported.
372         * @return layer name
373         */
374        public String getLayerName() {
375            return layerName;
376        }
377
378        /**
379         * Index of the layer that is currently imported.
380         * @return layer index
381         */
382        public int getLayerIndex() {
383            return layerIndex;
384        }
385
386        /**
387         * Dependencies - maps the layer index to the importer of the given
388         * layer. All the dependent importers have loaded completely at this point.
389         * @return layer dependencies
390         */
391        public List<LayerDependency> getLayerDependencies() {
392            return layerDependencies;
393        }
394
395        @Override
396        public String toString() {
397            return "ImportSupport [layerName=" + layerName + ", layerIndex=" + layerIndex + ", layerDependencies="
398                    + layerDependencies + ", inZipPath=" + inZipPath + ']';
399        }
400    }
401
402    public static class LayerDependency {
403        private final Integer index;
404        private final Layer layer;
405        private final SessionLayerImporter importer;
406
407        public LayerDependency(Integer index, Layer layer, SessionLayerImporter importer) {
408            this.index = index;
409            this.layer = layer;
410            this.importer = importer;
411        }
412
413        public SessionLayerImporter getImporter() {
414            return importer;
415        }
416
417        public Integer getIndex() {
418            return index;
419        }
420
421        public Layer getLayer() {
422            return layer;
423        }
424    }
425
426    private static void error(String msg) throws IllegalDataException {
427        throw new IllegalDataException(msg);
428    }
429
430    private void parseJos(Document doc, ProgressMonitor progressMonitor) throws IllegalDataException {
431        Element root = doc.getDocumentElement();
432        if (!"josm-session".equals(root.getTagName())) {
433            error(tr("Unexpected root element ''{0}'' in session file", root.getTagName()));
434        }
435        String version = root.getAttribute("version");
436        if (!"0.1".equals(version)) {
437            error(tr("Version ''{0}'' of session file is not supported. Expected: 0.1", version));
438        }
439
440        viewport = readViewportData(root);
441        projectionChoice = readProjectionChoiceData(root);
442
443        Element layersEl = getElementByTagName(root, "layers");
444        if (layersEl == null) return;
445
446        String activeAtt = layersEl.getAttribute("active");
447        try {
448            active = !activeAtt.isEmpty() ? (Integer.parseInt(activeAtt)-1) : -1;
449        } catch (NumberFormatException e) {
450            Logging.warn("Unsupported value for 'active' layer attribute. Ignoring it. Error was: "+e.getMessage());
451            active = -1;
452        }
453
454        MultiMap<Integer, Integer> deps = new MultiMap<>();
455        Map<Integer, Element> elems = new HashMap<>();
456
457        NodeList nodes = layersEl.getChildNodes();
458
459        for (int i = 0; i < nodes.getLength(); ++i) {
460            Node node = nodes.item(i);
461            if (node.getNodeType() == Node.ELEMENT_NODE) {
462                Element e = (Element) node;
463                if ("layer".equals(e.getTagName())) {
464                    if (!e.hasAttribute("index")) {
465                        error(tr("missing mandatory attribute ''index'' for element ''layer''"));
466                    }
467                    Integer idx = null;
468                    try {
469                        idx = Integer.valueOf(e.getAttribute("index"));
470                    } catch (NumberFormatException ex) {
471                        Logging.warn(ex);
472                    }
473                    if (idx == null) {
474                        error(tr("unexpected format of attribute ''index'' for element ''layer''"));
475                    } else if (elems.containsKey(idx)) {
476                        error(tr("attribute ''index'' ({0}) for element ''layer'' must be unique", Integer.toString(idx)));
477                    }
478                    elems.put(idx, e);
479
480                    deps.putVoid(idx);
481                    String depStr = e.getAttribute("depends");
482                    if (!depStr.isEmpty()) {
483                        for (String sd : depStr.split(",")) {
484                            Integer d = null;
485                            try {
486                                d = Integer.valueOf(sd);
487                            } catch (NumberFormatException ex) {
488                                Logging.warn(ex);
489                            }
490                            if (d != null) {
491                                deps.put(idx, d);
492                            }
493                        }
494                    }
495                }
496            }
497        }
498
499        List<Integer> sorted = Utils.topologicalSort(deps);
500        final Map<Integer, Layer> layersMap = new TreeMap<>(Collections.reverseOrder());
501        final Map<Integer, SessionLayerImporter> importers = new HashMap<>();
502        final Map<Integer, String> names = new HashMap<>();
503
504        progressMonitor.setTicksCount(sorted.size());
505        LAYER: for (int idx: sorted) {
506            Element e = elems.get(idx);
507            if (e == null) {
508                error(tr("missing layer with index {0}", idx));
509                return;
510            } else if (!e.hasAttribute("name")) {
511                error(tr("missing mandatory attribute ''name'' for element ''layer''"));
512                return;
513            }
514            String name = e.getAttribute("name");
515            names.put(idx, name);
516            if (!e.hasAttribute("type")) {
517                error(tr("missing mandatory attribute ''type'' for element ''layer''"));
518                return;
519            }
520            String type = e.getAttribute("type");
521            SessionLayerImporter imp = getSessionLayerImporter(type);
522            if (imp == null && !GraphicsEnvironment.isHeadless()) {
523                CancelOrContinueDialog dialog = new CancelOrContinueDialog();
524                dialog.show(
525                        tr("Unable to load layer"),
526                        tr("Cannot load layer of type ''{0}'' because no suitable importer was found.", type),
527                        JOptionPane.WARNING_MESSAGE,
528                        progressMonitor
529                        );
530                if (dialog.isCancel()) {
531                    progressMonitor.cancel();
532                    return;
533                } else {
534                    continue;
535                }
536            } else if (imp != null) {
537                importers.put(idx, imp);
538                List<LayerDependency> depsImp = new ArrayList<>();
539                for (int d : deps.get(idx)) {
540                    SessionLayerImporter dImp = importers.get(d);
541                    if (dImp == null) {
542                        CancelOrContinueDialog dialog = new CancelOrContinueDialog();
543                        dialog.show(
544                                tr("Unable to load layer"),
545                                tr("Cannot load layer {0} because it depends on layer {1} which has been skipped.", idx, d),
546                                JOptionPane.WARNING_MESSAGE,
547                                progressMonitor
548                                );
549                        if (dialog.isCancel()) {
550                            progressMonitor.cancel();
551                            return;
552                        } else {
553                            continue LAYER;
554                        }
555                    }
556                    depsImp.add(new LayerDependency(d, layersMap.get(d), dImp));
557                }
558                ImportSupport support = new ImportSupport(name, idx, depsImp);
559                Layer layer = null;
560                Exception exception = null;
561                try {
562                    layer = imp.load(e, support, progressMonitor.createSubTaskMonitor(1, false));
563                    if (layer == null) {
564                        throw new IllegalStateException("Importer " + imp + " returned null for " + support);
565                    }
566                } catch (IllegalDataException | IllegalStateException | IOException ex) {
567                    exception = ex;
568                }
569                if (exception != null) {
570                    Logging.error(exception);
571                    if (!GraphicsEnvironment.isHeadless()) {
572                        CancelOrContinueDialog dialog = new CancelOrContinueDialog();
573                        dialog.show(
574                                tr("Error loading layer"),
575                                tr("<html>Could not load layer {0} ''{1}''.<br>Error is:<br>{2}</html>", idx,
576                                        Utils.escapeReservedCharactersHTML(name),
577                                        Utils.escapeReservedCharactersHTML(exception.getMessage())),
578                                JOptionPane.ERROR_MESSAGE,
579                                progressMonitor
580                                );
581                        if (dialog.isCancel()) {
582                            progressMonitor.cancel();
583                            return;
584                        } else {
585                            continue;
586                        }
587                    }
588                }
589
590                layersMap.put(idx, layer);
591            }
592            progressMonitor.worked(1);
593        }
594
595        layers = new ArrayList<>();
596        for (Entry<Integer, Layer> entry : layersMap.entrySet()) {
597            Layer layer = entry.getValue();
598            if (layer == null) {
599                continue;
600            }
601            Element el = elems.get(entry.getKey());
602            if (el.hasAttribute("visible")) {
603                layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
604            }
605            if (el.hasAttribute("opacity")) {
606                try {
607                    double opacity = Double.parseDouble(el.getAttribute("opacity"));
608                    layer.setOpacity(opacity);
609                } catch (NumberFormatException ex) {
610                    Logging.warn(ex);
611                }
612            }
613            layer.setName(names.get(entry.getKey()));
614            layers.add(layer);
615        }
616    }
617
618    private static SessionViewportData readViewportData(Element root) {
619        Element viewportEl = getElementByTagName(root, "viewport");
620        if (viewportEl == null) return null;
621        LatLon center = null;
622        Element centerEl = getElementByTagName(viewportEl, "center");
623        if (centerEl == null || !centerEl.hasAttribute("lat") || !centerEl.hasAttribute("lon")) return null;
624        try {
625            center = new LatLon(Double.parseDouble(centerEl.getAttribute("lat")),
626                    Double.parseDouble(centerEl.getAttribute("lon")));
627        } catch (NumberFormatException ex) {
628            Logging.warn(ex);
629        }
630        if (center == null) return null;
631        Element scaleEl = getElementByTagName(viewportEl, "scale");
632        if (scaleEl == null || !scaleEl.hasAttribute("meter-per-pixel")) return null;
633        try {
634            double scale = Double.parseDouble(scaleEl.getAttribute("meter-per-pixel"));
635            return new SessionViewportData(center, scale);
636        } catch (NumberFormatException ex) {
637            Logging.warn(ex);
638            return null;
639        }
640    }
641
642    private static SessionProjectionChoiceData readProjectionChoiceData(Element root) {
643        Element projectionEl = getElementByTagName(root, "projection");
644        if (projectionEl == null) return null;
645        Element projectionChoiceEl = getElementByTagName(projectionEl, "projection-choice");
646        if (projectionChoiceEl == null) return null;
647        Element idEl = getElementByTagName(projectionChoiceEl, "id");
648        if (idEl == null) return null;
649        String id = idEl.getTextContent();
650        Element parametersEl = getElementByTagName(projectionChoiceEl, "parameters");
651        if (parametersEl == null) return null;
652        Collection<String> parameters = new ArrayList<>();
653        NodeList paramNl = parametersEl.getElementsByTagName("param");
654        for (int i = 0; i < paramNl.getLength(); i++) {
655            Element paramEl = (Element) paramNl.item(i);
656            parameters.add(paramEl.getTextContent());
657        }
658        return new SessionProjectionChoiceData(id, parameters);
659    }
660
661    /**
662     * Show Dialog when there is an error for one layer.
663     * Ask the user whether to cancel the complete session loading or just to skip this layer.
664     *
665     * This is expected to run in a worker thread (PleaseWaitRunnable), so invokeAndWait is
666     * needed to block the current thread and wait for the result of the modal dialog from EDT.
667     */
668    private static class CancelOrContinueDialog {
669
670        private boolean cancel;
671
672        public void show(final String title, final String message, final int icon, final ProgressMonitor progressMonitor) {
673            try {
674                SwingUtilities.invokeAndWait(() -> {
675                    ExtendedDialog dlg = new ExtendedDialog(
676                            Main.parent,
677                            title,
678                            tr("Cancel"), tr("Skip layer and continue"))
679                        .setButtonIcons("cancel", "dialogs/next")
680                        .setIcon(icon)
681                        .setContent(message);
682                    cancel = dlg.showDialog().getValue() != 2;
683                });
684            } catch (InvocationTargetException | InterruptedException ex) {
685                throw new JosmRuntimeException(ex);
686            }
687        }
688
689        public boolean isCancel() {
690            return cancel;
691        }
692    }
693
694    /**
695     * Loads session from the given file.
696     * @param sessionFile session file to load
697     * @param zip {@code true} if it's a zipped session (.joz)
698     * @param progressMonitor progress monitor
699     * @throws IllegalDataException if invalid data is detected
700     * @throws IOException if any I/O error occurs
701     */
702    public void loadSession(File sessionFile, boolean zip, ProgressMonitor progressMonitor) throws IllegalDataException, IOException {
703        try (InputStream josIS = createInputStream(sessionFile, zip)) {
704            loadSession(josIS, sessionFile.toURI(), zip, progressMonitor != null ? progressMonitor : NullProgressMonitor.INSTANCE);
705        }
706    }
707
708    private InputStream createInputStream(File sessionFile, boolean zip) throws IOException, IllegalDataException {
709        if (zip) {
710            try {
711                zipFile = new ZipFile(sessionFile, StandardCharsets.UTF_8);
712                return getZipInputStream(zipFile);
713            } catch (ZipException ex) {
714                throw new IOException(ex);
715            }
716        } else {
717            return Files.newInputStream(sessionFile.toPath());
718        }
719    }
720
721    private static InputStream getZipInputStream(ZipFile zipFile) throws IOException, IllegalDataException {
722        ZipEntry josEntry = null;
723        Enumeration<? extends ZipEntry> entries = zipFile.entries();
724        while (entries.hasMoreElements()) {
725            ZipEntry entry = entries.nextElement();
726            if (Utils.hasExtension(entry.getName(), "jos")) {
727                josEntry = entry;
728                break;
729            }
730        }
731        if (josEntry == null) {
732            error(tr("expected .jos file inside .joz archive"));
733        }
734        return zipFile.getInputStream(josEntry);
735    }
736
737    private void loadSession(InputStream josIS, URI sessionFileURI, boolean zip, ProgressMonitor progressMonitor)
738            throws IOException, IllegalDataException {
739
740        this.sessionFileURI = sessionFileURI;
741        this.zip = zip;
742
743        try {
744            parseJos(Utils.parseSafeDOM(josIS), progressMonitor);
745        } catch (SAXException e) {
746            throw new IllegalDataException(e);
747        } catch (ParserConfigurationException e) {
748            throw new IOException(e);
749        }
750    }
751
752    private static Element getElementByTagName(Element root, String name) {
753        NodeList els = root.getElementsByTagName(name);
754        return els.getLength() > 0 ? (Element) els.item(0) : null;
755    }
756}