Index: /trunk/src/org/openstreetmap/josm/actions/SessionSaveAsAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/SessionSaveAsAction.java	(revision 4685)
+++ /trunk/src/org/openstreetmap/josm/actions/SessionSaveAsAction.java	(revision 4685)
@@ -0,0 +1,241 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.SwingConstants;
+import javax.swing.border.EtchedBorder;
+import javax.swing.filechooser.FileFilter;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.io.session.SessionLayerExporter;
+import org.openstreetmap.josm.io.session.SessionWriter;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.MultiMap;
+import org.openstreetmap.josm.tools.WindowGeometry;
+
+public class SessionSaveAsAction extends DiskAccessAction {
+
+    private List<Layer> layers;
+    private Map<Layer, SessionLayerExporter> exporters;
+    private MultiMap<Layer, Layer> dependencies;
+
+    private boolean zipRequired;
+
+    /**
+     * Construct the action with "Save" as label.
+     * @param layer Save this layer.
+     */
+    public SessionSaveAsAction() {
+        super(tr("Save Session As..."), "save_as", tr("Save the current session to a new file."), null);
+        putValue("help", ht("/Action/SessionSaveAs"));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (!isEnabled()) {
+            return;
+        }
+
+        SessionSaveAsDialog dlg = new SessionSaveAsDialog();
+        dlg.showDialog();
+        if (dlg.getValue() != 2) return;
+
+        zipRequired = false;
+        for (Layer l : layers) {
+            SessionLayerExporter ex = exporters.get(l);
+            if (ex.requiresZip()) {
+                zipRequired = true;
+                break;
+            }
+        }
+
+        String curDir = Main.pref.get("lastDirectory");
+        if (curDir.equals("")) {
+            curDir = ".";
+        }
+        JFileChooser fc = new JFileChooser(new File(curDir));
+        fc.setDialogTitle(tr("Save session"));
+        fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
+        FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
+        if (zipRequired) {
+            fc.addChoosableFileFilter(joz);
+        } else {
+            fc.addChoosableFileFilter(jos);
+            fc.addChoosableFileFilter(joz);
+            fc.setFileFilter(jos);
+        }
+        int answer = fc.showSaveDialog(Main.parent);
+        if (answer != JFileChooser.APPROVE_OPTION)
+            return;
+
+        if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
+            Main.pref.put("lastDirectory", fc.getCurrentDirectory().getAbsolutePath());
+        }
+
+        File file = fc.getSelectedFile();
+        String fn = file.getName();
+
+        boolean zip;
+        FileFilter ff = fc.getFileFilter();
+        if (zipRequired) {
+            zip = true;
+        } else if (ff == joz) {
+            zip = true;
+        } else if (ff == jos) {
+            zip = false;
+        } else {
+            if (fn.toLowerCase().endsWith(".joz")) {
+                zip = true;
+            } else {
+                zip = false;
+            }
+        }
+        if (fn.indexOf('.') == -1) {
+            file = new File(file.getPath() + (zip ? ".joz" : ".jos"));
+        }
+        if (!SaveActionBase.confirmOverride(file))
+            return;
+
+        List<Layer> layersOut = new ArrayList<Layer>();
+        for (Layer layer : layers) {
+            if (exporters.get(layer) == null || !exporters.get(layer).shallExport()) continue;
+            // TODO: resolve dependencies for layers excluded by the user
+            layersOut.add(layer);
+        }
+
+        SessionWriter sw = new SessionWriter(layersOut, exporters, dependencies, zip);
+        try {
+            sw.write(file);
+        } catch (IOException ex) {
+            ex.printStackTrace();
+            HelpAwareOptionPane.showMessageDialogInEDT(
+                    Main.parent,
+                    tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>", file.getName(), ex.getMessage()),
+                    tr("IO Error"),
+                    JOptionPane.ERROR_MESSAGE,
+                    null
+            );
+        }
+    }
+
+    public class SessionSaveAsDialog extends ExtendedDialog {
+
+        public SessionSaveAsDialog() {
+            super(Main.parent, tr("Save Session"), new String[] {tr("Cancel"), tr("Save As")});
+            initialize();
+            setButtonIcons(new String[] {"cancel", "save_as"});
+            setDefaultButton(2);
+            setRememberWindowGeometry(getClass().getName() + ".geometry",
+                    WindowGeometry.centerInWindow(Main.parent, new Dimension(350, 450)));
+            setContent(build(), false);
+        }
+
+        public void initialize() {
+            layers = new ArrayList<Layer>(Main.map.mapView.getAllLayersAsList());
+            exporters = new HashMap<Layer, SessionLayerExporter>();
+            dependencies = new MultiMap<Layer, Layer>();
+
+            Set<Layer> noExporter = new HashSet<Layer>();
+
+            for (Layer layer : layers) {
+                SessionLayerExporter exporter = SessionWriter.getSessionLayerExporter(layer);
+                if (exporter != null) {
+                    exporters.put(layer, exporter);
+                    Collection<Layer> deps = exporter.getDependencies();
+                    if (deps != null) {
+                        dependencies.putAll(layer, deps);
+                    } else {
+                        dependencies.putVoid(layer);
+                    }
+                } else {
+                    noExporter.add(layer);
+                    exporters.put(layer, null);
+                }
+            }
+
+            int numNoExporter = 0;
+            WHILE:while (numNoExporter != noExporter.size()) {
+                numNoExporter = noExporter.size();
+                for (Layer layer : layers) {
+                    if (noExporter.contains(layer)) continue;
+                    for (Layer depLayer : dependencies.get(layer)) {
+                        if (noExporter.contains(depLayer)) {
+                            noExporter.add(layer);
+                            exporters.put(layer, null);
+                            break WHILE;
+                        }
+                    }
+                }
+            }
+        }
+
+        public Component build() {
+            JPanel p = new JPanel(new GridBagLayout());
+            JPanel ip = new JPanel(new GridBagLayout());
+            for (Layer layer : layers) {
+                JPanel wrapper = new JPanel(new GridBagLayout());
+                wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
+                Component exportPanel;
+                SessionLayerExporter exporter = exporters.get(layer);
+                if (exporter == null) {
+                    if (!exporters.containsKey(layer)) throw new AssertionError();
+                    exportPanel = getDisabledExportPanel(layer);
+                } else {
+                    exportPanel = exporter.getExportPanel();
+                }
+                wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
+                ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2,2,4,2));
+            }
+            ip.add(GBC.glue(0,1), GBC.eol().fill(GBC.VERTICAL));
+            JScrollPane sp = new JScrollPane(ip);
+            sp.setBorder(BorderFactory.createEmptyBorder());
+            p.add(sp, GBC.eol().fill());
+            final JTabbedPane tabs = new JTabbedPane();
+            tabs.addTab(tr("Layers"), p);
+            return tabs;
+        }
+
+        protected Component getDisabledExportPanel(Layer layer) {
+            JPanel p = new JPanel(new GridBagLayout());
+            JCheckBox include = new JCheckBox();
+            include.setEnabled(false);
+            JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEFT);
+            lbl.setToolTipText(tr("No exporter for this layer"));
+            lbl.setEnabled(false);
+            p.add(include, GBC.std());
+            p.add(lbl, GBC.std());
+            p.add(GBC.glue(1,0), GBC.std().fill(GBC.HORIZONTAL));
+            return p;
+        }
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/gui/MainMenu.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 4684)
+++ /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 4685)
@@ -71,4 +71,5 @@
 import org.openstreetmap.josm.actions.SelectAllAction;
 import org.openstreetmap.josm.actions.SessionLoadAction;
