source: josm/trunk/src/org/openstreetmap/josm/io/session/SessionReader.java@ 9087

Last change on this file since 9087 was 9087, checked in by Don-vip, 8 years ago

sonar - fix some errors, mainly NPEs

  • Property svn:eol-style set to native
File size: 24.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.session;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.io.BufferedInputStream;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.FileNotFoundException;
11import java.io.IOException;
12import java.io.InputStream;
13import java.lang.reflect.InvocationTargetException;
14import java.net.URI;
15import java.net.URISyntaxException;
16import java.nio.charset.StandardCharsets;
17import java.util.ArrayList;
18import java.util.Collections;
19import java.util.Enumeration;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23import java.util.Map.Entry;
24import java.util.TreeMap;
25import java.util.zip.ZipEntry;
26import java.util.zip.ZipException;
27import java.util.zip.ZipFile;
28
29import javax.swing.JOptionPane;
30import javax.swing.SwingUtilities;
31import javax.xml.parsers.DocumentBuilder;
32import javax.xml.parsers.DocumentBuilderFactory;
33import javax.xml.parsers.ParserConfigurationException;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.data.ViewportData;
37import org.openstreetmap.josm.data.coor.EastNorth;
38import org.openstreetmap.josm.data.coor.LatLon;
39import org.openstreetmap.josm.data.projection.Projection;
40import org.openstreetmap.josm.data.projection.Projections;
41import org.openstreetmap.josm.gui.ExtendedDialog;
42import org.openstreetmap.josm.gui.layer.Layer;
43import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
44import org.openstreetmap.josm.gui.progress.ProgressMonitor;
45import org.openstreetmap.josm.io.IllegalDataException;
46import org.openstreetmap.josm.tools.MultiMap;
47import org.openstreetmap.josm.tools.Utils;
48import org.w3c.dom.Document;
49import org.w3c.dom.Element;
50import org.w3c.dom.Node;
51import org.w3c.dom.NodeList;
52import org.xml.sax.SAXException;
53
54/**
55 * Reads a .jos session file and loads the layers in the process.
56 *
57 */
58public class SessionReader {
59
60 private static final Map<String, Class<? extends SessionLayerImporter>> sessionLayerImporters = new HashMap<>();
61
62 static {
63 registerSessionLayerImporter("osm-data", OsmDataSessionImporter.class);
64 registerSessionLayerImporter("imagery", ImagerySessionImporter.class);
65 registerSessionLayerImporter("tracks", GpxTracksSessionImporter.class);
66 registerSessionLayerImporter("geoimage", GeoImageSessionImporter.class);
67 registerSessionLayerImporter("markers", MarkerSessionImporter.class);
68 }
69
70 public static void registerSessionLayerImporter(String layerType, Class<? extends SessionLayerImporter> importer) {
71 sessionLayerImporters.put(layerType, importer);
72 }
73
74 public static SessionLayerImporter getSessionLayerImporter(String layerType) {
75 Class<? extends SessionLayerImporter> importerClass = sessionLayerImporters.get(layerType);
76 if (importerClass == null)
77 return null;
78 SessionLayerImporter importer = null;
79 try {
80 importer = importerClass.newInstance();
81 } catch (InstantiationException | IllegalAccessException e) {
82 throw new RuntimeException(e);
83 }
84 return importer;
85 }
86
87 private URI sessionFileURI;
88 private boolean zip; // true, if session file is a .joz file; false if it is a .jos file
89 private ZipFile zipFile;
90 private List<Layer> layers = new ArrayList<>();
91 private int active = -1;
92 private final List<Runnable> postLoadTasks = new ArrayList<>();
93 private ViewportData viewport;
94
95 /**
96 * @return list of layers that are later added to the mapview
97 */
98 public List<Layer> getLayers() {
99 return layers;
100 }
101
102 /**
103 * @return active layer, or {@code null} if not set
104 * @since 6271
105 */
106 public Layer getActive() {
107 // layers is in reverse order because of the way TreeMap is built
108 return (active >= 0 && active < layers.size()) ? layers.get(layers.size()-1-active) : null;
109 }
110
111 /**
112 * @return actions executed in EDT after layers have been added (message dialog, etc.)
113 */
114 public List<Runnable> getPostLoadTasks() {
115 return postLoadTasks;
116 }
117
118 /**
119 * Return the viewport (map position and scale).
120 * @return The viewport. Can be null when no viewport info is found in the file.
121 */
122 public ViewportData getViewport() {
123 return viewport;
124 }
125
126 public class ImportSupport {
127
128 private final String layerName;
129 private final int layerIndex;
130 private final List<LayerDependency> layerDependencies;
131
132 public ImportSupport(String layerName, int layerIndex, List<LayerDependency> layerDependencies) {
133 this.layerName = layerName;
134 this.layerIndex = layerIndex;
135 this.layerDependencies = layerDependencies;
136 }
137
138 /**
139 * Path of the file inside the zip archive.
140 * Used as alternative return value for getFile method.
141 */
142 private String inZipPath;
143
144 /**
145 * Add a task, e.g. a message dialog, that should
146 * be executed in EDT after all layers have been added.
147 */
148 public void addPostLayersTask(Runnable task) {
149 postLoadTasks.add(task);
150 }
151
152 /**
153 * Return an InputStream for a URI from a .jos/.joz file.
154 *
155 * The following forms are supported:
156 *
157 * - absolute file (both .jos and .joz):
158 * "file:///home/user/data.osm"
159 * "file:/home/user/data.osm"
160 * "file:///C:/files/data.osm"
161 * "file:/C:/file/data.osm"
162 * "/home/user/data.osm"
163 * "C:\files\data.osm" (not a URI, but recognized by File constructor on Windows systems)
164 * - standalone .jos files:
165 * - relative uri:
166 * "save/data.osm"
167 * "../project2/data.osm"
168 * - for .joz files:
169 * - file inside zip archive:
170 * "layers/01/data.osm"
171 * - relativ to the .joz file:
172 * "../save/data.osm" ("../" steps out of the archive)
173 * @param uriStr URI as string
174 * @return the InputStream
175 *
176 * @throws IOException Thrown when no Stream can be opened for the given URI, e.g. when the linked file has been deleted.
177 */
178 public InputStream getInputStream(String uriStr) throws IOException {
179 File file = getFile(uriStr);
180 if (file != null) {
181 try {
182 return new BufferedInputStream(new FileInputStream(file));
183 } catch (FileNotFoundException e) {
184 throw new IOException(tr("File ''{0}'' does not exist.", file.getPath()), e);
185 }
186 } else if (inZipPath != null) {
187 ZipEntry entry = zipFile.getEntry(inZipPath);
188 if (entry != null) {
189 return zipFile.getInputStream(entry);
190 }
191 }
192 throw new IOException(tr("Unable to locate file ''{0}''.", uriStr));
193 }
194
195 /**
196 * Return a File for a URI from a .jos/.joz file.
197 *
198 * Returns null if the URI points to a file inside the zip archive.
199 * In this case, inZipPath will be set to the corresponding path.
200 * @param uriStr the URI as string
201 * @return the resulting File
202 * @throws IOException if any I/O error occurs
203 */
204 public File getFile(String uriStr) throws IOException {
205 inZipPath = null;
206 try {
207 URI uri = new URI(uriStr);
208 if ("file".equals(uri.getScheme()))
209 // absolute path
210 return new File(uri);
211 else if (uri.getScheme() == null) {
212 // Check if this is an absolute path without 'file:' scheme part.
213 // At this point, (as an exception) platform dependent path separator will be recognized.
214 // (This form is discouraged, only for users that like to copy and paste a path manually.)
215 File file = new File(uriStr);
216 if (file.isAbsolute())
217 return file;
218 else {
219 // for relative paths, only forward slashes are permitted
220 if (isZip()) {
221 if (uri.getPath().startsWith("../")) {
222 // relative to session file - "../" step out of the archive
223 String relPath = uri.getPath().substring(3);
224 return new File(sessionFileURI.resolve(relPath));
225 } else {
226 // file inside zip archive
227 inZipPath = uriStr;
228 return null;
229 }
230 } else
231 return new File(sessionFileURI.resolve(uri));
232 }
233 } else
234 throw new IOException(tr("Unsupported scheme ''{0}'' in URI ''{1}''.", uri.getScheme(), uriStr));
235 } catch (URISyntaxException e) {
236 throw new IOException(e);
237 }
238 }
239
240 /**
241 * Determines if we are reading from a .joz file.
242 * @return {@code true} if we are reading from a .joz file, {@code false} otherwise
243 */
244 public boolean isZip() {
245 return zip;
246 }
247
248 /**
249 * Name of the layer that is currently imported.
250 * @return layer name
251 */
252 public String getLayerName() {
253 return layerName;
254 }
255
256 /**
257 * Index of the layer that is currently imported.
258 * @return layer index
259 */
260 public int getLayerIndex() {
261 return layerIndex;
262 }
263
264 /**
265 * Dependencies - maps the layer index to the importer of the given
266 * layer. All the dependent importers have loaded completely at this point.
267 * @return layer dependencies
268 */
269 public List<LayerDependency> getLayerDependencies() {
270 return layerDependencies;
271 }
272 }
273
274 public static class LayerDependency {
275 private final Integer index;
276 private final Layer layer;
277 private final SessionLayerImporter importer;
278
279 public LayerDependency(Integer index, Layer layer, SessionLayerImporter importer) {
280 this.index = index;
281 this.layer = layer;
282 this.importer = importer;
283 }
284
285 public SessionLayerImporter getImporter() {
286 return importer;
287 }
288
289 public Integer getIndex() {
290 return index;
291 }
292
293 public Layer getLayer() {
294 return layer;
295 }
296 }
297
298 private static void error(String msg) throws IllegalDataException {
299 throw new IllegalDataException(msg);
300 }
301
302 private void parseJos(Document doc, ProgressMonitor progressMonitor) throws IllegalDataException {
303 Element root = doc.getDocumentElement();
304 if (!"josm-session".equals(root.getTagName())) {
305 error(tr("Unexpected root element ''{0}'' in session file", root.getTagName()));
306 }
307 String version = root.getAttribute("version");
308 if (!"0.1".equals(version)) {
309 error(tr("Version ''{0}'' of session file is not supported. Expected: 0.1", version));
310 }
311
312 Element viewportEl = getElementByTagName(root, "viewport");
313 if (viewportEl != null) {
314 EastNorth center = null;
315 Element centerEl = getElementByTagName(viewportEl, "center");
316 if (centerEl != null && centerEl.hasAttribute("lat") && centerEl.hasAttribute("lon")) {
317 try {
318 LatLon centerLL = new LatLon(Double.parseDouble(centerEl.getAttribute("lat")),
319 Double.parseDouble(centerEl.getAttribute("lon")));
320 center = Projections.project(centerLL);
321 } catch (NumberFormatException ex) {
322 Main.warn(ex);
323 }
324 }
325 if (center != null) {
326 Element scaleEl = getElementByTagName(viewportEl, "scale");
327 if (scaleEl != null && scaleEl.hasAttribute("meter-per-pixel")) {
328 try {
329 double meterPerPixel = Double.parseDouble(scaleEl.getAttribute("meter-per-pixel"));
330 Projection proj = Main.getProjection();
331 // Get a "typical" distance in east/north units that
332 // corresponds to a couple of pixels. Shouldn't be too
333 // large, to keep it within projection bounds and
334 // not too small to avoid rounding errors.
335 double dist = 0.01 * proj.getDefaultZoomInPPD();
336 LatLon ll1 = proj.eastNorth2latlon(new EastNorth(center.east() - dist, center.north()));
337 LatLon ll2 = proj.eastNorth2latlon(new EastNorth(center.east() + dist, center.north()));
338 double meterPerEasting = ll1.greatCircleDistance(ll2) / dist / 2;
339 double scale = meterPerPixel / meterPerEasting; // unit: easting per pixel
340 viewport = new ViewportData(center, scale);
341 } catch (NumberFormatException ex) {
342 Main.warn(ex);
343 }
344 }
345 }
346 }
347
348 Element layersEl = getElementByTagName(root, "layers");
349 if (layersEl == null) return;
350
351 String activeAtt = layersEl.getAttribute("active");
352 try {
353 active = (activeAtt != null && !activeAtt.isEmpty()) ? Integer.parseInt(activeAtt)-1 : -1;
354 } catch (NumberFormatException e) {
355 Main.warn("Unsupported value for 'active' layer attribute. Ignoring it. Error was: "+e.getMessage());
356 active = -1;
357 }
358
359 MultiMap<Integer, Integer> deps = new MultiMap<>();
360 Map<Integer, Element> elems = new HashMap<>();
361
362 NodeList nodes = layersEl.getChildNodes();
363
364 for (int i = 0; i < nodes.getLength(); ++i) {
365 Node node = nodes.item(i);
366 if (node.getNodeType() == Node.ELEMENT_NODE) {
367 Element e = (Element) node;
368 if ("layer".equals(e.getTagName())) {
369 if (!e.hasAttribute("index")) {
370 error(tr("missing mandatory attribute ''index'' for element ''layer''"));
371 }
372 Integer idx = null;
373 try {
374 idx = Integer.valueOf(e.getAttribute("index"));
375 } catch (NumberFormatException ex) {
376 Main.warn(ex);
377 }
378 if (idx == null) {
379 error(tr("unexpected format of attribute ''index'' for element ''layer''"));
380 }
381 if (elems.containsKey(idx)) {
382 error(tr("attribute ''index'' ({0}) for element ''layer'' must be unique", Integer.toString(idx)));
383 }
384 elems.put(idx, e);
385
386 deps.putVoid(idx);
387 String depStr = e.getAttribute("depends");
388 if (depStr != null && !depStr.isEmpty()) {
389 for (String sd : depStr.split(",")) {
390 Integer d = null;
391 try {
392 d = Integer.valueOf(sd);
393 } catch (NumberFormatException ex) {
394 Main.warn(ex);
395 }
396 if (d != null) {
397 deps.put(idx, d);
398 }
399 }
400 }
401 }
402 }
403 }
404
405 List<Integer> sorted = Utils.topologicalSort(deps);
406 final Map<Integer, Layer> layersMap = new TreeMap<>(Collections.reverseOrder());
407 final Map<Integer, SessionLayerImporter> importers = new HashMap<>();
408 final Map<Integer, String> names = new HashMap<>();
409
410 progressMonitor.setTicksCount(sorted.size());
411 LAYER: for (int idx: sorted) {
412 Element e = elems.get(idx);
413 if (e == null) {
414 error(tr("missing layer with index {0}", idx));
415 return;
416 } else if (!e.hasAttribute("name")) {
417 error(tr("missing mandatory attribute ''name'' for element ''layer''"));
418 return;
419 }
420 String name = e.getAttribute("name");
421 names.put(idx, name);
422 if (!e.hasAttribute("type")) {
423 error(tr("missing mandatory attribute ''type'' for element ''layer''"));
424 return;
425 }
426 String type = e.getAttribute("type");
427 SessionLayerImporter imp = getSessionLayerImporter(type);
428 if (imp == null && !GraphicsEnvironment.isHeadless()) {
429 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
430 dialog.show(
431 tr("Unable to load layer"),
432 tr("Cannot load layer of type ''{0}'' because no suitable importer was found.", type),
433 JOptionPane.WARNING_MESSAGE,
434 progressMonitor
435 );
436 if (dialog.isCancel()) {
437 progressMonitor.cancel();
438 return;
439 } else {
440 continue;
441 }
442 } else if (imp != null) {
443 importers.put(idx, imp);
444 List<LayerDependency> depsImp = new ArrayList<>();
445 for (int d : deps.get(idx)) {
446 SessionLayerImporter dImp = importers.get(d);
447 if (dImp == null) {
448 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
449 dialog.show(
450 tr("Unable to load layer"),
451 tr("Cannot load layer {0} because it depends on layer {1} which has been skipped.", idx, d),
452 JOptionPane.WARNING_MESSAGE,
453 progressMonitor
454 );
455 if (dialog.isCancel()) {
456 progressMonitor.cancel();
457 return;
458 } else {
459 continue LAYER;
460 }
461 }
462 depsImp.add(new LayerDependency(d, layersMap.get(d), dImp));
463 }
464 ImportSupport support = new ImportSupport(name, idx, depsImp);
465 Layer layer = null;
466 Exception exception = null;
467 try {
468 layer = imp.load(e, support, progressMonitor.createSubTaskMonitor(1, false));
469 } catch (IllegalDataException | IOException ex) {
470 exception = ex;
471 }
472 if (exception != null) {
473 Main.error(exception);
474 if (!GraphicsEnvironment.isHeadless()) {
475 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
476 dialog.show(
477 tr("Error loading layer"),
478 tr("<html>Could not load layer {0} ''{1}''.<br>Error is:<br>{2}</html>", idx, name, exception.getMessage()),
479 JOptionPane.ERROR_MESSAGE,
480 progressMonitor
481 );
482 if (dialog.isCancel()) {
483 progressMonitor.cancel();
484 return;
485 } else {
486 continue;
487 }
488 }
489 }
490
491 if (layer == null) throw new RuntimeException();
492 layersMap.put(idx, layer);
493 }
494 progressMonitor.worked(1);
495 }
496
497 layers = new ArrayList<>();
498 for (Entry<Integer, Layer> entry : layersMap.entrySet()) {
499 Layer layer = entry.getValue();
500 if (layer == null) {
501 continue;
502 }
503 Element el = elems.get(entry.getKey());
504 if (el.hasAttribute("visible")) {
505 layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
506 }
507 if (el.hasAttribute("opacity")) {
508 try {
509 double opacity = Double.parseDouble(el.getAttribute("opacity"));
510 layer.setOpacity(opacity);
511 } catch (NumberFormatException ex) {
512 Main.warn(ex);
513 }
514 }
515 layer.setName(names.get(entry.getKey()));
516 layers.add(layer);
517 }
518 }
519
520 /**
521 * Show Dialog when there is an error for one layer.
522 * Ask the user whether to cancel the complete session loading or just to skip this layer.
523 *
524 * This is expected to run in a worker thread (PleaseWaitRunnable), so invokeAndWait is
525 * needed to block the current thread and wait for the result of the modal dialog from EDT.
526 */
527 private static class CancelOrContinueDialog {
528
529 private boolean cancel;
530
531 public void show(final String title, final String message, final int icon, final ProgressMonitor progressMonitor) {
532 try {
533 SwingUtilities.invokeAndWait(new Runnable() {
534 @Override public void run() {
535 ExtendedDialog dlg = new ExtendedDialog(
536 Main.parent,
537 title,
538 new String[] {tr("Cancel"), tr("Skip layer and continue")}
539 );
540 dlg.setButtonIcons(new String[] {"cancel", "dialogs/next"});
541 dlg.setIcon(icon);
542 dlg.setContent(message);
543 dlg.showDialog();
544 cancel = dlg.getValue() != 2;
545 }
546 });
547 } catch (InvocationTargetException | InterruptedException ex) {
548 throw new RuntimeException(ex);
549 }
550 }
551
552 public boolean isCancel() {
553 return cancel;
554 }
555 }
556
557 public void loadSession(File sessionFile, boolean zip, ProgressMonitor progressMonitor) throws IllegalDataException, IOException {
558 if (progressMonitor == null) {
559 progressMonitor = NullProgressMonitor.INSTANCE;
560 }
561
562 try (InputStream josIS = createInputStream(sessionFile, zip)) {
563 loadSession(josIS, sessionFile.toURI(), zip, progressMonitor);
564 }
565 }
566
567 private InputStream createInputStream(File sessionFile, boolean zip) throws IOException, IllegalDataException {
568 if (zip) {
569 try {
570 zipFile = new ZipFile(sessionFile, StandardCharsets.UTF_8);
571 return getZipInputStream(zipFile);
572 } catch (ZipException ze) {
573 throw new IOException(ze);
574 }
575 } else {
576 try {
577 return new FileInputStream(sessionFile);
578 } catch (FileNotFoundException ex) {
579 throw new IOException(ex);
580 }
581 }
582 }
583
584 private static InputStream getZipInputStream(ZipFile zipFile) throws ZipException, IOException, IllegalDataException {
585 ZipEntry josEntry = null;
586 Enumeration<? extends ZipEntry> entries = zipFile.entries();
587 while (entries.hasMoreElements()) {
588 ZipEntry entry = entries.nextElement();
589 if (Utils.hasExtension(entry.getName(), "jos")) {
590 josEntry = entry;
591 break;
592 }
593 }
594 if (josEntry == null) {
595 error(tr("expected .jos file inside .joz archive"));
596 }
597 return zipFile.getInputStream(josEntry);
598 }
599
600 private void loadSession(InputStream josIS, URI sessionFileURI, boolean zip, ProgressMonitor progressMonitor)
601 throws IOException, IllegalDataException {
602
603 this.sessionFileURI = sessionFileURI;
604 this.zip = zip;
605
606 try {
607 DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
608 builderFactory.setValidating(false);
609 builderFactory.setNamespaceAware(true);
610 DocumentBuilder builder = builderFactory.newDocumentBuilder();
611 Document document = builder.parse(josIS);
612 parseJos(document, progressMonitor);
613 } catch (SAXException e) {
614 throw new IllegalDataException(e);
615 } catch (ParserConfigurationException e) {
616 throw new IOException(e);
617 }
618 }
619
620 private static Element getElementByTagName(Element root, String name) {
621 NodeList els = root.getElementsByTagName(name);
622 if (els.getLength() == 0) return null;
623 return (Element) els.item(0);
624 }
625}
Note: See TracBrowser for help on using the repository browser.