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