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