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