+import org.openstreetmap.josm.actions.SessionSaveAsAction;
 import org.openstreetmap.josm.actions.ShowStatusReportAction;
 import org.openstreetmap.josm.actions.SimplifyWayAction;
@@ -120,4 +121,5 @@
     public final JosmAction saveAs = new SaveAsAction();
     public final JosmAction sessionLoad = new SessionLoadAction();
+    public final JosmAction sessionSaveAs = new SessionSaveAsAction();
     public final JosmAction gpxExport = new GpxExportAction();
     public final DownloadAction download = new DownloadAction();
@@ -365,4 +367,5 @@
             sessionMenu.setToolTipText(tr("Save and load the current session (list of layers, etc.)"));
             sessionMenu.setIcon(ImageProvider.get("session"));
+            add(sessionMenu, sessionSaveAs);
             add(sessionMenu, sessionLoad);
             fileMenu.add(sessionMenu);
Index: /trunk/src/org/openstreetmap/josm/io/session/OsmDataSessionExporter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/session/OsmDataSessionExporter.java	(revision 4685)
+++ /trunk/src/org/openstreetmap/josm/io/session/OsmDataSessionExporter.java	(revision 4685)
@@ -0,0 +1,234 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.session;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.CardLayout;
+import java.awt.Font;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URL;
+import java.net.MalformedURLException;
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.swing.AbstractAction;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+
+import org.w3c.dom.Element;
+
+import org.openstreetmap.josm.actions.SaveAction;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.OsmWriter;
+import org.openstreetmap.josm.io.OsmWriterFactory;
+import org.openstreetmap.josm.io.session.SessionWriter.ExportSupport;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageRequest;
+
+public class OsmDataSessionExporter implements SessionLayerExporter {
+
+    private OsmDataLayer layer;
+
+    public OsmDataSessionExporter(OsmDataLayer layer) {
+        this.layer = layer;
+    }
+
+    public OsmDataSessionExporter() {
+    }
+
+    public OsmDataSessionExporter newInstance(OsmDataLayer layer) {
+        return new OsmDataSessionExporter(layer);
+    }
+
+    @Override
+    public Collection<Layer> getDependencies() {
+        return Collections.emptySet();
+    }
+
+    private class LayerSaveAction extends AbstractAction {
+        public LayerSaveAction() {
+            putValue(SMALL_ICON, new ImageRequest().setName("save").setWidth(16).get());
+            putValue(SHORT_DESCRIPTION, tr("Layer contains unsaved data - save to file."));
+            updateEnabledState();
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            new SaveAction().doSave(layer);
+            updateEnabledState();
+        }
+
+        public void updateEnabledState() {
+            setEnabled(layer.requiresSaveToFile());
+        }
+    }
+
+    private JRadioButton link, include;
+    private JCheckBox export;
+
+    @Override
+    public JPanel getExportPanel() {
+        final JPanel p = new JPanel(new GridBagLayout());
+        JPanel topRow = new JPanel(new GridBagLayout());
+        export = new JCheckBox();
+        export.setSelected(true);
+        final JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEFT);
+        lbl.setToolTipText(layer.getToolTipText());
+
+        JLabel lblData = new JLabel(tr("Data:"));
+        link = new JRadioButton(tr("local file"));
+        link.putClientProperty("actionname", "link");
+        link.setToolTipText(tr("Link to a OSM data file on your local disk."));
+        include = new JRadioButton(tr("include"));
+        include.setToolTipText(tr("Include OSM data in the .joz session file."));
+        include.putClientProperty("actionname", "include");
+        ButtonGroup group = new ButtonGroup();
+        group.add(link);
+        group.add(include);
+
+        JPanel cardLink = new JPanel(new GridBagLayout());
+        final File file = layer.getAssociatedFile();
+        final LayerSaveAction saveAction = new LayerSaveAction();
+        final JButton save = new JButton(saveAction);
+        if (file != null) {
+            JTextField tf = new JTextField();
+            tf.setText(file.getPath());
+            tf.setEditable(false);
+            cardLink.add(tf, GBC.std());
+            save.setMargin(new Insets(0,0,0,0));
+            cardLink.add(save, GBC.eol().insets(2,0,0,0));
+        } else {
+            cardLink.add(new JLabel(tr("No file association")), GBC.eol());
+        }
+
+        JPanel cardInclude = new JPanel(new GridBagLayout());
+        JLabel lblIncl = new JLabel(tr("OSM data will be included in the session file."));
+        lblIncl.setFont(lblIncl.getFont().deriveFont(Font.PLAIN));
+        cardInclude.add(lblIncl, GBC.eol().fill(GBC.HORIZONTAL));
+
+        final CardLayout cl = new CardLayout();
+        final JPanel cards = new JPanel(cl);
+        cards.add(cardLink, "link");
+        cards.add(cardInclude, "include");
+
+        if (file != null) {
+            link.setSelected(true);
+        } else {
+            link.setEnabled(false);
+            link.setToolTipText(tr("No file association"));
+            include.setSelected(true);
+            cl.show(cards, "include");
+        }
+
+        link.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                cl.show(cards, "link");
+            }
+        });
+        include.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                cl.show(cards, "include");
+            }
+        });
+
+        topRow.add(export, GBC.std());
+        topRow.add(lbl, GBC.std());
+        topRow.add(GBC.glue(1,0), GBC.std().fill(GBC.HORIZONTAL));
+        p.add(topRow, GBC.eol().fill(GBC.HORIZONTAL));
+        p.add(lblData, GBC.std().insets(10,0,0,0));
+        p.add(link, GBC.std());
+        p.add(include, GBC.eol());
+        p.add(cards, GBC.eol().insets(15,0,3,3));
+
+        export.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent e) {
+                if (e.getStateChange() == ItemEvent.DESELECTED) {
+                    GuiHelper.setEnabledRec(p, false);
+                    export.setEnabled(true);
+                } else {
+                    GuiHelper.setEnabledRec(p, true);
+                    save.setEnabled(saveAction.isEnabled());
+                    link.setEnabled(file != null);
+                }
+            }
+        });
+        return p;
+    }
+
+    @Override
+    public boolean shallExport() {
+        return export.isSelected();
+    }
+
+    @Override
+    public boolean requiresZip() {
+        return include.isSelected();
+    }
+
+    @Override
+    public Element export(ExportSupport support) throws IOException {
+        Element layerEl = support.createElement("layer");
+        layerEl.setAttribute("type", "osm-data");
+        layerEl.setAttribute("version", "0.1");
+
+        Element file = support.createElement("file");
+        layerEl.appendChild(file);
+
+        if (requiresZip()) {
+            String zipPath = "layers/" + String.format("%02d", support.getLayerIndex()) + "/data.osm";
+            file.appendChild(support.createTextNode(zipPath));
+            addDataFile(support.getOutputStreamZip(zipPath));
+        } else {
+            URI uri = layer.getAssociatedFile().toURI();
+            URL url = null;
+            try {
+                url = uri.toURL();
+            } catch (MalformedURLException e) {
+                throw new IOException(e);
+            }
+            file.appendChild(support.createTextNode(url.toString()));
+        }
+        return layerEl;
+    }
+
+    protected void addDataFile(OutputStream out) throws IOException {
+        Writer writer = null;
+        try {
+            writer = new OutputStreamWriter(out, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, layer.data.getVersion());
+        layer.data.getReadLock().lock();
+        try {
+            w.header();
+            w.writeDataSources(layer.data);
+            w.writeContent(layer.data);
+            w.footer();
+            w.flush();
+        } finally {
+            layer.data.getReadLock().unlock();
+        }
+    }
+}
+
Index: /trunk/src/org/openstreetmap/josm/io/session/SessionLayerExporter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/session/SessionLayerExporter.java	(revision 4685)
+++ /trunk/src/org/openstreetmap/josm/io/session/SessionLayerExporter.java	(revision 4685)
@@ -0,0 +1,50 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.session;
+
+import java.awt.Component;
+import java.io.IOException;
+import java.util.Collection;
+
+import org.w3c.dom.Element;
+
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.io.session.SessionWriter.ExportSupport;
+
+public interface SessionLayerExporter {
+
+    /**
+     * Return the Layers, this Layer depends on.
+     */
+    Collection<Layer> getDependencies();
+
+    /**
+     * The GUI for exporting this layer.
+     */
+    Component getExportPanel();
+
+    /**
+     * Return true, if the layer should be included in the
+     * list of exported layers.
+     *
+     * The user can veto this in the export panel.
+     */
+    boolean shallExport();
+
+    /**
+     * Return true, if some data needs to be included in
+     * the zip archive. This decision depends on the user
+     * selection in the export panel.
+     *
+     * If any layer requires zip, the user can only save as
+     * .joz. Otherwise both .jos and .joz are possible.
+     */
+    boolean requiresZip();
+
+    /**
+     * Save meta data to the .jos file. Return a <layer> element.
+     * Use support to save files in the zip archive as needed.
+     */
+    Element export(ExportSupport support) throws IOException;
+
+}
+
Index: /trunk/src/org/openstreetmap/josm/io/session/SessionWriter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/session/SessionWriter.java	(revision 4685)
+++ /trunk/src/org/openstreetmap/josm/io/session/SessionWriter.java	(revision 4685)
@@ -0,0 +1,205 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.session;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Text;
+
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.MultiMap;
+import org.openstreetmap.josm.tools.Utils;
+
+public class SessionWriter {
+
+    private static Map<Class<? extends Layer>, Class<? extends SessionLayerExporter>> sessionLayerExporters =
+            new HashMap<Class<? extends Layer>, Class<? extends SessionLayerExporter>>();
+    static {
+        registerSessionLayerExporter(OsmDataLayer.class , OsmDataSessionExporter.class);
+    }
+
+    /**
+     * Register a session layer exporter.
+     *
+     * The exporter class must have an one-argument constructor with layerClass as formal parameter type.
+     */
+    public static void registerSessionLayerExporter(Class<? extends Layer> layerClass, Class<? extends SessionLayerExporter> exporter) {
+        sessionLayerExporters.put(layerClass, exporter);
+    }
+
+    public static SessionLayerExporter getSessionLayerExporter(Layer layer) {
+        Class<? extends Layer> layerClass = layer.getClass();
+        Class<? extends SessionLayerExporter> exporterClass = sessionLayerExporters.get(layerClass);
+        if (exporterClass == null) return null;
+        try {
+            @SuppressWarnings("unchecked")
+            Constructor<? extends SessionLayerExporter> constructor = (Constructor) exporterClass.getConstructor(layerClass);
+            return constructor.newInstance(layer);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private List<Layer> layers;
+    private Map<Layer, SessionLayerExporter> exporters;
+    private MultiMap<Layer, Layer> dependencies;
+    private boolean zip;
+
+    private ZipOutputStream zipOut;
+
+    public SessionWriter(List<Layer> layers, Map<Layer, SessionLayerExporter> exporters,
+                MultiMap<Layer, Layer> dependencies, boolean zip) {
+        this.layers = layers;
+        this.exporters = exporters;
+        this.dependencies = dependencies;
+        this.zip = zip;
+    }
+
+    public class ExportSupport {
+        private Document doc;
+        private int layerIndex;
+
+        public ExportSupport(Document doc, int layerIndex) {
+            this.doc = doc;
+            this.layerIndex = layerIndex;
+        }
+
+        public Element createElement(String name) {
+            return doc.createElement(name);
+        }
+
+        public Text createTextNode(String text) {
+            return doc.createTextNode(text);
+        }
+
+        public int getLayerIndex() {
+            return layerIndex;
+        }
+
+        /**
+         * Create a file in the zip archive.
+         *
+         * @return never close the output stream, but make sure to flush buffers
+         */
+        public OutputStream getOutputStreamZip(String zipPath) throws IOException {
+            if (!isZip()) throw new RuntimeException();
+            ZipEntry entry = new ZipEntry(zipPath);
+            zipOut.putNextEntry(entry);
+            return zipOut;
+        }
+
+        public boolean isZip() {
+            return zip;
+        }
+    }
+
+    public Document createJosDocument() throws IOException {
+        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+        builderFactory.setValidating(false);
+        builderFactory.setNamespaceAware(true);
+        DocumentBuilder builder = null;
+        try {
+            builder = builderFactory.newDocumentBuilder();
+        } catch (ParserConfigurationException e) {
+            throw new RuntimeException(e);
+        }
+        Document doc = builder.newDocument();
+
+        Element root = doc.createElement("josm-session");
+        root.setAttribute("version", "0.1");
+        doc.appendChild(root);
+
+        Element layersEl = doc.createElement("layers");
+        root.appendChild(layersEl);
+
+        for (int index=0; index<layers.size(); ++index) {
+            Layer layer = layers.get(index);
+            SessionLayerExporter exporter = exporters.get(layer);
+            ExportSupport support = new ExportSupport(doc, index+1);
+            Element el = exporter.export(support);
+            el.setAttribute("index", Integer.toString(index+1));
+            el.setAttribute("name", layer.getName());
+            Set<Layer> deps = dependencies.get(layer);
+            if (deps.size() > 0) {
+                List<Integer> depsInt = new ArrayList<Integer>();
+                for (Layer depLayer : deps) {
+                    int depIndex = layers.indexOf(depLayer);
+                    if (depIndex == -1) throw new AssertionError();
+                    depsInt.add(depIndex+1);
+                }
+                el.setAttribute("depends", Utils.join(",", depsInt));
+            }
+            layersEl.appendChild(el);
+        }
+        return doc;
+    }
+
+    public void writeJos(Document doc, OutputStream out) throws IOException {
+        try {
+            OutputStreamWriter writer = new OutputStreamWriter(out, "utf-8");
+            writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
+            TransformerFactory transfac = TransformerFactory.newInstance();
+            Transformer trans = transfac.newTransformer();
+            trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+            trans.setOutputProperty(OutputKeys.INDENT, "yes");
+            trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+            StreamResult result = new StreamResult(writer);
+            DOMSource source = new DOMSource(doc);
+            trans.transform(source, result);
+        } catch (TransformerException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void write(File f) throws IOException {
+        OutputStream out = null;
+        try {
+            out = new FileOutputStream(f);
+        } catch (FileNotFoundException e) {
+            throw new IOException(e);
+        }
+        write(out);
+    }
+
+    public void write (OutputStream out) throws IOException {
+        if (zip) {
+            zipOut = new ZipOutputStream(new BufferedOutputStream(out));
+        }
+        Document doc = createJosDocument(); // as side effect, files may be added to zipOut
+        if (zip) {
+            ZipEntry entry = new ZipEntry("session.jos");
+            zipOut.putNextEntry(entry);
+            writeJos(doc, zipOut);
+            zipOut.close();
+        } else {
+            writeJos(doc, new BufferedOutputStream(out));
+        }
+        Utils.close(out);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/tools/MultiMap.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/MultiMap.java	(revision 4684)
+++ /trunk/src/org/openstreetmap/josm/tools/MultiMap.java	(revision 4685)
@@ -31,5 +31,7 @@
 
     /**
-     * Map a key to a value. Can be called multiple times with the same key, but different value.
+     * Map a key to a value.
+     *
+     * Can be called multiple times with the same key, but different value.
      */
     public void put(A key, B value) {
@@ -44,4 +46,5 @@
     /**
      * Put a key that maps to nothing. (Only if it is not already in the map)
+     *
      * Afterwards containsKey(key) will return true and get(key) will return
      * an empty Set instead of null.
@@ -54,5 +57,19 @@
 
     /**
-     * Get the keySet
+     * Map the key to all the given values.
+     *
+     * Adds to the mappings that are already there.
+     */
+    public void putAll(A key, Collection<B> values) {
+        LinkedHashSet<B> vals = map.get(key);
+        if (vals == null) {
+            vals = new LinkedHashSet<B>(values);
+            map.put(key, vals);
+        }
+        vals.addAll(values);
+    }
+
+    /**
+     * Get the keySet.
      */
     public Set<A> keySet() {
@@ -61,7 +78,9 @@
 
     /**
-     * Return the Set associated with the given key. Result is null if
-     * nothing has been mapped to this key. Modifications of the returned list
-     * changes the underling map, but you should better not do that.
+     * Returns the Set associated with the given key. Result is null if
+     * nothing has been mapped to this key.
+     *
+     * Modifications of the returned list changes the underling map,
+     * but you should better not do that.
      */
     public Set<B> get(A key) {
@@ -87,5 +106,6 @@
 
     /**
-     * Returns true if the multimap contains a value for a key
+     * Returns true if the multimap contains a value for a key.
+     *
      * @param key The key
      * @param value The value
@@ -106,5 +126,5 @@
 
     /**
-     * number of keys
+     * Returns the number of keys.
      */
     public int size() {
@@ -113,5 +133,5 @@
 
     /**
-     * returns a collection of all value sets
+     * Returns a collection of all value sets.
      */
     public Collection<LinkedHashSet<B>> values() {
@@ -120,5 +140,5 @@
 
     /**
-     * Removes a cerain key=value mapping
+     * Removes a cerain key=value mapping.
      *
      * @return true, if something was removed
@@ -133,5 +153,5 @@
 
     /**
-     * Removes all mappings for a certain key
+     * Removes all mappings for a certain key.
      */
     public LinkedHashSet<B> remove(A key) {
