Index: applications/editors/josm/plugins/sds/LICENSE
===================================================================
--- applications/editors/josm/plugins/sds/LICENSE	(revision 28160)
+++ applications/editors/josm/plugins/sds/LICENSE	(revision 28160)
@@ -0,0 +1,10 @@
+This plugin was originally written by Frederik Ramm 
+<ramm@geofabrik.de> as part of a Geofabrik contract
+for the Humanitarian OpenStreetMap Team. HOT's work 
+in Indonesia is sponsored by the Australia-Indonesia 
+Facility for Disaster Reduction grant.
+
+The plugin is licensed unter GNU GPL v2 or later.
+
+It contains excerpts of code from the JOSM core.
+
Index: applications/editors/josm/plugins/sds/README
===================================================================
--- applications/editors/josm/plugins/sds/README	(revision 28160)
+++ applications/editors/josm/plugins/sds/README	(revision 28160)
@@ -0,0 +1,34 @@
+SDS plugin
+----------
+
+The idea in this project is to enable JOSM users to record additional data
+with OSM objects, but that additional data lives in a different repository.
+The driver, in this instance, was that the data to be collected was
+unsuitable for public release due to privacy issues (personal data like
+household income etc.), but there may be other uses, for example if you want
+to record stuff that is too volatile or detailed for OSM.
+
+The separate data store has all information keyed against OSM object IDs,
+i.e. it cannot record geometries - only additional tags.
+
+The SDS plugin makes it possible to have JOSM query another data source for
+additional data related to objects just downloaded from OSM. For example,
+you download ways #15, #20, #25 from OSM, then the SDS plugin will query a
+different server "do you have extra info pertaining to ways #15, #20, #25?"
+and the server may or may not return extra info.
+
+These extra tags are then brought into JOSM just like any other tags, and
+they can be edited, styled, filtered, and validated normally.
+
+On upload, the plugin will again separate the extra tags from normal OSM
+tagging, and will upload extra tags to the separate server only. (This is
+based on a tag name rule, i.e. tags that begin with a defined prefix go to
+the separate server, and all else goes to OSM.)
+
+Geofabrik has also written a server (in Ruby on Rails) that serves as the
+counterpart for the SDS plugin. The server is slightly more HOT specific
+than the JOSM plugin, and comes with a search function and web editing
+interface for tags stored there.
+
+For more documentation on the SDS plugin and server, see <tbd>
+
Index: applications/editors/josm/plugins/sds/REVISION
===================================================================
--- applications/editors/josm/plugins/sds/REVISION	(revision 28160)
+++ applications/editors/josm/plugins/sds/REVISION	(revision 28160)
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<info>
+<entry
+   kind="dir"
+   path="."
+   revision="27196">
+<url>http://svn.openstreetmap.org/applications/editors/josm/plugins/licensechange</url>
+<repository>
+<root>http://svn.openstreetmap.org</root>
+<uuid>b9d5c4c9-76e1-0310-9c85-f3177eceb1e4</uuid>
+</repository>
+<wc-info>
+<schedule>normal</schedule>
+<depth>infinity</depth>
+</wc-info>
+<commit
+   revision="27161">
+<author>frederik</author>
+<date>2011-11-26T17:18:26.688652Z</date>
+</commit>
+</entry>
+</info>
Index: applications/editors/josm/plugins/sds/build.xml
===================================================================
--- applications/editors/josm/plugins/sds/build.xml	(revision 28160)
+++ applications/editors/josm/plugins/sds/build.xml	(revision 28160)
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+** This is the build.xml for the SDS plugin
+**
+** Usage
+** =====
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in your default plugin directory) run
+**
+**    > ant  install
+**
+** To build against the core in ../../core, create a correct manifest and deploy to
+** SVN,
+**    set the properties commit.message and plugin.main.version
+** and run
+**    > ant  publish
+**
+**
+-->
+<project name="sds" default="dist" basedir=".">
+    <!--
+      ** update before publishing
+    -->
+    <property name="commit.message" value=""/>
+    <property name="plugin.main.version" value="4549"/>
+    <property name="josm" location="../josm/build" />
+    <property name="plugin.dist.dir" value="."/>
+    <property name="plugin.build.dir" value="build"/>
+    <property name="plugin.jar" value="${plugin.dist.dir}/${ant.project.name}.jar"/>
+    <property name="ant.build.javac.target" value="1.5"/>
+    <target name="init">
+        <mkdir dir="${plugin.build.dir}"/>
+    </target>
+    <target name="compile" depends="init">
+        <echo message="creating ${plugin.jar}"/>
+        <javac srcdir="src" classpath="${josm}" debug="true" destdir="${plugin.build.dir}">
+            <compilerarg value="-Xlint:deprecation"/>
+            <compilerarg value="-Xlint:unchecked"/>
+        </javac>
+    </target>
+    <target name="dist" depends="compile,revision">
+        <copy todir="${plugin.build.dir}/images">
+            <fileset dir="images"/>
+        </copy>
+        <copy todir="${plugin.build.dir}/data">
+            <fileset dir="data"/>
+        </copy>
+        <jar destfile="${plugin.jar}" basedir="${plugin.build.dir}">
+            <manifest>
+                <attribute name="Author" value="Frederik Ramm"/>
+                <attribute name="Plugin-Class" value="org.openstreetmap.hot.sds.SeparateDataStorePlugin"/>
+                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+                <attribute name="Plugin-Description" value="Loads data from SDS" />
+                <attribute name="Plugin-Icon" value="images/sds.png"/>
+                <!--<attribute name="Plugin-Link" value=""/>-->
+                <attribute name="Plugin-Mainversion" value="${plugin.main.version}"/>
+                <attribute name="Plugin-Version" value="${version.entry.commit.revision}"/>
+            </manifest>
+        </jar>
+    </target>
+    <target name="revision">
+    <!--
+        <exec append="false" error="/dev/null" output="REVISION" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="__xml"/>
+            <arg value="."/>
+        </exec>
+        -->
+        <xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true"/>
+        <!--
+        <delete file="REVISION"/>
+        -->
+    </target>
+    <target name="clean">
+        <delete dir="${plugin.build.dir}"/>
+        <delete file="${plugin.jar}"/>
+    </target>
+    <target name="install" depends="dist">
+        <property environment="env"/>
+        <condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+            <and>
+                <os family="windows"/>
+            </and>
+        </condition>
+        <copy file="${plugin.jar}" todir="${josm.plugins.dir}"/>
+    </target>
+    <!--
+     ************************** Publishing the plugin ***********************************
+    -->
+    <!--
+    ** extracts the JOSM release for the JOSM version in ../core and saves it in the
+    ** property ${coreversion.info.entry.revision}
+    **
+    -->
+    <target name="core-info">
+        <exec append="false" output="core.info.xml" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="../../core"/>
+        </exec>
+        <xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true"/>
+        <echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
+        <echo>Plugin-Mainversion is set to ${plugin.main.version}.</echo>
+        <delete file="core.info.xml"/>
+    </target>
+    <!--
+    ** commits the source tree for this plugin
+    -->
+    <target name="commit-current">
+        <echo>Commiting the plugin source with message '${commit.message}' ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="commit"/>
+            <arg value="."/>
+        </exec>
+    </target>
+    <!--
+    ** updates (svn up) the source tree for this plugin
+    -->
+    <target name="update-current">
+        <echo>Updating plugin source ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="."/>
+        </exec>
+        <echo>Updating ${plugin.jar} ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="../dist/${plugin.jar}"/>
+        </exec>
+    </target>
+    <!--
+    ** commits the plugin.jar
+    -->
+    <target name="commit-dist">
+        <echo>
+***** Properties of published ${plugin.jar} *****
+Commit message    : '${commit.message}'
+Plugin-Mainversion: ${plugin.main.version}
+JOSM build version: ${coreversion.info.entry.revision}
+Plugin-Version    : ${version.entry.commit.revision}
+***** / Properties of published ${plugin.jar} *****
+
+Now commiting ${plugin.jar} ...
+</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="commit"/>
+            <arg value="${plugin.jar}"/>
+        </exec>
+    </target>
+    <!-- ** make sure svn is present as a command line tool ** -->
+    <target name="ensure-svn-present">
+        <exec append="true" output="svn.log" executable="svn" failonerror="false" resultproperty="svn.exit.code">
+            <env key="LANG" value="C"/>
+            <arg value="--version"/>
+        </exec>
+        <fail message="Fatal: command 'svn' not found. Please make sure svn is installed on your system.">
+            <condition>
+                <isfailure code="${svn.exit.code}"/>
+            </condition>
+        </fail>
+    </target>
+    <target name="publish" depends="ensure-svn-present,core-info,commit-current,update-current,clean,dist,commit-dist">
+    </target>
+</project>
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/DetermineSdsModificationsUploadHook.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/DetermineSdsModificationsUploadHook.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/DetermineSdsModificationsUploadHook.java	(revision 28160)
@@ -0,0 +1,175 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.hot.sds;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.openstreetmap.josm.actions.upload.UploadHook;
+import org.openstreetmap.josm.data.APIDataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+/**
+ * This upload hook does the following things:
+ * 
+ * 1. Find out if there are any changes in the special tags that need to 
+ *    be uploaded to a different server.
+ * 2. Find out if any objects that did have special tags have now been 
+ *    deleted, resulting in tag deletions on the special server.
+ * 3. Find out if any objects carrying special tags have been newly created.
+ * 4. Also, if it is determined that an object modification consists exclusively
+ *    of special tags, then skip uploading that object, by removing it from 
+ *    the apiDataSet.
+ *    
+ * This upload hook stores its findings with the SeparateDataStorePlugin, and
+ * changes are sent to the SDS server only after the OSM upload has sucessfully
+ * completed. The UploadSuccessHook is responsible for that.
+ */
+public class DetermineSdsModificationsUploadHook implements UploadHook
+{
+	private SeparateDataStorePlugin plugin;
+
+	DetermineSdsModificationsUploadHook(SeparateDataStorePlugin plugin)	{
+		this.plugin = plugin;
+	}
+	
+    public boolean checkUpload(APIDataSet apiDataSet) {
+    	
+    	ArrayList<OsmPrimitive> droplist = new ArrayList<OsmPrimitive>();
+    	
+    	// check deleted primitives for special tags.
+    	for (OsmPrimitive del : apiDataSet.getPrimitivesToDelete()) {
+    		IPrimitive old = plugin.getOriginalPrimitive(del);
+    		if (hasSpecialTags(old)) {
+    			// request deletion of all tags for this object on special server.
+    			plugin.enqueueForUpload(del, new HashMap<String, String>(), false);
+    		}
+    	}
+
+    	// check modified primitives.
+       	for (OsmPrimitive upd : apiDataSet.getPrimitivesToUpdate()) {
+       		
+       		HashSet<String> allKeys = new HashSet<String>();
+       		boolean specialTags = false;
+       		
+       		// process tags of new object
+       		for (String key : upd.keySet()) {
+       			allKeys.add(key);
+       			if (!specialTags && isSpecialKey(key)) specialTags = true;
+       		}
+       		
+       		// process tags of old object
+       		IPrimitive old = plugin.getOriginalPrimitive(upd);
+       		for (String key : old.keySet()) {
+       			allKeys.add(key);
+      			if (!specialTags && isSpecialKey(key)) specialTags = true;
+       		}
+
+       		// if neither has special tags, done with this object.
+       		if (!specialTags) continue;
+       		
+       		// special tags are involved. find out what, exactly, has changed.
+       		boolean changeInSpecialTags = false;
+       		boolean changeInOtherTags = false;
+       		for (String key : allKeys) {
+       			if (old.get(key) == null || upd.get(key) == null || !old.get(key).equals(upd.get(key))) {
+       				if (isSpecialKey(key)) changeInSpecialTags = true; else changeInOtherTags = true;
+       				if (changeInSpecialTags && changeInOtherTags) break;
+       			}
+       		}
+       		
+       		// change *only* in standard tags - done with this object.
+       		if (!changeInSpecialTags) continue;
+       		
+       		// assemble new set of special tags. might turn out to be empty.
+       		HashMap<String, String> newSpecialTags = new HashMap<String, String>();
+       		for (String key : upd.keySet()) {
+       			if (isSpecialKey(key)) newSpecialTags.put(key, upd.get(key));
+       		}
+       		
+       		boolean uploadToOsm = changeInOtherTags;
+       		
+       		// not done yet: if no changes in standard tags, we need to find out if 
+       		// there were changes in the other properties (node: lat/lon, way/relation:
+       		// member list). If the answer is no, then the object must be removed from 
+       		// JOSM's normal upload queue, else we would be uploading a non-edit.
+       		if (!changeInOtherTags) {
+       			switch(old.getType()) {
+       			case NODE:
+       				INode nold = (INode) old;
+       				INode nupd = (INode) upd;
+       				uploadToOsm = !(nold.getCoor().equals(nupd.getCoor()));
+       				break;
+       			case WAY:
+       				IWay wold = (IWay) old;
+       				IWay wupd = (IWay) upd;
+       				if (wold.getNodesCount() != wupd.getNodesCount()) {
+       					uploadToOsm = true;
+       					break;
+       				} 
+       				for (int i = 0; i < wold.getNodesCount(); i++) {
+       					if (wold.getNodeId(i) != wupd.getNodeId(i)) {
+       						uploadToOsm = true;
+       						break;
+       					}
+       				}
+       				break;
+       			case RELATION:
+       				IRelation rold = (IRelation) old;
+       				IRelation rupd = (IRelation) upd;
+       				if (rold.getMembersCount()!= rupd.getMembersCount()) {
+       					uploadToOsm = true;
+       					break;
+       				} 
+       				for (int i = 0; i < rold.getMembersCount(); i++) {
+       					if (rold.getMemberType(i) != rupd.getMemberType(i) ||
+       						rold.getMemberId(i) != rupd.getMemberId(i)) {
+       						uploadToOsm = true;
+       						break;
+       					}
+       				}
+       				break;
+       			}
+       		}
+       		
+       		// request that new set of special tags be uploaded
+       		plugin.enqueueForUpload(upd, newSpecialTags, !uploadToOsm);
+       		
+       		// we cannot remove from getPrimitivesToUpdate, this would result in a 
+       		// ConcurrentModificationException.
+       		if (!uploadToOsm) droplist.add(upd);
+       		
+    	}
+       	
+    	apiDataSet.getPrimitivesToUpdate().removeAll(droplist);
+		       	
+       	// check added primitives. 
+       	for (OsmPrimitive add : apiDataSet.getPrimitivesToAdd()) {
+       		// assemble new set of special tags. might turn out to be empty.
+       		HashMap<String, String> newSpecialTags = new HashMap<String, String>();
+       		for (String key : add.keySet()) {
+       			if (isSpecialKey(key)) newSpecialTags.put(key, add.get(key));
+       		}
+       		if (!newSpecialTags.isEmpty()) plugin.enqueueForUpload(add, newSpecialTags, false);
+    	}
+    	
+       	// FIXME it is possible that the list of OSM edits is totally empty.
+		return true;
+
+    }
+    
+    boolean hasSpecialTags(IPrimitive p) {
+    	for (String key : p.keySet()) {
+    		if (isSpecialKey(key)) return true;
+    	}    
+    	return false;
+    }
+    
+    boolean isSpecialKey(String key) {
+    	return key.startsWith(plugin.getIgnorePrefix());
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/ReadPostprocessor.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/ReadPostprocessor.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/ReadPostprocessor.java	(revision 28160)
@@ -0,0 +1,101 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.data.osm.Changeset;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.Visitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmServerReadPostprocessor;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class ReadPostprocessor implements OsmServerReadPostprocessor {
+	
+	private ArrayList<Long> nodeList;
+	private ArrayList<Long> wayList;
+	private ArrayList<Long> relationList;
+	
+	private SeparateDataStorePlugin plugin;
+
+	public ReadPostprocessor(SeparateDataStorePlugin plugin) {
+		this.plugin = plugin;
+	}
+	
+    @Override
+    public void postprocessDataSet(DataSet ds, ProgressMonitor progress) {
+        
+		nodeList = new ArrayList<Long>();
+		wayList = new ArrayList<Long>();
+		relationList = new ArrayList<Long>();
+
+		Visitor adder = new Visitor() {
+			@Override
+			public void visit(Node n) {
+				nodeList.add(n.getId());
+				plugin.originalNodes.put(n.getId(), n.save());
+			}
+			@Override
+			public void visit(Way w) {
+				wayList.add(w.getId());
+				plugin.originalWays.put(w.getId(), w.save());
+			}
+			@Override
+			public void visit(Relation e) {
+				relationList.add(e.getId());
+				plugin.originalNodes.put(e.getId(), e.save());
+			}
+			@Override
+			public void visit(Changeset cs) {}
+		};
+		
+		for (OsmPrimitive p : ds.allPrimitives()) {
+			p.visit(adder);
+		}
+			
+		SdsApi api = SdsApi.getSdsApi();
+		String rv = "";
+		try {
+			rv = api.requestShadowsFromSds(nodeList, wayList, relationList, progress);
+		} catch (SdsTransferException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		
+		// this is slightly inefficient, as we're re-making the string into 
+		// an input stream when there was an input stream to be had inside the
+		// SdsApi already, but this encapsulates things better.
+        InputStream xmlStream;
+		try {
+			xmlStream = new ByteArrayInputStream(rv.getBytes("UTF-8"));
+	        InputSource inputSource = new InputSource(xmlStream);
+			SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new SdsParser(ds, plugin));
+		} catch (UnsupportedEncodingException e1) {
+			// TODO Auto-generated catch block
+			e1.printStackTrace();
+		} catch (SAXException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (ParserConfigurationException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+
+    }
+
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsApi.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsApi.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsApi.java	(revision 28160)
@@ -0,0 +1,536 @@
+//License: GPL. See README for details.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.ConnectException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmApiException;
+import org.openstreetmap.josm.io.OsmTransferCanceledException;
+import org.openstreetmap.josm.io.ProgressInputStream;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Class that encapsulates the communications with the SDS API.
+ *
+ * This is modeled after JOSM's own OsmAPI class.
+ * 
+ */
+public class SdsApi extends SdsConnection {
+    /** max number of retries to send a request in case of HTTP 500 errors or timeouts */
+    static public final int DEFAULT_MAX_NUM_RETRIES = 5;
+
+    /** the collection of instantiated OSM APIs */
+    private static HashMap<String, SdsApi> instances = new HashMap<String, SdsApi>();
+    
+    /**
+     * replies the {@see OsmApi} for a given server URL
+     *
+     * @param serverUrl  the server URL
+     * @return the OsmApi
+     * @throws IllegalArgumentException thrown, if serverUrl is null
+     *
+     */
+    static public SdsApi getSdsApi(String serverUrl) {
+        SdsApi api = instances.get(serverUrl);
+        if (api == null) {
+            api = new SdsApi(serverUrl);
+            instances.put(serverUrl,api);
+        }
+        return api;
+    }
+    /**
+     * replies the {@see OsmApi} for the URL given by the preference <code>sds-server.url</code>
+     *
+     * @return the OsmApi
+     *
+     */
+    static public SdsApi getSdsApi() {
+        String serverUrl = Main.pref.get("sds-server.url", "http://datastore.hotosm.org");
+        if (serverUrl == null)
+            throw new IllegalStateException(tr("Preference ''{0}'' missing. Cannot initialize SdsApi.", "sds-server.url"));
+        return getSdsApi(serverUrl);
+    }
+
+    /** the server URL */
+    private String serverUrl;
+
+    /**
+     * API version used for server communications
+     */
+    private String version = null;
+
+    /**
+     * creates an OSM api for a specific server URL
+     *
+     * @param serverUrl the server URL. Must not be null
+     * @exception IllegalArgumentException thrown, if serverUrl is null
+     */
+    protected SdsApi(String serverUrl)  {
+        CheckParameterUtil.ensureParameterNotNull(serverUrl, "serverUrl");
+        this.serverUrl = serverUrl;
+    }
+
+    /**
+     * Returns the OSM protocol version we use to talk to the server.
+     * @return protocol version, or null if not yet negotiated.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+
+    /**
+     * Returns the base URL for API requests, including the negotiated version number.
+     * @return base URL string
+     */
+    public String getBaseUrl() {
+        StringBuffer rv = new StringBuffer(serverUrl);
+        if (version != null) {
+            rv.append("/");
+            rv.append(version);
+        }
+        rv.append("/");
+        // this works around a ruby (or lighttpd) bug where two consecutive slashes in
+        // an URL will cause a "404 not found" response.
+        int p; while ((p = rv.indexOf("//", 6)) > -1) { rv.delete(p, p + 1); }
+        return rv.toString();
+    }
+
+    /**
+     * Creates an OSM primitive on the server. The OsmPrimitive object passed in
+     * is modified by giving it the server-assigned id.
+     *
+     * @param osm the primitive
+     * @throws SdsTransferException if something goes wrong
+     
+    public void createPrimitive(IPrimitive osm, ProgressMonitor monitor) throws SdsTransferException {
+        String ret = "";
+        try {
+            ensureValidChangeset();
+            initialize(monitor);
+            ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/create", toXml(osm, true),monitor);
+            osm.setOsmId(Long.parseLong(ret.trim()), 1);
+            osm.setChangesetId(getChangeset().getId());
+        } catch(NumberFormatException e){
+            throw new SdsTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret));
+        }
+    }
+    */
+
+    /**
+     * Modifies an OSM primitive on the server.
+     *
+     * @param osm the primitive. Must not be null.
+     * @param monitor the progress monitor
+     * @throws SdsTransferException if something goes wrong
+     
+    public void modifyPrimitive(IPrimitive osm, ProgressMonitor monitor) throws SdsTransferException {
+        String ret = null;
+        try {
+            ensureValidChangeset();
+            initialize(monitor);
+            // normal mode (0.6 and up) returns new object version.
+            ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.getId(), toXml(osm, true), monitor);
+            osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim()));
+            osm.setChangesetId(getChangeset().getId());
+            osm.setVisible(true);
+        } catch(NumberFormatException e) {
+            throw new SdsTransferException(tr("Unexpected format of new version of modified primitive ''{0}''. Got ''{1}''.", osm.getId(), ret));
+        }
+    }
+    */
+
+    /**
+     * Deletes an OSM primitive on the server.
+     * @param osm the primitive
+     * @throws SdsTransferException if something goes wrong
+     
+    public void deletePrimitive(IPrimitive osm, ProgressMonitor monitor) throws SdsTransferException {
+        ensureValidChangeset();
+        initialize(monitor);
+        // can't use a the individual DELETE method in the 0.6 API. Java doesn't allow
+        // submitting a DELETE request with content, the 0.6 API requires it, however. Falling back
+        // to diff upload.
+        //
+        uploadDiff(Collections.singleton(osm), monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
+    }
+    */
+
+    /**
+     * Uploads a list of changes in "diff" form to the server.
+     *
+     * @param list the list of changed OSM Primitives
+     * @param  monitor the progress monitor
+     * @return 
+     * @return list of processed primitives
+     * @throws SdsTransferException 
+     * @throws SdsTransferException if something is wrong
+     
+    public Collection<IPrimitive> uploadDiff(Collection<? extends IPrimitive> list, ProgressMonitor monitor) throws SdsTransferException {
+        try {
+            monitor.beginTask("", list.size() * 2);
+            if (changeset == null)
+                throw new SdsTransferException(tr("No changeset present for diff upload."));
+
+            initialize(monitor);
+
+            // prepare upload request
+            //
+            OsmChangeBuilder changeBuilder = new OsmChangeBuilder(changeset);
+            monitor.subTask(tr("Preparing upload request..."));
+            changeBuilder.start();
+            changeBuilder.append(list);
+            changeBuilder.finish();
+            String diffUploadRequest = changeBuilder.getDocument();
+
+            // Upload to the server
+            //
+            monitor.indeterminateSubTask(
+                    trn("Uploading {0} object...", "Uploading {0} objects...", list.size(), list.size()));
+            String diffUploadResponse = sendRequest("POST", "changeset/" + changeset.getId() + "/upload", diffUploadRequest,monitor);
+
+            // Process the response from the server
+            //
+            DiffResultProcessor reader = new DiffResultProcessor(list);
+            reader.parse(diffUploadResponse, monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
+            return reader.postProcess(
+                    getChangeset(),
+                    monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)
+            );
+        } catch(SdsTransferException e) {
+            throw e;
+        } catch(OsmDataParsingException e) {
+            throw new SdsTransferException(e);
+        } finally {
+            monitor.finishTask();
+        }
+    }
+    */
+    
+    public String requestShadowsFromSds(List<Long> nodes, List<Long> ways, List<Long> relations, ProgressMonitor pm) throws SdsTransferException {
+    	
+    	StringBuilder request = new StringBuilder();
+    	String delim = "";
+    	String comma = "";
+    	
+    	if (nodes != null && !nodes.isEmpty()) {
+    		request.append(delim);
+    		delim = "&";
+    		comma = "";
+    		request.append("nodes=");
+    		for (long i : nodes) {
+    			request.append(comma);
+    			comma = ",";
+    			request.append(i);
+    		}
+    	}
+    	if (ways != null && !ways.isEmpty()) {
+    		request.append(delim);
+    		delim = "&";
+    		comma = "";
+    		request.append("ways=");
+    		for (long i : ways) {
+    			request.append(comma);
+    			comma = ",";
+    			request.append(i);
+    		}
+    	}
+    	if (relations != null && !relations.isEmpty()) {
+    		request.append(delim);
+    		delim = "&";
+    		comma = "";
+    		request.append("relations=");
+    		for (long i : relations) {
+    			request.append(comma);
+    			comma = ",";
+    			request.append(i);
+    		}
+    	}
+    	
+    	return sendRequest("POST", "collectshadows", request.toString(), pm ,true);
+   
+    }
+
+    private void sleepAndListen(int retry, ProgressMonitor monitor) throws SdsTransferException {
+        System.out.print(tr("Waiting 10 seconds ... "));
+        for(int i=0; i < 10; i++) {
+            if (monitor != null) {
+                monitor.setCustomText(tr("Starting retry {0} of {1} in {2} seconds ...", getMaxRetries() - retry,getMaxRetries(), 10-i));
+            }
+            if (cancel)
+                throw new SdsTransferException();
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException ex) {}
+        }
+        System.out.println(tr("OK - trying again."));
+    }
+
+    /**
+     * Replies the max. number of retries in case of 5XX errors on the server
+     *
+     * @return the max number of retries
+     */
+    protected int getMaxRetries() {
+        int ret = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
+        return Math.max(ret,0);
+    }
+
+    private String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor) throws SdsTransferException {
+        return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false);
+    }
+
+    private String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor, boolean doAuth) throws SdsTransferException {
+        return sendRequest(requestMethod, urlSuffix, requestBody, monitor, doAuth, false);
+    }
+    
+    public boolean updateSds(String message, ProgressMonitor pm) {
+    	try {
+			sendRequest("POST", "createshadows", message, pm);
+		} catch (SdsTransferException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+    	return true;
+    }
+
+    /**
+     * Generic method for sending requests to the OSM API.
+     *
+     * This method will automatically re-try any requests that are answered with a 5xx
+     * error code, or that resulted in a timeout exception from the TCP layer.
+     *
+     * @param requestMethod The http method used when talking with the server.
+     * @param urlSuffix The suffix to add at the server url, not including the version number,
+     *    but including any object ids (e.g. "/way/1234/history").
+     * @param requestBody the body of the HTTP request, if any.
+     * @param monitor the progress monitor
+     * @param doAuthenticate  set to true, if the request sent to the server shall include authentication
+     * credentials;
+     * @param fastFail true to request a short timeout
+     *
+     * @return the body of the HTTP response, if and only if the response code was "200 OK".
+     * @exception SdsTransferException if the HTTP return code was not 200 (and retries have
+     *    been exhausted), or rewrapping a Java exception.
+     */
+    private String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor, boolean doAuthenticate, boolean fastFail) throws SdsTransferException {
+        StringBuffer responseBody = new StringBuffer();
+        int retries = getMaxRetries();
+
+        while(true) { // the retry loop
+            try {
+                URL url = new URL(new URL(getBaseUrl()), urlSuffix);
+                System.out.print(requestMethod + " " + url + "... ");
+                activeConnection = (HttpURLConnection)url.openConnection();
+                activeConnection.setConnectTimeout(fastFail ? 1000 : Main.pref.getInteger("socket.timeout.connect",15)*1000);
+                activeConnection.setRequestMethod(requestMethod);
+                if (doAuthenticate) {
+                    addAuth(activeConnection);
+                }
+
+                if (requestMethod.equals("PUT") || requestMethod.equals("POST") || requestMethod.equals("DELETE")) {
+                    activeConnection.setDoOutput(true);
+                    activeConnection.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
+                    OutputStream out = activeConnection.getOutputStream();
+
+                    // It seems that certain bits of the Ruby API are very unhappy upon
+                    // receipt of a PUT/POST message without a Content-length header,
+                    // even if the request has no payload.
+                    // Since Java will not generate a Content-length header unless
+                    // we use the output stream, we create an output stream for PUT/POST
+                    // even if there is no payload.
+                    if (requestBody != null) {
+                        BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
+                        bwr.write(requestBody);
+                        bwr.flush();
+                    }
+                    out.close();
+                }
+
+                activeConnection.connect();
+                System.out.println(activeConnection.getResponseMessage());
+                int retCode = activeConnection.getResponseCode();
+
+                if (retCode >= 500) {
+                    if (retries-- > 0) {
+                        sleepAndListen(retries, monitor);
+                        System.out.println(tr("Starting retry {0} of {1}.", getMaxRetries() - retries,getMaxRetries()));
+                        continue;
+                    }
+                }
+
+                // populate return fields.
+                responseBody.setLength(0);
+
+                // If the API returned an error code like 403 forbidden, getInputStream
+                // will fail with an IOException.
+                InputStream i = null;
+                try {
+                    i = activeConnection.getInputStream();
+                } catch (IOException ioe) {
+                    i = activeConnection.getErrorStream();
+                }
+                if (i != null) {
+                    // the input stream can be null if both the input and the error stream
+                    // are null. Seems to be the case if the OSM server replies a 401
+                    // Unauthorized, see #3887.
+                    //
+                    BufferedReader in = new BufferedReader(new InputStreamReader(i));
+                    String s;
+                    while((s = in.readLine()) != null) {
+                        responseBody.append(s);
+                        responseBody.append("\n");
+                    }
+                }
+                String errorHeader = null;
+                // Look for a detailed error message from the server
+                if (activeConnection.getHeaderField("Error") != null) {
+                    errorHeader = activeConnection.getHeaderField("Error");
+                    System.err.println("Error header: " + errorHeader);
+                } else if (retCode != 200 && responseBody.length()>0) {
+                    System.err.println("Error body: " + responseBody);
+                }
+                activeConnection.disconnect();
+
+                errorHeader = errorHeader == null? null : errorHeader.trim();
+                String errorBody = responseBody.length() == 0? null : responseBody.toString().trim();
+                switch(retCode) {
+                case HttpURLConnection.HTTP_OK:
+                    return responseBody.toString();
+                case HttpURLConnection.HTTP_FORBIDDEN:
+                    throw new SdsTransferException("FORBIDDEN");
+                default:
+                    throw new SdsTransferException(errorHeader + errorBody);
+                }
+            } catch (UnknownHostException e) {
+                throw new SdsTransferException(e);
+            } catch (SocketTimeoutException e) {
+                if (retries-- > 0) {
+                    continue;
+                }
+                throw new SdsTransferException(e);
+            } catch (ConnectException e) {
+                if (retries-- > 0) {
+                    continue;
+                }
+                throw new SdsTransferException(e);
+            } catch(IOException e){
+                throw new SdsTransferException(e);
+            } catch(SdsTransferException e) {
+                throw e;
+            }
+        }
+    }
+    
+    protected InputStream getInputStream(String urlStr, ProgressMonitor progressMonitor) throws SdsTransferException {
+        urlStr = getBaseUrl() + urlStr;
+    	try {
+            URL url = null;
+            try {
+                url = new URL(urlStr.replace(" ", "%20"));
+            } catch(MalformedURLException e) {
+                throw new SdsTransferException(e);
+            }
+            try {
+                activeConnection = (HttpURLConnection)url.openConnection();
+            } catch(Exception e) {
+                throw new SdsTransferException(tr("Failed to open connection to API {0}.", url.toExternalForm()), e);
+            }
+            if (cancel) {
+                activeConnection.disconnect();
+                return null;
+            }
+
+            addAuth(activeConnection);
+
+            if (cancel)
+                throw new SdsTransferException();
+            if (Main.pref.getBoolean("osm-server.use-compression", true)) {
+                activeConnection.setRequestProperty("Accept-Encoding", "gzip, deflate");
+            }
+
+            activeConnection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
+
+            try {
+                System.out.println("GET " + url);
+                activeConnection.connect();
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw new SdsTransferException(tr("Could not connect to the OSM server. Please check your internet connection."), e);
+            }
+            try {
+                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED)
+                    throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED,null,null);
+
+                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_PROXY_AUTH)
+                    throw new OsmTransferCanceledException();
+
+                String encoding = activeConnection.getContentEncoding();
+                if (activeConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                    String errorHeader = activeConnection.getHeaderField("Error");
+                    StringBuilder errorBody = new StringBuilder();
+                    try
+                    {
+                        InputStream i = FixEncoding(activeConnection.getErrorStream(), encoding);
+                        if (i != null) {
+                            BufferedReader in = new BufferedReader(new InputStreamReader(i));
+                            String s;
+                            while((s = in.readLine()) != null) {
+                                errorBody.append(s);
+                                errorBody.append("\n");
+                            }
+                        }
+                    }
+                    catch(Exception e) {
+                        errorBody.append(tr("Reading error text failed."));
+                    }
+
+                    throw new OsmApiException(activeConnection.getResponseCode(), errorHeader, errorBody.toString());
+                }
+
+                return FixEncoding(new ProgressInputStream(activeConnection, progressMonitor), encoding);
+            } catch(Exception e) {
+                if (e instanceof SdsTransferException)
+                    throw (SdsTransferException)e;
+                else
+                    throw new SdsTransferException(e);
+
+            }
+        } finally {
+            progressMonitor.invalidate();
+        }
+    }
+
+    private InputStream FixEncoding(InputStream stream, String encoding) throws IOException
+    {
+        if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
+            stream = new GZIPInputStream(stream);
+        }
+        else if (encoding != null && encoding.equalsIgnoreCase("deflate")) {
+            stream = new InflaterInputStream(stream, new Inflater(true));
+        }
+        return stream;
+    }
+
+
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsConnection.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsConnection.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsConnection.java	(revision 28160)
@@ -0,0 +1,105 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.hot.sds;
+
+import java.net.HttpURLConnection;
+import java.net.Authenticator.RequestorType;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import org.openstreetmap.josm.io.auth.CredentialsAgentException;
+import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
+import org.openstreetmap.josm.tools.Base64;
+
+/**
+ * Base class that handles common things like authentication for the reader and writer
+ * to the SDS server.
+ * 
+ * Modeled after JOSM's OsmConnection class.
+ */
+public class SdsConnection {
+	
+    protected boolean cancel = false;
+    protected HttpURLConnection activeConnection;
+    private SdsCredentialAgent credAgent = new SdsCredentialAgent();
+    
+    /**
+     * Initialize the http defaults and the authenticator.
+     */
+    static {
+        try {
+            HttpURLConnection.setFollowRedirects(true);
+        } catch (SecurityException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void cancel() {
+        cancel = true;
+        synchronized (this) {
+            if (activeConnection != null) {
+                activeConnection.setConnectTimeout(100);
+                activeConnection.setReadTimeout(100);
+            }
+        }
+        try {
+            Thread.sleep(100);
+        } catch (InterruptedException ex) {
+        }
+
+        synchronized (this) {
+            if (activeConnection != null) {
+                activeConnection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * Adds an authentication header for basic authentication
+     *
+     * @param con the connection
+     * @throws SdsTransferException thrown if something went wrong. Check for nested exceptions
+     */
+    protected void addBasicAuthorizationHeader(HttpURLConnection con) throws SdsTransferException {
+        CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
+        CredentialsAgentResponse response;
+        String token;
+        try {
+                response = credAgent.getCredentials(RequestorType.SERVER, con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
+        } catch (CredentialsAgentException e) {
+            throw new SdsTransferException(e);
+        }
+        if (response == null) {
+            token = ":";
+        } else if (response.isCanceled()) {
+            cancel = true;
+            return;
+        } else {
+            String username= response.getUsername() == null ? "" : response.getUsername();
+            String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
+            token = username + ":" + password;
+            try {
+                ByteBuffer bytes = encoder.encode(CharBuffer.wrap(token));
+                con.addRequestProperty("Authorization", "Basic "+Base64.encode(bytes));
+            } catch(CharacterCodingException e) {
+                throw new SdsTransferException(e);
+            }
+        }
+    }
+
+     protected void addAuth(HttpURLConnection connection) throws SdsTransferException {
+            addBasicAuthorizationHeader(connection);
+    }
+
+    /**
+     * Replies true if this connection is canceled
+     *
+     * @return true if this connection is canceled
+     * @return
+     */
+    public boolean isCanceled() {
+        return cancel;
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialAgent.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialAgent.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialAgent.java	(revision 28160)
@@ -0,0 +1,192 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.net.PasswordAuthentication;
+import java.net.Authenticator.RequestorType;
+import java.util.Map;
+import java.util.HashMap;
+
+import javax.swing.text.html.HTMLEditorKit;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.oauth.OAuthToken;
+import org.openstreetmap.josm.gui.io.CredentialDialog;
+import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel;
+import org.openstreetmap.josm.gui.widgets.HtmlPanel;
+import org.openstreetmap.josm.io.auth.AbstractCredentialsAgent;
+import org.openstreetmap.josm.io.auth.CredentialsAgentException;
+import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
+
+/**
+ * Factored after JOSM's JosmPreferencesCredentialAgent. 
+ */
+public class SdsCredentialAgent extends AbstractCredentialsAgent {
+
+    Map<RequestorType, PasswordAuthentication> sdsMemoryCredentialsCache = new HashMap<RequestorType, PasswordAuthentication>();
+
+    /**
+     * @see CredentialsAgent#lookup(RequestorType)
+     */
+    @Override
+    public PasswordAuthentication lookup(RequestorType requestorType, String host) throws CredentialsAgentException{
+        if (requestorType == null)
+            return null;
+        String user;
+        String password;
+        switch(requestorType) {
+        case SERVER:
+            user = Main.pref.get("sds-server.username", null);
+            password = Main.pref.get("sds-server.password", null);
+            if (user == null)
+                return null;
+            return new PasswordAuthentication(user, password == null ? new char[0] : password.toCharArray());
+        case PROXY:
+            user = Main.pref.get(ProxyPreferencesPanel.PROXY_USER, null);
+            password = Main.pref.get(ProxyPreferencesPanel.PROXY_PASS, null);
+            if (user == null)
+                return null;
+            return new PasswordAuthentication(user, password == null ? new char[0] : password.toCharArray());
+        }
+        return null;
+    }
+
+    /**
+     * @see CredentialsAgent#store(RequestorType, PasswordAuthentication)
+     */
+    @Override
+    public void store(RequestorType requestorType, String host, PasswordAuthentication credentials) throws CredentialsAgentException {
+        if (requestorType == null)
+            return;
+        switch(requestorType) {
+        case SERVER:
+            Main.pref.put("sds-server.username", credentials.getUserName());
+            if (credentials.getPassword() == null) {
+                Main.pref.put("sds-server.password", null);
+            } else {
+                Main.pref.put("sds-server.password", String.valueOf(credentials.getPassword()));
+            }
+            break;
+        case PROXY:
+            Main.pref.put(ProxyPreferencesPanel.PROXY_USER, credentials.getUserName());
+            if (credentials.getPassword() == null) {
+                Main.pref.put(ProxyPreferencesPanel.PROXY_PASS, null);
+            } else {
+                Main.pref.put(ProxyPreferencesPanel.PROXY_PASS, String.valueOf(credentials.getPassword()));
+            }
+            break;
+        }
+    }
+
+    /**
+     * Lookup the current OAuth Access Token to access the OSM server. Replies null, if no
+     * Access Token is currently managed by this CredentialManager.
+     *
+     * @return the current OAuth Access Token to access the OSM server.
+     * @throws CredentialsAgentException thrown if something goes wrong
+     */
+    @Override
+    public OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException {
+        String accessTokenKey = Main.pref.get("oauth.access-token.key", null);
+        String accessTokenSecret = Main.pref.get("oauth.access-token.secret", null);
+        if (accessTokenKey == null && accessTokenSecret == null)
+            return null;
+        return new OAuthToken(accessTokenKey, accessTokenSecret);
+    }
+
+    @Override
+    public Component getPreferencesDecorationPanel() {
+        HtmlPanel pnlMessage = new HtmlPanel();
+        HTMLEditorKit kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit();
+        kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
+        pnlMessage.setText(
+                tr(
+                        "<html><body>"
+                        + "<p class=\"warning-body\">"
+                        + "<strong>Warning:</strong> The password is stored in plain text in the JOSM preferences file. "
+                        + "</p>"
+                        + "</body></html>"
+                )
+        );
+        return pnlMessage;
+    }
+    
+    @Override
+    public String getSaveUsernameAndPasswordCheckboxText() {
+        return tr("Save user and password (unencrypted)");
+    }
+
+	@Override
+	public void storeOAuthAccessToken(OAuthToken accessToken)
+			throws CredentialsAgentException {
+		// no-op
+		
+	}
+	
+    @Override
+    public CredentialsAgentResponse getCredentials(RequestorType requestorType, String host, boolean noSuccessWithLastResponse) throws CredentialsAgentException{
+        if (requestorType == null)
+            return null;
+        PasswordAuthentication credentials =  lookup(requestorType, host);
+        String username = (credentials == null || credentials.getUserName() == null) ? "" : credentials.getUserName();
+        String password = (credentials == null || credentials.getPassword() == null) ? "" : String.valueOf(credentials.getPassword());
+
+        CredentialsAgentResponse response = new CredentialsAgentResponse();
+
+        /*
+         * Last request was successful and there was no credentials stored
+         * in file (or only the username is stored).
+         * -> Try to recall credentials that have been entered
+         * manually in this session.
+         */
+        if (!noSuccessWithLastResponse && sdsMemoryCredentialsCache.containsKey(requestorType) &&
+                (credentials == null || credentials.getPassword() == null || credentials.getPassword().length == 0)) {
+            PasswordAuthentication pa = sdsMemoryCredentialsCache.get(requestorType);
+            response.setUsername(pa.getUserName());
+            response.setPassword(pa.getPassword());
+            response.setCanceled(false);
+        /*
+         * Prompt the user for credentials. This happens the first time each
+         * josm start if the user does not save the credentials to preference
+         * file (username=="") and each time after authentication failed
+         * (noSuccessWithLastResponse == true).
+         */
+        } else if (noSuccessWithLastResponse || username.equals("") || password.equals("")) {
+            CredentialDialog dialog = null;
+            switch(requestorType) {
+            case SERVER: dialog = SdsCredentialDialog.getSdsApiCredentialDialog(username, password, host, getSaveUsernameAndPasswordCheckboxText()); break;
+            case PROXY: dialog = CredentialDialog.getHttpProxyCredentialDialog(username, password, host, getSaveUsernameAndPasswordCheckboxText()); break;
+            }
+            dialog.setVisible(true);
+            response.setCanceled(dialog.isCanceled());
+            if (dialog.isCanceled())
+                return response;
+            response.setUsername(dialog.getUsername());
+            response.setPassword(dialog.getPassword());
+            if (dialog.isSaveCredentials()) {
+                store(requestorType, host, new PasswordAuthentication(
+                        response.getUsername(),
+                        response.getPassword()
+                ));
+            /*
+             * User decides not to save credentials to file. Keep it
+             * in memory so we don't have to ask over and over again.
+             */
+            } else {
+                PasswordAuthentication pa = new PasswordAuthentication(dialog.getUsername(), dialog.getPassword());
+                sdsMemoryCredentialsCache.put(requestorType, pa);
+            }
+        /*
+         * We got it from file.
+         */
+        } else {
+            response.setUsername(username);
+            response.setPassword(password.toCharArray());
+            response.setCanceled(false);
+        }
+        return response;
+    }
+    
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialDialog.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialDialog.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsCredentialDialog.java	(revision 28160)
@@ -0,0 +1,52 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+
+import org.openstreetmap.josm.gui.io.CredentialDialog;
+
+@SuppressWarnings("serial")
+public class SdsCredentialDialog extends CredentialDialog {
+
+	static public SdsCredentialDialog getSdsApiCredentialDialog(String username, String password, String host, String saveUsernameAndPasswordCheckboxText) {
+        SdsCredentialDialog dialog = new SdsCredentialDialog(saveUsernameAndPasswordCheckboxText);
+        dialog.prepareForSdsApiCredentials(username, password);
+        dialog.pack();
+        return dialog;
+    }
+
+    String saveUsernameAndPasswordCheckboxText;
+
+    public SdsCredentialDialog(String saveUsernameAndPasswordCheckboxText) {
+    	super(saveUsernameAndPasswordCheckboxText);
+    }
+    	 
+    public void prepareForSdsApiCredentials(String username, String password) {
+        setTitle(tr("Enter credentials for Separate Data Store API"));
+        getContentPane().add(pnlCredentials = new SdsApiCredentialsPanel(this), BorderLayout.CENTER);
+        pnlCredentials.init(username, password);
+        validate();
+    }
+ 
+    private static class SdsApiCredentialsPanel extends CredentialPanel {
+
+		@Override
+        protected void build() {
+            super.build();
+            tfUserName.setToolTipText(tr("Please enter the user name of your SDS account"));
+            tfPassword.setToolTipText(tr("Please enter the password of your SDS account"));
+            lblHeading.setText(
+                    "<html>" + tr("Authenticating at the SDS API ''{0}'' failed. Please enter a valid username and a valid password.",
+                            SdsApi.getSdsApi().getBaseUrl()) + "</html>");
+            lblWarning.setText(tr("Warning: The password is transferred unencrypted."));
+        }
+
+        public SdsApiCredentialsPanel(SdsCredentialDialog owner) {
+            super(owner);
+            build();
+        }
+    }
+
+  }
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsDiskAccessAction.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsDiskAccessAction.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsDiskAccessAction.java	(revision 28160)
@@ -0,0 +1,119 @@
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+
+import javax.swing.JFileChooser;
+import javax.swing.filechooser.FileFilter;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.DiskAccessAction;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.tools.Shortcut;
+
+@SuppressWarnings("serial")
+public abstract class SdsDiskAccessAction extends DiskAccessAction {
+	
+    public SdsDiskAccessAction(String name, String iconName, String tooltip,
+			Shortcut shortcut) {
+		super(name, iconName, tooltip, shortcut);
+	}
+
+	public static JFileChooser createAndOpenFileChooser(boolean open, boolean multiple, String title) {
+        String curDir = Main.pref.get("lastDirectory");
+        if (curDir.equals("")) {
+            curDir = ".";
+        }
+        JFileChooser fc = new JFileChooser(new File(curDir));
+        if (title != null) {
+            fc.setDialogTitle(title);
+        }
+
+        fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+        fc.setMultiSelectionEnabled(multiple);
+        fc.setAcceptAllFileFilterUsed(false);
+        		
+        fc.setFileFilter(new FileFilter() {
+        	public boolean accept(File pathname) { return pathname.getName().endsWith(".sds") || pathname.isDirectory(); }
+			public String getDescription() { return (tr("SDS data file")); }
+        });     
+
+        int answer = open ? fc.showOpenDialog(Main.parent) : fc.showSaveDialog(Main.parent);
+        if (answer != JFileChooser.APPROVE_OPTION)
+            return null;
+
+        if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
+            Main.pref.put("lastDirectory", fc.getCurrentDirectory().getAbsolutePath());
+        }
+
+        if (!open) {
+            File file = fc.getSelectedFile();
+            if (file != null && file.exists()) {
+                ExtendedDialog dialog = new ExtendedDialog(
+                        Main.parent,
+                        tr("Overwrite"),
+                        new String[] {tr("Overwrite"), tr("Cancel")}
+                );
+                dialog.setContent(tr("File exists. Overwrite?"));
+                dialog.setButtonIcons(new String[] {"save_as.png", "cancel.png"});
+                dialog.showDialog();
+                if (dialog.getValue() != 1)
+                    return null;
+            }
+        }
+
+        return fc;
+    }
+
+    public static File createAndOpenSaveFileChooser(String title) {
+        String curDir = Main.pref.get("lastDirectory");
+        if (curDir.equals("")) {
+            curDir = ".";
+        }
+        JFileChooser fc = new JFileChooser(new File(curDir));
+        if (title != null) {
+            fc.setDialogTitle(title);
+        }
+
+        fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        fc.setMultiSelectionEnabled(false);
+        fc.setAcceptAllFileFilterUsed(false);
+        
+        fc.setFileFilter(new FileFilter() {
+        	public boolean accept(File pathname) { return pathname.getName().endsWith(".sds") || pathname.isDirectory(); }
+			public String getDescription() { return (tr("SDS data file")); }
+        }); 
+        
+        int answer = fc.showSaveDialog(Main.parent);
+        if (answer != JFileChooser.APPROVE_OPTION)
+            return null;
+
+        if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
+            Main.pref.put("lastDirectory", fc.getCurrentDirectory().getAbsolutePath());
+        }
+
+        File file = fc.getSelectedFile();
+        String fn = file.getPath();
+
+        if (!confirmOverwrite(file))
+            return null;
+        return file;
+    }
+    
+    public static boolean confirmOverwrite(File file) {
+        if (file == null || (file.exists())) {
+            ExtendedDialog dialog = new ExtendedDialog(
+                    Main.parent,
+                    tr("Overwrite"),
+                    new String[] {tr("Overwrite"), tr("Cancel")}
+            );
+            dialog.setContent(tr("File exists. Overwrite?"));
+            dialog.setButtonIcons(new String[] {"save_as.png", "cancel.png"});
+            dialog.showDialog();
+            return (dialog.getValue() == 1);
+        }
+        return true;
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsLoadAction.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsLoadAction.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsLoadAction.java	(revision 28160)
@@ -0,0 +1,110 @@
+//License: GPL.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JFileChooser;
+import javax.swing.filechooser.FileFilter;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.DiskAccessAction;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.tools.Shortcut;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+@SuppressWarnings("serial")
+public class SdsLoadAction extends SdsDiskAccessAction {
+	
+	private SeparateDataStorePlugin plugin;
+
+    @SuppressWarnings("deprecation")
+	public SdsLoadAction(SeparateDataStorePlugin p) {
+        super(tr("Load..."), "sds_load", tr("Load separate data store data from a file."),
+            null);
+    	plugin = p;
+    }
+    
+    public void actionPerformed(ActionEvent e) {
+        JFileChooser fc = createAndOpenFileChooser(true, true, null);
+        if (fc == null)
+            return;
+        File[] files = fc.getSelectedFiles();
+        openFiles(Arrays.asList(files));
+    }
+
+    public void openFiles(List<File> fileList) {
+        OpenFileTask task = new OpenFileTask(fileList, plugin);
+        Main.worker.submit(task);
+    }
+
+    public class OpenFileTask extends PleaseWaitRunnable {
+        private List<File> files;
+        private boolean canceled;
+        private SeparateDataStorePlugin plugin;
+
+        public OpenFileTask(List<File> files, SeparateDataStorePlugin p) {
+            super(tr("Loading files"), false /* don't ignore exception */);
+            this.files = new ArrayList<File>(files);
+            plugin = p;
+        }
+
+        @Override
+        protected void cancel() {
+            this.canceled = true;
+        }
+
+        @Override
+        protected void finish() {
+            // do nothing
+        }
+
+        @Override
+        protected void realRun() throws SAXException, IOException, OsmTransferException {
+            if (files == null || files.isEmpty()) return;
+
+            getProgressMonitor().setTicksCount(files.size());
+
+            for (File f : files) {
+                if (canceled) return;
+                getProgressMonitor().indeterminateSubTask(tr("Opening file ''{0}'' ...", f.getAbsolutePath()));
+
+                InputStream fileStream;
+                try {
+                    fileStream = new FileInputStream(f);
+                    SdsParser p = new SdsParser(Main.main.getCurrentDataSet(), plugin, false);
+                    InputSource inputSource = new InputSource(fileStream);
+                    SAXParserFactory.newInstance().newSAXParser().parse(inputSource, p);
+                } catch (UnsupportedEncodingException e1) {
+                    // TODO Auto-generated catch block
+                    e1.printStackTrace();
+                } catch (SAXException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                } catch (IOException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                } catch (ParserConfigurationException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+
+            }
+        }
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsMenu.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsMenu.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsMenu.java	(revision 28160)
@@ -0,0 +1,140 @@
+//License: GPL. See README for details.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.lang.reflect.Method;
+
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.preferences.PreferenceDialog;
+
+@SuppressWarnings("serial")
+public class SdsMenu extends JMenu implements LayerChangeListener {
+
+    private JMenuItem saveItem;
+    private JMenuItem loadItem;
+    private JMenuItem prefsItem;
+    private JMenuItem aboutItem;
+    private JMenu menu;
+
+    public SdsMenu(final SeparateDataStorePlugin thePlugin) {
+        MainMenu mm = Main.main.menu;
+        menu = mm.addMenu(marktr("SDS"), KeyEvent.VK_S, mm.defaultMenuPos, null);
+        saveItem = new JMenuItem(new SdsSaveAction());
+        menu.add(saveItem);
+        loadItem = new JMenuItem(new SdsLoadAction(thePlugin));
+        menu.add(loadItem);
+        menu.addSeparator();
+        prefsItem = new JMenuItem(new SdsPreferencesAction());
+        menu.add(prefsItem);
+        menu.addSeparator();
+        aboutItem = new JMenuItem(new SdsAboutAction());
+        menu.add(aboutItem);
+        
+        MapView.addLayerChangeListener(this);
+        setEnabledState();
+    }
+
+    void setEnabledState() {
+    	boolean en = (Main.map != null) && (Main.map.mapView != null) && (Main.map.mapView.getActiveLayer() instanceof OsmDataLayer);
+    	loadItem.setEnabled(en);
+    	saveItem.setEnabled(en);
+    }
+  
+	@Override
+	public void activeLayerChange(Layer oldLayer, Layer newLayer) {	setEnabledState(); }
+
+	@Override
+	public void layerAdded(Layer newLayer) { setEnabledState(); }
+
+	@Override
+	public void layerRemoved(Layer oldLayer) { setEnabledState(); }
+
+	private class SdsAboutAction extends JosmAction {
+
+	    public SdsAboutAction() {
+	        super(tr("About"), "sds", tr("Information about SDS."), null, true);
+	    }
+
+	    public void actionPerformed(ActionEvent e) {
+	        JPanel about = new JPanel();
+
+	        JTextArea l = new JTextArea();
+	        l.setLineWrap(true);
+	        l.setWrapStyleWord(true);
+	        l.setEditable(false);
+	        l.setText("Separate Data Store\n\nThis plugin provides access to a \"Separate Data Store\" server. " +
+	        		"Whenever data is loaded from the OSM API, it queries the SDS for additional tags that have been stored for the objects just loaded, " +
+	        		"and adds these tags. When you upload data to JOSM, SDS tags will again be separated and, instead of sending them to OSM, they will be uplaoded to SDS." +
+	        		"\n\n" +
+	        		"This depends on SDS tags starting with a special prefix, which can be configured in the SDS preferences." + 
+	        		"\n\n" + 
+	        		"Using the SDS server will usually require an account to be set up there, which is completely independent of your OSM account.");
+	        
+	        l.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+	        l.setOpaque(false);
+	        l.setPreferredSize(new Dimension(500,300));
+	        JScrollPane sp = new JScrollPane(l);
+	        sp.setBorder(null);
+	        sp.setOpaque(false);
+	        	        
+	        about.add(sp);
+	        
+	        about.setPreferredSize(new Dimension(500,300));
+
+	        JOptionPane.showMessageDialog(Main.parent, about, tr("About SDS..."),
+	                JOptionPane.INFORMATION_MESSAGE, null);
+	    }
+	}
+	
+	private class SdsPreferencesAction extends JosmAction implements Runnable {
+
+	    private SdsPreferencesAction() {
+	        super(tr("Preferences..."), "preference", tr("Open a preferences dialog for SDS."),
+	                null, true);
+	        putValue("help", ht("/Action/Preferences"));
+	    }
+
+	    /**
+	     * Launch the preferences dialog.
+	     */
+	    public void actionPerformed(ActionEvent e) {
+	        run();
+	    }
+
+	    public void run() {
+	    	PreferenceDialog pd = new PreferenceDialog(Main.parent);
+	    	// unusual reflection mechanism to cater for older JOSM versions where 
+	    	// the selectPreferencesTabByName method was not public
+	    	try {
+	    		Method sptbn = pd.getClass().getMethod("selectPreferencesTabByName", String.class);
+	    		sptbn.invoke(pd, "sds");
+	    	} catch (Exception ex) {
+	    		// ignore
+	    	}
+	    	pd.setVisible(true);
+	    }
+	}
+
+
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriter.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriter.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriter.java	(revision 28160)
@@ -0,0 +1,58 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.hot.sds;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+import org.openstreetmap.josm.data.osm.Changeset;
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.io.OsmWriter;
+import org.openstreetmap.josm.io.XmlWriter;
+
+/**
+ * This is a special version of JOSM's OsmWriter that makes
+ * sure that special tags are never written to JOSM's standard
+ * output channels.
+ * 
+ * In the context of HOT's separate data store, this is very
+ * important as otherwise private/confidential information could
+ * end up on public servers.
+ * 
+ * @author Frederik Ramm
+ *
+ */
+public class SdsOsmWriter extends OsmWriter {
+
+	private SeparateDataStorePlugin plugin;
+	
+    public SdsOsmWriter(SeparateDataStorePlugin plugin, PrintWriter out, boolean osmConform, String version) {
+        super(out, osmConform, version);
+        this.plugin = plugin;
+    }
+
+    @Override
+    protected void addTags(Tagged osm, String tagname, boolean tagOpen) {
+        if (osm.hasKeys()) {
+            if (tagOpen) {
+                out.println(">");
+            }
+            List<Entry<String, String>> entries = new ArrayList<Entry<String,String>>(osm.getKeys().entrySet());
+            Collections.sort(entries, byKeyComparator);
+            for (Entry<String, String> e : entries) {
+            	String key = e.getKey();
+                if (!(osm instanceof Changeset) && ("created_by".equals(key))) continue;
+                if (key.startsWith(plugin.getIgnorePrefix())) continue;          
+                out.println("    <tag k='"+ XmlWriter.encode(e.getKey()) +
+                            "' v='"+XmlWriter.encode(e.getValue())+ "' />");
+            }
+            out.println("  </" + tagname + ">");
+        } else if (tagOpen) {
+            out.println(" />");
+        } else {
+            out.println("  </" + tagname + ">");
+        }
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriterFactory.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriterFactory.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsOsmWriterFactory.java	(revision 28160)
@@ -0,0 +1,27 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import java.io.PrintWriter;
+
+import org.openstreetmap.josm.io.OsmWriter;
+import org.openstreetmap.josm.io.OsmWriterFactory;
+
+/**
+ * Replaces JOSM's original writer factory, so that JOSM uses 
+ * our writer instead of its own.
+ * 
+ * @author Frederik Ramm
+ */
+public class SdsOsmWriterFactory extends OsmWriterFactory {
+
+	SeparateDataStorePlugin plugin;
+	
+	public SdsOsmWriterFactory(SeparateDataStorePlugin plugin) {
+		this.plugin = plugin;
+	}
+	
+	@Override
+    protected OsmWriter createOsmWriterImpl(PrintWriter out, boolean osmConform, String version) {
+        return new SdsOsmWriter(plugin, out, osmConform, version);
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsParser.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsParser.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsParser.java	(revision 28160)
@@ -0,0 +1,69 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Parser for answers from SDS. These anwers look like this:
+ * 
+ * <pre>
+<?xml version="1.0" encoding="UTF-8"?>
+<osm_sds>
+  <osm_shadow osm_id="499770" osm_type="way">
+    <tag k="hot:bbb:grant_received_date" v="a"/>
+    <tag k="hot:bbb:grant_received" v="b"/>
+    <tag k="hot:bbb:home_owner_name" v="lll"/>
+  </osm_shadow>
+</osm_sds>
+ * </pre>
+ * @author Frederik Ramm
+ */
+public class SdsParser extends DefaultHandler
+{
+    private DataSet dataSet;
+    private OsmPrimitive currentPrimitive;
+    private SeparateDataStorePlugin plugin;
+    private boolean ensureMatch;
+    
+    public SdsParser(DataSet ds, SeparateDataStorePlugin p, boolean ensureMatch) {
+        this.dataSet = ds;
+        plugin = p;
+        this.ensureMatch = ensureMatch;
+    }
+    
+    public SdsParser(DataSet ds, SeparateDataStorePlugin p) {
+        this(ds, p, true);
+    }
+    
+    @Override public void endElement(String namespaceURI, String localName, String qName)
+    {
+        // after successfully reading a full set of tags from the separate data store,
+        // update it in our cache so we can determine changes later.
+        if ("osm_shadow".equals(qName) && currentPrimitive != null) {
+            plugin.learn(currentPrimitive.save());
+        }
+    }
+    @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException
+    {
+        if ("osm_shadow".equals(qName))
+        {
+            String type = atts.getValue("osm_type");
+            String id = atts.getValue("osm_id");     	
+            currentPrimitive = dataSet.getPrimitiveById(Long.parseLong(id), OsmPrimitiveType.fromApiTypeName(type));
+            if (currentPrimitive == null && ensureMatch) {
+                throw new SAXException("unexpected object in response");
+            }
+        }
+        else if ("tag".equals(qName))
+        {
+            String v = atts.getValue("v");
+            String k = atts.getValue("k");
+            if (currentPrimitive != null) currentPrimitive.put(k, v);
+        }
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsPluginPreferences.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsPluginPreferences.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsPluginPreferences.java	(revision 28160)
@@ -0,0 +1,114 @@
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Collections;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.tools.GBC;
+
+public class SdsPluginPreferences extends DefaultTabPreferenceSetting {
+
+    static final String SDS_SERVER = "sds-server.url";
+    static final String SDS_USERNAME = "sds-server.username";
+    static final String SDS_PASSWORD = "sds-server.password";
+    static final String SDS_PREFIX = "sds-server.tag-prefix";
+
+    private final JTextField server = new JTextField(8);
+    private final JTextField username = new JTextField(8);
+    private final JPasswordField password = new JPasswordField(8);
+    private final JTextField prefix = new JTextField(8);
+   
+    public SdsPluginPreferences() {
+    	super("sds", tr("Separate Data Store"), tr("Configures access to the Separate Data Store."));
+    }
+    @Override
+    public void addGui(final PreferenceTabbedPane gui) {
+        final JPanel tab = gui.createPreferenceTab(this);
+        	
+        final JPanel access = new JPanel(new GridBagLayout());
+        access.setBorder(BorderFactory.createTitledBorder(tr("Server")));
+
+        server.setText(Main.pref.get(SDS_SERVER, "http://datastore.hotosm.org"));
+        username.setText(Main.pref.get(SDS_USERNAME, ""));
+        password.setText(Main.pref.get(SDS_PASSWORD, ""));
+        prefix.setText(Main.pref.get(SDS_PREFIX, "hot:"));
+        server.setToolTipText(tr("The URL under which the SDS server can be contacted."));
+        username.setToolTipText(tr("The user name at the SDS server. You need to create an account with the SDS admin first."));
+        password.setToolTipText(tr("The password at the SDS server. You need to create an account with the SDS admin first."));
+        prefix.setToolTipText(tr("Tags beginning with this prefix are never saved to OSM, but to the SDS server only."));
+
+        access.add(new JLabel(tr("SDS server URL")), GBC.std());
+        access.add(server, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+
+        access.add(new JLabel(tr("SDS username")), GBC.std());
+        access.add(username, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+
+        access.add(new JLabel(tr("SDS password")), GBC.std());
+        access.add(password, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+
+        JButton test = new JButton(tr("Test credentials now"));
+        access.add(test, GBC.eol().anchor(GBC.EAST).insets(5,0,0,5));
+
+        tab.add(access, GBC.eol().fill(GBC.HORIZONTAL));
+
+        tab.add(new JLabel(tr("SDS tag prefix")), GBC.std());
+        tab.add(prefix, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+
+        tab.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL));
+
+        test.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent ev) {
+                SdsApi api = new SdsApi(server.getText());
+                String olduser = Main.pref.get(SDS_USERNAME);
+                String oldpass = Main.pref.get(SDS_PASSWORD);
+                Main.pref.put(SDS_USERNAME, username.getText());
+                Main.pref.put(SDS_PASSWORD, new String(password.getPassword()));
+                try {
+                    api.requestShadowsFromSds(Collections.singletonList(new Long(1)), null, null, null);
+                    JOptionPane.showMessageDialog(
+                            Main.parent,
+                            tr("Connection successful."),
+                            tr("Success"),
+                            JOptionPane.PLAIN_MESSAGE
+                    );
+                } catch(Exception ex) {
+                    JOptionPane.showMessageDialog(
+                            Main.parent,
+                            tr("Cannot connect to SDS server: ") + ex.getMessage(), 
+                            tr("Error"),
+                            JOptionPane.ERROR_MESSAGE
+                    );
+                }
+                // restore old credentials even if successful; user might still 
+                // choose to press cancel!
+                Main.pref.put(SDS_USERNAME, olduser);
+                Main.pref.put(SDS_PASSWORD, oldpass);
+            }
+        });
+    }
+
+    @Override
+    public boolean ok() {
+        Main.pref.put(SDS_SERVER, server.getText());
+        Main.pref.put(SDS_USERNAME, username.getText());
+        Main.pref.put(SDS_PASSWORD, new String(password.getPassword()));
+        Main.pref.put(SDS_PREFIX, prefix.getText());
+        return false;
+    }
+
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsSaveAction.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsSaveAction.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsSaveAction.java	(revision 28160)
@@ -0,0 +1,142 @@
+//License: GPL.
+package org.openstreetmap.hot.sds;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.filechooser.FileFilter;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.SaveActionBase;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.Shortcut;
+
+@SuppressWarnings("serial")
+public class SdsSaveAction extends SdsDiskAccessAction {
+
+    @SuppressWarnings("deprecation")
+	public SdsSaveAction() {
+        super(tr("Save..."), "sds_save", tr("Save the current separate data store information to a file."),
+            null);
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        if (!isEnabled())
+            return;
+        doSave();
+    }
+
+    public boolean doSave() {
+        Layer layer = null;
+        if (Main.isDisplayingMapView() && (Main.map.mapView.getActiveLayer() instanceof OsmDataLayer))
+            layer = Main.map.mapView.getActiveLayer();
+        
+        if (layer == null)
+            return false;
+        return doSave(layer);
+    }
+
+     public boolean doSave(Layer iLayer) {
+        if (iLayer == null)
+            return false;
+        if (!(iLayer instanceof OsmDataLayer))
+            return false;
+        OsmDataLayer layer = (OsmDataLayer) iLayer;
+
+        File file = createAndOpenSaveFileChooser(tr("Save SDS file"));
+
+        if (file == null)
+            return false;
+
+        File tmpFile = null;
+
+        try {
+
+            if (file.exists()) {
+                tmpFile = new File(file.getPath() + "~");
+                copy(file, tmpFile);
+            }
+
+            OutputStream out = new FileOutputStream(file);
+            Writer writer = new OutputStreamWriter(out, "UTF-8");
+
+            SdsWriter w = new SdsWriter(new PrintWriter(writer));
+            layer.data.getReadLock().lock();
+            try {
+                w.header();
+                for (IPrimitive p : layer.data.allNonDeletedPrimitives()) {
+                    w.write(p, p.getKeys());
+                }
+                w.footer();
+                w.close();
+            } finally {
+                layer.data.getReadLock().unlock();
+            }
+            // FIXME - how to close?
+            if (!Main.pref.getBoolean("save.keepbackup", false) && (tmpFile != null)) {
+                tmpFile.delete();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            JOptionPane.showMessageDialog(
+                    Main.parent,
+                    tr("<html>An error occurred while saving.<br>Error is:<br>{0}</html>", e.getMessage()),
+                    tr("Error"),
+                    JOptionPane.ERROR_MESSAGE
+            );
+
+            try {
+                // if the file save failed, then the tempfile will not
+                // be deleted.  So, restore the backup if we made one.
+                if (tmpFile != null && tmpFile.exists()) {
+                    copy(tmpFile, file);
+                }
+            } catch (IOException e2) {
+                e2.printStackTrace();
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("<html>An error occurred while restoring backup file.<br>Error is:<br>{0}</html>", e2.getMessage()),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE
+                );
+            }
+        }
+        return true;
+    }
+
+    private void copy(File src, File dst) throws IOException {
+        FileInputStream srcStream;
+        FileOutputStream dstStream;
+        try {
+            srcStream = new FileInputStream(src);
+            dstStream = new FileOutputStream(dst);
+        } catch (FileNotFoundException e) {
+            JOptionPane.showMessageDialog(Main.parent, tr("Could not back up file. Exception is: {0}", e
+                    .getMessage()), tr("Error"), JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+        byte buf[] = new byte[1 << 16];
+        int len;
+        while ((len = srcStream.read(buf)) != -1) {
+            dstStream.write(buf, 0, len);
+        }
+        srcStream.close();
+        dstStream.close();
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsTransferException.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsTransferException.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsTransferException.java	(revision 28160)
@@ -0,0 +1,36 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+@SuppressWarnings("serial")
+public class SdsTransferException extends Exception {
+
+    private String url = SdsApi.getSdsApi().getBaseUrl();
+
+    public SdsTransferException() {
+    }
+
+    public SdsTransferException(String message) {
+        super(message);
+    }
+
+    public SdsTransferException(Throwable cause) {
+        super(cause);
+    }
+
+    public SdsTransferException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    /**
+     *
+     * @return Api base url or url set using setUrl method
+     */
+    public String getUrl() {
+        return url;
+    }
+
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsWriter.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsWriter.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SdsWriter.java	(revision 28160)
@@ -0,0 +1,55 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.hot.sds;
+
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.io.XmlWriter;
+
+/**
+ * Save the dataset into a stream as osm intern xml format. This is not using any
+ * xml library for storing.
+ * @author imi
+ */
+public class SdsWriter extends XmlWriter {
+
+    protected SdsWriter(PrintWriter out) {
+        super(out);
+    }
+
+     public void header() {
+        out.println("<?xml version='1.0' encoding='UTF-8'?>");
+        out.print("<osm_sds>");
+    }
+    public void footer() {
+        out.println("</osm_sds>");
+    }
+
+    public void write(IPrimitive what, Map<String,String> tags) {
+    	out.print("<osm_shadow osm_type=\"");
+    	out.print(what.getType().getAPIName());
+    	out.print("\" osm_id=\"");
+    	out.print(what.getId());
+    	out.println("\">");
+        
+    	if (tags != null) {
+    		for(Entry<String,String> e : tags.entrySet()) {
+    			out.println("    <tag k='"+ XmlWriter.encode(e.getKey()) +
+    					"' v='"+XmlWriter.encode(e.getValue())+ "' />");
+    		}
+    	}
+    	
+    	out.println("</osm_shadow>");
+    }
+
+    public void close() {
+        out.close();
+    }
+
+    @Override
+    public void flush() {
+        out.flush();
+    }
+}
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SeparateDataStorePlugin.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SeparateDataStorePlugin.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/SeparateDataStorePlugin.java	(revision 28160)
@@ -0,0 +1,136 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.hot.sds;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.UploadAction;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.io.OsmReader;
+import org.openstreetmap.josm.io.OsmServerWriter;
+import org.openstreetmap.josm.io.OsmWriterFactory;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+/**
+ *
+ * Plugin that loads extra data from HOT separate data store.
+ *
+ * @author Frederik Ramm <frederik.ramm@geofabrik.de>
+ */
+public class SeparateDataStorePlugin extends Plugin 
+{
+
+	public HashMap<Long, IPrimitive> originalNodes = new HashMap<Long, IPrimitive>();
+	public HashMap<Long, IPrimitive> originalWays = new HashMap<Long, IPrimitive>();
+	public HashMap<Long, IPrimitive> originalRelations = new HashMap<Long, IPrimitive>();
+	
+	public ArrayList<QueueItem> uploadQueue = new ArrayList<QueueItem>();
+	
+	private PrimitiveVisitor learnVisitor = new PrimitiveVisitor() {
+		public void visit(INode i) { originalNodes.put(i.getId(), i); }
+		public void visit(IWay i) { originalWays.put(i.getId(), i); }
+		public void visit(IRelation i) { originalRelations.put(i.getId(), i); }
+	};
+	
+	class QueueItem {
+		public IPrimitive primitive;
+		public HashMap<String,String> tags;
+		public boolean sdsOnly;
+		public boolean processed;
+		public QueueItem(IPrimitive p, HashMap<String,String> t, boolean s) {
+			primitive = p;
+			tags = t;
+			sdsOnly = s;
+			processed = false;
+		}
+	}
+	
+    /**
+     * Creates the plugin
+     */
+    public SeparateDataStorePlugin(PluginInformation info) 
+    {
+        super(info);
+    	System.out.println("initializing SDS plugin");
+    	
+    	// this lets us see what JOSM load from the server, and augment it with our data:
+        OsmReader.registerPostprocessor(new ReadPostprocessor(this));
+        
+        // this makes sure that our data is never written to the OSM server on a low level;
+        OsmWriterFactory.theFactory = new SdsOsmWriterFactory(this);
+        
+        // this lets us see what JOSM is planning to upload, and prepare our own uploads
+        // accordingly
+        UploadAction.registerUploadHook(new DetermineSdsModificationsUploadHook(this));
+        
+        // this lets us perform our own uploads after JOSM has succeeded:
+        OsmServerWriter.registerPostprocessor(new WritePostprocessor(this));
+
+        // add menu
+        new SdsMenu(this);
+    }
+
+	public String getIgnorePrefix() {
+        return Main.pref.get("sds-server.tag-prefix", "hot:");
+	}
+	
+	public IPrimitive getOriginalPrimitive(IPrimitive other) {
+		switch (other.getType()) {
+		case NODE: return originalNodes.get(other.getId());
+		case WAY: return originalWays.get(other.getId());
+		case RELATION: return originalRelations.get(other.getId());
+		}
+		return null;	
+	}
+	
+	protected void enqueueForUpload(IPrimitive prim, HashMap<String, String> tags, boolean onlySds) {
+		uploadQueue.add(new QueueItem(prim, tags, onlySds));
+	}
+	
+	/** 
+	 * Stores the given primitive in the plugin's cache in order to
+	 * determine changes later.
+	 * @param prim
+	 */
+	protected void learn(IPrimitive prim) {
+		if (prim instanceof OsmPrimitive) {
+			((OsmPrimitive)prim).save().visit(learnVisitor);
+		} else {
+			prim.visit(learnVisitor);
+		}
+	}
+	
+	/**
+	 * removes all elements from the upload queue that have the processed flag set.
+	 */
+	protected void clearQueue() {
+		ArrayList<QueueItem> newQueue = new ArrayList<QueueItem>();
+		for (QueueItem q : uploadQueue) {
+			if (!q.processed) newQueue.add(q);
+		}
+		uploadQueue = newQueue;
+	}
+	
+	/**
+	 * reset the processed flag for all elements of the queue.
+	 */
+	protected void resetQueue() {
+		for (QueueItem q : uploadQueue) {
+			q.processed = false;
+		}
+	}
+
+    public PreferenceSetting getPreferenceSetting() {
+        return new SdsPluginPreferences();
+    }
+
+}
+
Index: applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/WritePostprocessor.java
===================================================================
--- applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/WritePostprocessor.java	(revision 28160)
+++ applications/editors/josm/plugins/sds/src/org/openstreetmap/hot/sds/WritePostprocessor.java	(revision 28160)
@@ -0,0 +1,72 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.hot.sds;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+
+import org.openstreetmap.hot.sds.SeparateDataStorePlugin.QueueItem;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmServerWritePostprocessor;
+
+public class WritePostprocessor implements OsmServerWritePostprocessor {
+
+	SeparateDataStorePlugin plugin;
+
+	public WritePostprocessor(SeparateDataStorePlugin plugin) {
+		this.plugin = plugin;
+	}	
+
+	@Override
+	public void postprocessUploadedPrimitives(Collection<IPrimitive> primitives,
+			ProgressMonitor progress) {
+		
+	    StringWriter swriter = new StringWriter();
+	    SdsWriter sdsWriter = new SdsWriter(new PrintWriter(swriter));
+	    sdsWriter.header();
+	    boolean somethingWritten = false;
+	   
+	    for (IPrimitive p : primitives) {
+			for (QueueItem q : plugin.uploadQueue) {
+				if (q.primitive.equals(p) && !q.sdsOnly) {
+					sdsWriter.write(q.primitive, q.tags);
+					somethingWritten = true;
+					q.processed = true;
+					continue;
+				}
+			}
+	    }
+	    
+		for (QueueItem q : plugin.uploadQueue) {
+			if (q.sdsOnly) {
+				sdsWriter.write(q.primitive, q.tags);
+				somethingWritten = true;
+				q.processed = true;
+			}
+		}
+
+		if (somethingWritten) {
+			sdsWriter.footer();
+
+			SdsApi api = SdsApi.getSdsApi();
+			System.out.println("sending message:\n" + swriter.toString());
+			api.updateSds(swriter.toString(), progress);
+		}
+		
+		for (IPrimitive p : primitives) {
+			plugin.learn(p);
+		}
+
+		for (QueueItem q : plugin.uploadQueue) {
+			if (q.sdsOnly) {
+				q.primitive.setModified(false);
+				plugin.learn(q.primitive);
+			}
+		}
+		
+		plugin.clearQueue();
+		// TODO: if exception -> resetQueue
+	}
+
+}
