001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.io.session;
003    
004    import java.io.BufferedOutputStream;
005    import java.io.File;
006    import java.io.FileNotFoundException;
007    import java.io.FileOutputStream;
008    import java.io.IOException;
009    import java.io.OutputStream;
010    import java.io.OutputStreamWriter;
011    import java.lang.reflect.Constructor;
012    import java.util.ArrayList;
013    import java.util.HashMap;
014    import java.util.List;
015    import java.util.Map;
016    import java.util.Set;
017    import java.util.zip.ZipEntry;
018    import java.util.zip.ZipOutputStream;
019    
020    import javax.xml.parsers.DocumentBuilder;
021    import javax.xml.parsers.DocumentBuilderFactory;
022    import javax.xml.parsers.ParserConfigurationException;
023    import javax.xml.transform.OutputKeys;
024    import javax.xml.transform.Transformer;
025    import javax.xml.transform.TransformerException;
026    import javax.xml.transform.TransformerFactory;
027    import javax.xml.transform.dom.DOMSource;
028    import javax.xml.transform.stream.StreamResult;
029    
030    import org.openstreetmap.josm.gui.layer.GpxLayer;
031    import org.openstreetmap.josm.gui.layer.Layer;
032    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
033    import org.openstreetmap.josm.gui.layer.TMSLayer;
034    import org.openstreetmap.josm.gui.layer.WMSLayer;
035    import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
036    import org.openstreetmap.josm.tools.MultiMap;
037    import org.openstreetmap.josm.tools.Utils;
038    import org.w3c.dom.Document;
039    import org.w3c.dom.Element;
040    import org.w3c.dom.Text;
041    
042    public class SessionWriter {
043    
044        private static Map<Class<? extends Layer>, Class<? extends SessionLayerExporter>> sessionLayerExporters =
045                new HashMap<Class<? extends Layer>, Class<? extends SessionLayerExporter>>();
046        static {
047            registerSessionLayerExporter(OsmDataLayer.class , OsmDataSessionExporter.class);
048            registerSessionLayerExporter(TMSLayer.class , ImagerySessionExporter.class);
049            registerSessionLayerExporter(WMSLayer.class , ImagerySessionExporter.class);
050            registerSessionLayerExporter(GpxLayer.class , GpxTracksSessionExporter.class);
051            registerSessionLayerExporter(GeoImageLayer.class , GeoImageSessionExporter.class);
052        }
053    
054        /**
055         * Register a session layer exporter.
056         *
057         * The exporter class must have an one-argument constructor with layerClass as formal parameter type.
058         */
059        public static void registerSessionLayerExporter(Class<? extends Layer> layerClass, Class<? extends SessionLayerExporter> exporter) {
060            sessionLayerExporters.put(layerClass, exporter);
061        }
062    
063        public static SessionLayerExporter getSessionLayerExporter(Layer layer) {
064            Class<? extends Layer> layerClass = layer.getClass();
065            Class<? extends SessionLayerExporter> exporterClass = sessionLayerExporters.get(layerClass);
066            if (exporterClass == null) return null;
067            try {
068                @SuppressWarnings("unchecked")
069                Constructor<? extends SessionLayerExporter> constructor = (Constructor) exporterClass.getConstructor(layerClass);
070                return constructor.newInstance(layer);
071            } catch (Exception e) {
072                throw new RuntimeException(e);
073            }
074        }
075    
076        private List<Layer> layers;
077        private Map<Layer, SessionLayerExporter> exporters;
078        private MultiMap<Layer, Layer> dependencies;
079        private boolean zip;
080    
081        private ZipOutputStream zipOut;
082    
083        public SessionWriter(List<Layer> layers, Map<Layer, SessionLayerExporter> exporters,
084                    MultiMap<Layer, Layer> dependencies, boolean zip) {
085            this.layers = layers;
086            this.exporters = exporters;
087            this.dependencies = dependencies;
088            this.zip = zip;
089        }
090    
091        /**
092         * A class that provides some context for the individual {@link SessionLayerExporter}
093         * when doing the export.
094         */
095        public class ExportSupport {
096            private Document doc;
097            private int layerIndex;
098    
099            public ExportSupport(Document doc, int layerIndex) {
100                this.doc = doc;
101                this.layerIndex = layerIndex;
102            }
103    
104            public Element createElement(String name) {
105                return doc.createElement(name);
106            }
107    
108            public Text createTextNode(String text) {
109                return doc.createTextNode(text);
110            }
111    
112            /**
113             * Get the index of the layer that is currently exported.
114             * @return the index of the layer that is currently exported
115             */
116            public int getLayerIndex() {
117                return layerIndex;
118            }
119    
120            /**
121             * Create a file inside the zip archive.
122             *
123             * @param zipPath the path inside the zip archive, e.g. "layers/03/data.xml"
124             * @return the OutputStream you can write to. Never close the returned
125             * output stream, but make sure to flush buffers.
126             */
127            public OutputStream getOutputStreamZip(String zipPath) throws IOException {
128                if (!isZip()) throw new RuntimeException();
129                ZipEntry entry = new ZipEntry(zipPath);
130                zipOut.putNextEntry(entry);
131                return zipOut;
132            }
133    
134            /**
135             * Check, if the session is exported as a zip archive.
136             *
137             * @return true, if the session is exported as a zip archive (.joz file
138             * extension). It will always return true, if one of the
139             * {@link SessionLayerExporter} returns true for the
140             * {@link SessionLayerExporter#requiresZip()} method. Otherwise, the
141             * user can decide in the file chooser dialog.
142             */
143            public boolean isZip() {
144                return zip;
145            }
146        }
147    
148        public Document createJosDocument() throws IOException {
149            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
150            builderFactory.setValidating(false);
151            builderFactory.setNamespaceAware(true);
152            DocumentBuilder builder = null;
153            try {
154                builder = builderFactory.newDocumentBuilder();
155            } catch (ParserConfigurationException e) {
156                throw new RuntimeException(e);
157            }
158            Document doc = builder.newDocument();
159    
160            Element root = doc.createElement("josm-session");
161            root.setAttribute("version", "0.1");
162            doc.appendChild(root);
163    
164            Element layersEl = doc.createElement("layers");
165            root.appendChild(layersEl);
166    
167            for (int index=0; index<layers.size(); ++index) {
168                Layer layer = layers.get(index);
169                SessionLayerExporter exporter = exporters.get(layer);
170                ExportSupport support = new ExportSupport(doc, index+1);
171                Element el = exporter.export(support);
172                el.setAttribute("index", Integer.toString(index+1));
173                el.setAttribute("name", layer.getName());
174                Set<Layer> deps = dependencies.get(layer);
175                if (deps.size() > 0) {
176                    List<Integer> depsInt = new ArrayList<Integer>();
177                    for (Layer depLayer : deps) {
178                        int depIndex = layers.indexOf(depLayer);
179                        if (depIndex == -1) throw new AssertionError();
180                        depsInt.add(depIndex+1);
181                    }
182                    el.setAttribute("depends", Utils.join(",", depsInt));
183                }
184                layersEl.appendChild(el);
185            }
186            return doc;
187        }
188    
189        public void writeJos(Document doc, OutputStream out) throws IOException {
190            try {
191                OutputStreamWriter writer = new OutputStreamWriter(out, "utf-8");
192                writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
193                TransformerFactory transfac = TransformerFactory.newInstance();
194                Transformer trans = transfac.newTransformer();
195                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
196                trans.setOutputProperty(OutputKeys.INDENT, "yes");
197                trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
198                StreamResult result = new StreamResult(writer);
199                DOMSource source = new DOMSource(doc);
200                trans.transform(source, result);
201            } catch (TransformerException e) {
202                throw new RuntimeException(e);
203            }
204        }
205    
206        public void write(File f) throws IOException {
207            OutputStream out = null;
208            try {
209                out = new FileOutputStream(f);
210            } catch (FileNotFoundException e) {
211                throw new IOException(e);
212            }
213            write(out);
214        }
215    
216        public void write (OutputStream out) throws IOException {
217            if (zip) {
218                zipOut = new ZipOutputStream(new BufferedOutputStream(out));
219            }
220            Document doc = createJosDocument(); // as side effect, files may be added to zipOut
221            if (zip) {
222                ZipEntry entry = new ZipEntry("session.jos");
223                zipOut.putNextEntry(entry);
224                writeJos(doc, zipOut);
225                zipOut.close();
226            } else {
227                writeJos(doc, new BufferedOutputStream(out));
228            }
229            Utils.close(out);
230        }
231    }