source: josm/trunk/src/org/openstreetmap/josm/io/session/SessionWriter.java

Last change on this file was 18833, checked in by taylor.smock, 7 months ago

Fix #17052: Allow plugins to save state to session file

The primary feature request was for the TODO plugin to save the list elements for
a future session.

This allows plugins to register via ServiceLoader classes which need to be
called to save or restore their state.

In addition, this fixes an ordering issue with tests whereby the OsmApi cache
would be cleared, but the FakeOsmApi class would not recache itself when called.

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.session;
3
4import java.io.BufferedOutputStream;
5import java.io.File;
6import java.io.IOException;
7import java.io.OutputStream;
8import java.io.OutputStreamWriter;
9import java.nio.charset.StandardCharsets;
10import java.nio.file.Files;
11import java.util.Collection;
12import java.util.EnumSet;
13import java.util.HashMap;
14import java.util.List;
15import java.util.Locale;
16import java.util.Map;
17import java.util.Objects;
18import java.util.Set;
19import java.util.stream.Collectors;
20import java.util.zip.ZipEntry;
21import java.util.zip.ZipOutputStream;
22
23import javax.xml.parsers.DocumentBuilder;
24import javax.xml.parsers.ParserConfigurationException;
25import javax.xml.transform.OutputKeys;
26import javax.xml.transform.Transformer;
27import javax.xml.transform.TransformerException;
28import javax.xml.transform.dom.DOMSource;
29import javax.xml.transform.stream.StreamResult;
30
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.coor.LatLon;
33import org.openstreetmap.josm.data.projection.ProjectionRegistry;
34import org.openstreetmap.josm.gui.MainApplication;
35import org.openstreetmap.josm.gui.MapView;
36import org.openstreetmap.josm.gui.layer.GpxLayer;
37import org.openstreetmap.josm.gui.layer.GpxRouteLayer;
38import org.openstreetmap.josm.gui.layer.Layer;
39import org.openstreetmap.josm.gui.layer.NoteLayer;
40import org.openstreetmap.josm.gui.layer.OsmDataLayer;
41import org.openstreetmap.josm.gui.layer.TMSLayer;
42import org.openstreetmap.josm.gui.layer.WMSLayer;
43import org.openstreetmap.josm.gui.layer.WMTSLayer;
44import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
45import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
46import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
47import org.openstreetmap.josm.plugins.PluginHandler;
48import org.openstreetmap.josm.tools.JosmRuntimeException;
49import org.openstreetmap.josm.tools.Logging;
50import org.openstreetmap.josm.tools.MultiMap;
51import org.openstreetmap.josm.tools.Utils;
52import org.openstreetmap.josm.tools.XmlUtils;
53import org.w3c.dom.Document;
54import org.w3c.dom.Element;
55import org.w3c.dom.Text;
56
57/**
58 * Writes a .jos session file from current supported layers.
59 * @since 4685
60 */
61public class SessionWriter {
62
63 /**
64 * {@link SessionWriter} options
65 * @since 18833
66 */
67 public enum SessionWriterFlags {
68 /**
69 * Use if the file to be written needs to be a zip file
70 */
71 IS_ZIP,
72 /**
73 * Use if there are plugins that want to save information
74 */
75 SAVE_PLUGIN_INFORMATION
76 }
77
78 private static final Map<Class<? extends Layer>, Class<? extends SessionLayerExporter>> sessionLayerExporters = new HashMap<>();
79
80 private final List<Layer> layers;
81 private final int active;
82 private final Map<Layer, SessionLayerExporter> exporters;
83 private final MultiMap<Layer, Layer> dependencies;
84 private final boolean zip;
85 private final boolean plugins;
86
87 private ZipOutputStream zipOut;
88
89 static {
90 registerSessionLayerExporter(OsmDataLayer.class, OsmDataSessionExporter.class);
91 registerSessionLayerExporter(TMSLayer.class, ImagerySessionExporter.class);
92 registerSessionLayerExporter(WMSLayer.class, ImagerySessionExporter.class);
93 registerSessionLayerExporter(WMTSLayer.class, ImagerySessionExporter.class);
94 registerSessionLayerExporter(GpxLayer.class, GpxTracksSessionExporter.class);
95 registerSessionLayerExporter(GpxRouteLayer.class, GpxRoutesSessionExporter.class);
96 registerSessionLayerExporter(GeoImageLayer.class, GeoImageSessionExporter.class);
97 registerSessionLayerExporter(MarkerLayer.class, MarkerSessionExporter.class);
98 registerSessionLayerExporter(NoteLayer.class, NoteSessionExporter.class);
99 }
100
101 /**
102 * Register a session layer exporter.
103 * <p>
104 * The exporter class must have a one-argument constructor with layerClass as formal parameter type.
105 * @param layerClass layer class
106 * @param exporter exporter for this layer class
107 */
108 public static void registerSessionLayerExporter(Class<? extends Layer> layerClass, Class<? extends SessionLayerExporter> exporter) {
109 sessionLayerExporters.put(layerClass, exporter);
110 }
111
112 /**
113 * Returns the session layer exporter for the given layer.
114 * @param layer layer to export
115 * @return session layer exporter for the given layer
116 * @throws IllegalArgumentException if layer cannot be exported
117 */
118 public static SessionLayerExporter getSessionLayerExporter(Layer layer) {
119 Class<? extends Layer> layerClass = layer.getClass();
120 Class<? extends SessionLayerExporter> exporterClass = sessionLayerExporters.get(layerClass);
121 if (exporterClass == null)
122 return null;
123 try {
124 return exporterClass.getConstructor(layerClass).newInstance(layer);
125 } catch (ReflectiveOperationException e) {
126 throw new JosmRuntimeException(e);
127 }
128 }
129
130 /**
131 * Constructs a new {@code SessionWriter}.
132 * @param layers The ordered list of layers to save
133 * @param active The index of active layer in {@code layers} (starts at 0). Ignored if set to -1
134 * @param exporters The exporters to use to save layers
135 * @param dependencies layer dependencies
136 * @param zip {@code true} if a joz archive has to be created, {@code false otherwise}
137 * @since 6271
138 */
139 public SessionWriter(List<Layer> layers, int active, Map<Layer, SessionLayerExporter> exporters,
140 MultiMap<Layer, Layer> dependencies, boolean zip) {
141 this(layers, active, exporters, dependencies,
142 zip ? new SessionWriterFlags[] {SessionWriterFlags.IS_ZIP} : new SessionWriterFlags[0]);
143 }
144
145 /**
146 * Constructs a new {@code SessionWriter}.
147 * @param layers The ordered list of layers to save
148 * @param active The index of active layer in {@code layers} (starts at 0). Ignored if set to -1
149 * @param exporters The exporters to use to save layers
150 * @param dependencies layer dependencies
151 * @param flags The flags to use when writing data
152 * @since 18833
153 */
154 public SessionWriter(List<Layer> layers, int active, Map<Layer, SessionLayerExporter> exporters,
155 MultiMap<Layer, Layer> dependencies, SessionWriterFlags... flags) {
156 this.layers = layers;
157 this.active = active;
158 this.exporters = exporters;
159 this.dependencies = dependencies;
160 final EnumSet<SessionWriterFlags> flagSet = flags.length == 0 ? EnumSet.noneOf(SessionWriterFlags.class) :
161 EnumSet.of(flags[0], flags);
162 this.zip = flagSet.contains(SessionWriterFlags.IS_ZIP);
163 this.plugins = flagSet.contains(SessionWriterFlags.SAVE_PLUGIN_INFORMATION);
164 }
165
166 /**
167 * A class that provides some context for the individual {@link SessionLayerExporter}
168 * when doing the export.
169 */
170 public class ExportSupport {
171 private final Document doc;
172 private final int layerIndex;
173
174 /**
175 * Constructs a new {@code ExportSupport}.
176 * @param doc XML document
177 * @param layerIndex layer index
178 */
179 public ExportSupport(Document doc, int layerIndex) {
180 this.doc = doc;
181 this.layerIndex = layerIndex;
182 }
183
184 /**
185 * Creates an element of the type specified.
186 * @param name The name of the element type to instantiate
187 * @return A new {@code Element} object
188 * @see Document#createElement
189 */
190 public Element createElement(String name) {
191 return doc.createElement(name);
192 }
193
194 /**
195 * Creates a text node given the specified string.
196 * @param text The data for the node.
197 * @return The new {@code Text} object.
198 * @see Document#createTextNode
199 */
200 public Text createTextNode(String text) {
201 return doc.createTextNode(text);
202 }
203
204 /**
205 * Get the index of the layer that is currently exported.
206 * @return the index of the layer that is currently exported
207 */
208 public int getLayerIndex() {
209 return layerIndex;
210 }
211
212 /**
213 * Get the index of the specified layer
214 * @param layer the layer
215 * @return the index of the specified layer
216 * @since 18466
217 */
218 public int getLayerIndexOf(Layer layer) {
219 return layers.indexOf(layer) + 1;
220 }
221
222 /**
223 * Create a file inside the zip archive.
224 *
225 * @param zipPath the path inside the zip archive, e.g. "layers/03/data.xml"
226 * @return the OutputStream you can write to. Never close the returned
227 * output stream, but make sure to flush buffers.
228 * @throws IOException if any I/O error occurs
229 */
230 public OutputStream getOutputStreamZip(String zipPath) throws IOException {
231 if (!isZip()) throw new JosmRuntimeException("not zip");
232 ZipEntry entry = new ZipEntry(zipPath);
233 zipOut.putNextEntry(entry);
234 return zipOut;
235 }
236
237 /**
238 * Check, if the session is exported as a zip archive.
239 *
240 * @return true, if the session is exported as a zip archive (.joz file
241 * extension). It will always return true, if one of the
242 * {@link SessionLayerExporter} returns true for the
243 * {@link SessionLayerExporter#requiresZip()} method. Otherwise, the
244 * user can decide in the file chooser dialog.
245 */
246 public boolean isZip() {
247 return zip;
248 }
249 }
250
251 /**
252 * Creates XML (.jos) session document.
253 * @return new document
254 * @throws IOException if any I/O error occurs
255 */
256 public Document createJosDocument() throws IOException {
257 DocumentBuilder builder;
258 try {
259 builder = XmlUtils.newSafeDOMBuilder();
260 } catch (ParserConfigurationException e) {
261 throw new IOException(e);
262 }
263 Document doc = builder.newDocument();
264
265 Element root = doc.createElement("josm-session");
266 root.setAttribute("version", "0.1");
267 doc.appendChild(root);
268
269 writeViewPort(root);
270 writeProjection(root);
271
272 Element layersEl = doc.createElement("layers");
273 if (active >= 0) {
274 layersEl.setAttribute("active", Integer.toString(active+1));
275 }
276 root.appendChild(layersEl);
277
278 for (int index = 0; index < layers.size(); ++index) {
279 Layer layer = layers.get(index);
280 SessionLayerExporter exporter = exporters.get(layer);
281 ExportSupport support = new ExportSupport(doc, index+1);
282 Element el = exporter.export(support);
283 if (el == null) continue;
284 el.setAttribute("index", Integer.toString(index+1));
285 el.setAttribute("name", layer.getName());
286 el.setAttribute("visible", Boolean.toString(layer.isVisible()));
287 if (!Utils.equalsEpsilon(layer.getOpacity(), 1.0)) {
288 el.setAttribute("opacity", Double.toString(layer.getOpacity()));
289 }
290 Set<Layer> deps = dependencies.get(layer);
291 final String depends = deps == null ? "" : deps.stream().map(depLayer -> {
292 int depIndex = layers.indexOf(depLayer);
293 if (depIndex == -1) {
294 Logging.warn("Unable to find " + depLayer);
295 return null;
296 } else {
297 return Integer.toString(depIndex+1);
298 }
299 }).filter(Objects::nonNull).collect(Collectors.joining(","));
300 if (!depends.isEmpty()) {
301 el.setAttribute("depends", depends);
302 }
303 layersEl.appendChild(el);
304 }
305 return doc;
306 }
307
308 private static void writeViewPort(Element root) {
309 Document doc = root.getOwnerDocument();
310 Element viewportEl = doc.createElement("viewport");
311 root.appendChild(viewportEl);
312 Element centerEl = doc.createElement("center");
313 viewportEl.appendChild(centerEl);
314 MapView mapView = MainApplication.getMap().mapView;
315 EastNorth center = mapView.getCenter();
316 LatLon centerLL = ProjectionRegistry.getProjection().eastNorth2latlon(center);
317 centerEl.setAttribute("lat", Double.toString(centerLL.lat()));
318 centerEl.setAttribute("lon", Double.toString(centerLL.lon()));
319 Element scale = doc.createElement("scale");
320 viewportEl.appendChild(scale);
321 double dist100px = mapView.getDist100Pixel();
322 scale.setAttribute("meter-per-pixel", String.format(Locale.ROOT, "%6f", dist100px / 100));
323 }
324
325 private static void writeProjection(Element root) {
326 Document doc = root.getOwnerDocument();
327 Element projectionEl = doc.createElement("projection");
328 root.appendChild(projectionEl);
329 String pcId = ProjectionPreference.getCurrentProjectionChoiceId();
330 Element projectionChoiceEl = doc.createElement("projection-choice");
331 projectionEl.appendChild(projectionChoiceEl);
332 Element idEl = doc.createElement("id");
333 projectionChoiceEl.appendChild(idEl);
334 idEl.setTextContent(pcId);
335 Collection<String> parameters = ProjectionPreference.getSubprojectionPreference(pcId);
336 Element parametersEl = doc.createElement("parameters");
337 projectionChoiceEl.appendChild(parametersEl);
338 if (parameters != null) {
339 for (String param : parameters) {
340 Element paramEl = doc.createElement("param");
341 parametersEl.appendChild(paramEl);
342 paramEl.setTextContent(param);
343 }
344 }
345 String code = ProjectionRegistry.getProjection().toCode();
346 if (code != null) {
347 Element codeEl = doc.createElement("code");
348 projectionEl.appendChild(codeEl);
349 codeEl.setTextContent(code);
350 }
351 }
352
353 /**
354 * Writes given .jos document to an output stream.
355 * @param doc session document
356 * @param out output stream
357 * @throws IOException if any I/O error occurs
358 */
359 public void writeJos(Document doc, OutputStream out) throws IOException {
360 try {
361 OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
362 writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
363 Transformer trans = XmlUtils.newSafeTransformerFactory().newTransformer();
364 trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
365 trans.setOutputProperty(OutputKeys.INDENT, "yes");
366 trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
367 StreamResult result = new StreamResult(writer);
368 DOMSource source = new DOMSource(doc);
369 trans.transform(source, result);
370 } catch (TransformerException e) {
371 throw new JosmRuntimeException(e);
372 }
373 }
374
375 /**
376 * Writes session to given file.
377 * @param f output file
378 * @throws IOException if any I/O error occurs
379 */
380 public void write(File f) throws IOException {
381 try (OutputStream out = Files.newOutputStream(f.toPath())) {
382 write(out);
383 }
384 }
385
386 /**
387 * Writes session to given output stream.
388 * @param out output stream
389 * @throws IOException if any I/O error occurs
390 */
391 public void write(OutputStream out) throws IOException {
392 if (zip) {
393 zipOut = new ZipOutputStream(new BufferedOutputStream(out), StandardCharsets.UTF_8);
394 }
395 Document doc = createJosDocument(); // as side effect, files may be added to zipOut
396 if (zip) {
397 ZipEntry entry = new ZipEntry("session.jos");
398 zipOut.putNextEntry(entry);
399 writeJos(doc, zipOut);
400 if (this.plugins) {
401 for (PluginSessionExporter exporter : PluginHandler.load(PluginSessionExporter.class)) {
402 exporter.writeZipEntries(zipOut);
403 }
404 }
405 Utils.close(zipOut);
406 } else {
407 writeJos(doc, new BufferedOutputStream(out));
408 }
409 }
410}
Note: See TracBrowser for help on using the repository browser.