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