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 }