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}