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