package de.blau.android.osm; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import org.acra.ACRA; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import android.app.Activity; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import de.blau.android.App; import de.blau.android.R; import de.blau.android.contract.Urls; import de.blau.android.exception.OsmException; import de.blau.android.exception.OsmIOException; import de.blau.android.exception.OsmServerException; import de.blau.android.prefs.API; import de.blau.android.services.util.StreamUtils; import de.blau.android.tasks.Note; import de.blau.android.tasks.NoteComment; import de.blau.android.util.Base64; import de.blau.android.util.DateFormatter; import de.blau.android.util.OAuthHelper; import de.blau.android.util.SavingHelper; import de.blau.android.util.Snack; import oauth.signpost.OAuthConsumer; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; /** * @author mb */ public class Server { private static final String DEBUG_TAG = Server.class.getName(); /** * Timeout for connections in milliseconds. */ private static final int TIMEOUT = 45 * 1000; /** * Location of OSM API */ private final String serverURL; /** * Location of optional read only OSM API */ private final String readonlyURL; /** * Location of optional notes OSM API */ private final String notesURL; /** * username for write-access on the server. */ private final String username; /** * password for write-access on the server. */ private final String password; /** * use oauth */ private boolean oauth; /** * oauth access token */ private final String accesstoken; /** * oauth access token secret */ private final String accesstokensecret; /** * display name of the user and other stuff */ private final UserDetails userDetails; /** * Current capabilities */ private Capabilities capabilities = Capabilities.getDefault(); /** * Current readonly capabilities */ private Capabilities readOnlyCapabilities = Capabilities.getReadOnlyDefault(); /** * <a href="http://wiki.openstreetmap.org/wiki/API">API</a>-Version. */ private static final String version = "0.6"; private final String osmChangeVersion = "0.3"; private long changesetId = -1; private final String generator; private final XmlPullParserFactory xmlParserFactory; private final DiscardedTags discardedTags; /** * Date pattern used for suggesting a file name when uploading GPX tracks. */ private static final String DATE_PATTERN_GPX_TRACK_UPLOAD_SUGGESTED_FILE_NAME_PART = "yyyy-MM-dd'T'HHmmss"; /** * Server path component for "api/" as in "http://api.openstreetmap.org/api/". */ private static final String SERVER_API_PATH = "api/"; /** * Server path component for "changeset/" as in "http://api.openstreetmap.org/api/0.6/changeset/". */ private static final String SERVER_CHANGESET_PATH = "changeset/"; /** * Server path component for "notes/" as in "http://api.openstreetmap.org/api/0.6/notes/". */ private static final String SERVER_NOTES_PATH = "notes/"; /** * Constructor. Sets {@link #rootOpen} and {@link #createdByTag}. * @param apiurl The OSM API URL to use (e.g. "http://api.openstreetmap.org/api/0.6/"). * @param username * @param password * @param oauth * @param generator the name of the editor. */ public Server(Context context, final API api,final String generator) { Log.d(DEBUG_TAG, "constructor"); if (api.url != null && !api.url.equals("")) { this.serverURL = api.url; } else { this.serverURL = Urls.DEFAULT_API_NO_HTTPS; // probably not needed anymore } this.readonlyURL = api.readonlyurl; this.notesURL = api.notesurl; this.password = api.pass; this.username = api.user; this.oauth = api.oauth; this.generator = generator; this.accesstoken = api.accesstoken; this.accesstokensecret = api.accesstokensecret; userDetails = null; Log.d(DEBUG_TAG, "using " + this.username + " with " + this.serverURL); Log.d(DEBUG_TAG, "oAuth: " + this.oauth + " token " + this.accesstoken + " secret " + this.accesstokensecret); XmlPullParserFactory factory = null; try { factory = XmlPullParserFactory.newInstance(); } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Problem creating parser factory", e); } xmlParserFactory = factory; // initialize list of redundant tags discardedTags = new DiscardedTags(context); } /** * display name and message counts is the only thing that is interesting * @author simon * */ public class UserDetails { public String display_name = "unknown"; public int received = 0; public int unread = 0; public int sent = 0; } /** * Get the details for the user. * @return The display name for the user, or null if it couldn't be determined. */ public UserDetails getUserDetails() { UserDetails result = null; if (userDetails == null) { // Haven't retrieved the details from OSM - try to try { HttpURLConnection connection = openConnectionForWriteAccess(getUserDetailsUrl(), "GET"); try { //connection.getOutputStream().close(); GET doesn't have an outputstream checkResponseCode(connection); XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(connection.getInputStream(), null); int eventType; result = new UserDetails(); boolean messages = false; while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG && "user".equals(tagName)) { result.display_name = parser.getAttributeValue(null, "display_name"); Log.d(DEBUG_TAG,"getUserDetails display name " + result.display_name); } if (eventType == XmlPullParser.START_TAG && "messages".equals(tagName)) { messages = true; } if (eventType == XmlPullParser.END_TAG && "messages".equals(tagName)) { messages = false; } if (messages) { if (eventType == XmlPullParser.START_TAG && "received".equals(tagName)) { result.received = Integer.parseInt(parser.getAttributeValue(null, "count")); Log.d(DEBUG_TAG,"getUserDetails received " + result.received); result.unread = Integer.parseInt(parser.getAttributeValue(null, "unread")); Log.d(DEBUG_TAG,"getUserDetails unread " + result.unread); } if (eventType == XmlPullParser.START_TAG && "sent".equals(tagName)) { result.sent = Integer.parseInt(parser.getAttributeValue(null, "count")); Log.d(DEBUG_TAG,"getUserDetails sent " + result.sent); } } } } finally { disconnect(connection); } } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Problem parsing user details", e); } catch (MalformedURLException e) { Log.e(DEBUG_TAG, "Problem retrieving user details", e); } catch (ProtocolException e) { Log.e(DEBUG_TAG, "Problem accessing user details", e); } catch (IOException e) { Log.e(DEBUG_TAG, "Problem accessing user details", e); } catch (NumberFormatException e) { Log.e(DEBUG_TAG, "Problem accessing user details", e); } return result; } return userDetails; // might not make sense } /** * return the username for this server, may be null * @return */ public String getDisplayName() { return username; } /** * @return true if a read only API URL is set */ public boolean hasReadOnly() { return readonlyURL != null && !"".equals(readonlyURL); } public Capabilities getReadOnlyCapabilities() { try { Capabilities result = getCapabilities(getReadOnlyCapabilitiesUrl()); if (result != null) { readOnlyCapabilities = result; } return readOnlyCapabilities; // if retrieving failed return the default } catch (MalformedURLException e) { Log.e(DEBUG_TAG, "Problem with read-only capabilities URL", e); } return null; } public Capabilities getCachedCapabilities() { if (capabilities==null) { return Capabilities.getDefault(); } return capabilities; } /** * Get the capabilities for the current API * Side effect set capabilities field and update limits that are used elsewhere * @return The capabilities for this server, or null if it couldn't be determined. */ public Capabilities getCapabilities() { try { Capabilities result = getCapabilities(getCapabilitiesUrl()); if (result != null) { capabilities = result; capabilities.updateLimits(); } return capabilities; // if retrieving failed return the default } catch (MalformedURLException e) { Log.e(DEBUG_TAG, "Problem with capabilities URL", e); } return null; } /** * Get the capabilities for the supplied URL * @param capabilitiesURL * @return The capabilities for this server, or null if it couldn't be determined. */ private Capabilities getCapabilities(URL capabilitiesURL) { Capabilities result; HttpURLConnection con = null; // try { Log.d(DEBUG_TAG,"getCapabilities using " + capabilitiesURL.toString()); con = (HttpURLConnection) capabilitiesURL.openConnection(); //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("User-Agent", App.userAgent); con.setInstanceFollowRedirects(true); //connection.getOutputStream().close(); GET doesn't have an outputstream checkResponseCode(con); XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(con.getInputStream(), null); int eventType; result = new Capabilities(); // very hackish just keys on tag names and not in which section of the response we are while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { try { String tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG && "version".equals(tagName)) { result.minVersion = parser.getAttributeValue(null, "minimum"); result.maxVersion = parser.getAttributeValue(null, "maximum"); Log.d(DEBUG_TAG,"getCapabilities min/max API version " + result.minVersion + "/" + result.maxVersion); } if (eventType == XmlPullParser.START_TAG && "area".equals(tagName)) { String maxArea = parser.getAttributeValue(null, "maximum"); if (maxArea != null) { result.areaMax = Float.parseFloat(maxArea); } Log.d(DEBUG_TAG,"getCapabilities maximum area " + maxArea); } if (eventType == XmlPullParser.START_TAG && "tracepoints".equals(tagName)) { String perPage = parser.getAttributeValue(null, "per_page"); if (perPage != null) { result.maxTracepointsPerPage = Integer.parseInt(perPage); } Log.d(DEBUG_TAG,"getCapabilities maximum #tracepoints per page " + perPage); } if (eventType == XmlPullParser.START_TAG && "waynodes".equals(tagName)) { String maximumWayNodes = parser.getAttributeValue(null, "maximum"); if (maximumWayNodes != null) { result.maxWayNodes = Integer.parseInt(maximumWayNodes); } Log.d(DEBUG_TAG,"getCapabilities maximum #nodes in a way " + maximumWayNodes); } if (eventType == XmlPullParser.START_TAG && "changesets".equals(tagName)) { String maximumElements = parser.getAttributeValue(null, "maximum_elements"); if (maximumElements != null) { result.maxElementsInChangeset = Integer.parseInt(maximumElements); } Log.d(DEBUG_TAG,"getCapabilities maximum elements in changesets " + maximumElements); } if (eventType == XmlPullParser.START_TAG && "timeout".equals(tagName)) { String seconds = parser.getAttributeValue(null, "seconds"); if (seconds != null) { result.timeout = Integer.parseInt(seconds); } Log.d(DEBUG_TAG,"getCapabilities timeout seconds " + seconds); } if (eventType == XmlPullParser.START_TAG && "status".equals(tagName)) { result.dbStatus = Capabilities.stringToStatus(parser.getAttributeValue(null, "database")); result.apiStatus = Capabilities.stringToStatus(parser.getAttributeValue(null, "api")); result.gpxStatus = Capabilities.stringToStatus(parser.getAttributeValue(null, "gpx")); Log.d(DEBUG_TAG,"getCapabilities service status DB " + result.dbStatus + " API " + result.apiStatus + " GPX " + result.gpxStatus); } if (eventType == XmlPullParser.START_TAG && "blacklist".equals(tagName)) { if (result.imageryBlacklist == null) { result.imageryBlacklist = new ArrayList<String>(); } String regex = parser.getAttributeValue(null, "regex"); if (regex != null) { result.imageryBlacklist.add(regex); } Log.d(DEBUG_TAG,"getCapabilities blacklist regex " + regex); } } catch (NumberFormatException e) { Log.e(DEBUG_TAG, "Problem accessing capabilities", e); } } return result; } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Problem parsing capabilities", e); } catch (ProtocolException e) { Log.e(DEBUG_TAG, "Problem accessing capabilities", e); } catch (IOException e) { Log.e(DEBUG_TAG, "Problem accessing capabilities", e); } finally { disconnect(con); } return null; } public boolean apiAvailable() { return capabilities.apiStatus.equals(Capabilities.Status.ONLINE) || capabilities.apiStatus.equals(Capabilities.Status.READONLY); } public boolean readableDB() { return capabilities.dbStatus.equals(Capabilities.Status.ONLINE) || capabilities.dbStatus.equals(Capabilities.Status.READONLY); } public boolean writableDB() { return capabilities.dbStatus.equals(Capabilities.Status.ONLINE); } public boolean readOnlyApiAvailable() { return readOnlyCapabilities.apiStatus.equals(Capabilities.Status.ONLINE) || readOnlyCapabilities.apiStatus.equals(Capabilities.Status.READONLY); } public boolean readOnlyReadableDB() { return readOnlyCapabilities.dbStatus.equals(Capabilities.Status.ONLINE) || readOnlyCapabilities.dbStatus.equals(Capabilities.Status.READONLY); } /** * Open a connection to an OSM server and request all data in box * * @param context Android context * @param box the specified bounding box * @return the stream * @throws OsmServerException * @throws IOException */ public InputStream getStreamForBox(@Nullable final Context context, final BoundingBox box) throws OsmServerException, IOException { Log.d(DEBUG_TAG, "getStreamForBox"); URL url = new URL(getReadOnlyUrl() + "map?bbox=" + box.toApiString()); HttpURLConnection con = (HttpURLConnection) url.openConnection(); boolean isServerGzipEnabled; Log.d(DEBUG_TAG, "getStreamForBox " + url.toString()); //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); con.setInstanceFollowRedirects(true); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); // retry if we have no response-code if (con.getResponseCode() == -1) { Log.w(getClass().getName()+ ":getStreamForBox", "no valid http response-code, trying again"); con = (HttpURLConnection) url.openConnection(); //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); con.setInstanceFollowRedirects(true); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); } if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { if (context != null && context instanceof Activity) { if (con.getResponseCode() == 400) { ((Activity) context).runOnUiThread(new Runnable() { @Override public void run() { Snack.barError((Activity)context, R.string.toast_download_bbox_failed); } }); } else { ((Activity) context).runOnUiThread(new DownloadErrorToast(context, con.getResponseCode(), con.getResponseMessage())); } } throwOsmServerException(con); } if (isServerGzipEnabled) { return new GZIPInputStream(con.getInputStream()); } else { return con.getInputStream(); } } /** * Get a single element from the API * * @param context Android context * @param mode "full" or null * @param type type (node, way, relation) of the object * @param id the OSM id of the object * @return the stream * @throws OsmServerException * @throws IOException */ public InputStream getStreamForElement(@Nullable final Context context, @Nullable final String mode, @NonNull final String type, final long id) throws OsmServerException, IOException { Log.d(DEBUG_TAG, "getStreamForElement"); URL url = new URL(getReadOnlyUrl() + type + "/" + id + (mode != null ? "/" + mode : "")); HttpURLConnection con = (HttpURLConnection) url.openConnection(); boolean isServerGzipEnabled; Log.d(DEBUG_TAG, "getStreamForElement " + url.toString()); //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); // retry if we have no response-code if (con.getResponseCode() == -1) { Log.w(getClass().getName()+ ":getStreamForElement", "no valid http response-code, trying again"); con = (HttpURLConnection) url.openConnection(); //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); } if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { if (context != null && context instanceof Activity) { if (con.getResponseCode() == 400) { ((Activity) context).runOnUiThread(new Runnable() { @Override public void run() { Snack.barError((Activity)context, R.string.toast_download_failed); } }); } else { ((Activity) context).runOnUiThread(new DownloadErrorToast(context, con.getResponseCode(), con.getResponseMessage())); } } throwOsmServerException(con); } if (isServerGzipEnabled) { return new GZIPInputStream(con.getInputStream()); } else { return con.getInputStream(); } } class DownloadErrorToast implements Runnable { final int code; final String message; final Context context; DownloadErrorToast(Context context, int code, String message) { this.code = code; this.message = message; this.context = context; } @Override public void run() { if (context != null && context instanceof Activity) { try { Snack.barError((Activity) context, context.getResources().getString(R.string.toast_download_failed, code, message)); } catch (Exception ex) { // do nothing ... this is stop bugs in the Android format parsing crashing the app, report the error because it is likely caused by a translation error ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(ex); } } } } /** * Sends an delete-request to the server. * * @param elem the element which should be deleted. * @return true when the server indicates the successful deletion (HTTP 200), otherwise false. * @throws MalformedURLException * @throws ProtocolException * @throws IOException */ public boolean deleteElement(final OsmElement elem) throws MalformedURLException, ProtocolException, IOException { HttpURLConnection connection = null; // elem.addOrUpdateTag(createdByTag, createdByKey); Log.d(DEBUG_TAG,"Deleting " + elem.getName() + " #" + elem.getOsmId()); try { connection = openConnectionForWriteAccess(getDeleteUrl(elem), "POST"); sendPayload(connection, new XmlSerializable() { @Override public void toXml(XmlSerializer serializer, Long changeSetId) throws IllegalArgumentException, IllegalStateException, IOException { final String action = "delete"; startChangeXml(serializer, action); elem.toXml(serializer, changeSetId); endChangeXml(serializer, action); } }, changesetId); checkResponseCode(connection, elem); } finally { disconnect(connection); } return true; } /** * Return true if either login/pass is set or if oAuth is enabled * @return */ public boolean isLoginSet() { return (username != null && (password != null && !username.equals("") && !password.equals(""))) || oauth; } /** * @param connection */ private static void disconnect(final HttpURLConnection connection) { if (connection != null) { connection.disconnect(); } } public long updateElement(final OsmElement elem) throws MalformedURLException, ProtocolException, IOException { long osmVersion = -1; HttpURLConnection connection = null; InputStream in = null; try { URL updateElementUrl = getUpdateUrl(elem); Log.d(DEBUG_TAG,"Updating " + elem.getName() + " #" + elem.getOsmId() + " " + updateElementUrl); connection = openConnectionForWriteAccess(updateElementUrl, "PUT"); // remove redundant tags discardedTags.remove(elem); sendPayload(connection, new XmlSerializable() { @Override public void toXml(XmlSerializer serializer, Long changeSetId) throws IllegalArgumentException, IllegalStateException, IOException { startXml(serializer); elem.toXml(serializer, changeSetId); endXml(serializer); } }, changesetId); checkResponseCode(connection, elem); in = connection.getInputStream(); try { osmVersion = Long.parseLong(readLine(in)); } catch (NumberFormatException e) { throw new OsmServerException(-1,"Server returned illegal element version " + e.getMessage()); } } finally { disconnect(connection); SavingHelper.close(in); } return osmVersion; } private void sendPayload(final HttpURLConnection connection, final XmlSerializable xmlSerializable, long changeSetId) throws OsmIOException { OutputStreamWriter out = null; try { XmlSerializer xmlSerializer = getXmlSerializer(); out = new OutputStreamWriter(connection.getOutputStream(), Charset .defaultCharset()); xmlSerializer.setOutput(out); xmlSerializable.toXml(xmlSerializer, changeSetId); } catch (IOException e) { throw new OsmIOException("Could not send data to server", e); } catch (IllegalArgumentException e) { throw new OsmIOException("Sending illegal format object failed", e); } finally { SavingHelper.close(out); } } /** * @param elem * @param xml * @return * @throws IOException * @throws MalformedURLException * @throws ProtocolException */ private HttpURLConnection openConnectionForWriteAccess(final URL url, final String requestMethod) throws IOException, MalformedURLException, ProtocolException { return openConnectionForWriteAccess(url, requestMethod, "text/xml"); } private HttpURLConnection openConnectionForWriteAccess(final URL url, final String requestMethod, final String contentType) throws IOException, MalformedURLException, ProtocolException { Log.d(DEBUG_TAG, "openConnectionForWriteAccess url " + url); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("Content-Type", "" + contentType + "; charset=utf-8"); connection.setRequestProperty("User-Agent", App.userAgent); connection.setConnectTimeout(TIMEOUT); connection.setReadTimeout(TIMEOUT); connection.setRequestMethod(requestMethod); if (oauth) { OAuthHelper oa = new OAuthHelper(); OAuthConsumer consumer = oa.getConsumer(getBaseUrl(getReadWriteUrl())); consumer.setTokenWithSecret(accesstoken, accesstokensecret); // sign the request try { consumer.sign(connection); // HttpParameters h = consumer.getRequestParameters(); } catch (OAuthMessageSignerException e) { // user will get error when we actually try to write Log.e(DEBUG_TAG, "OAuth fail",e); } catch (OAuthExpectationFailedException e) { Log.e(DEBUG_TAG, "OAuth fail",e); } catch (OAuthCommunicationException e) { Log.e(DEBUG_TAG, "OAuth fail",e); } } else { connection.setRequestProperty("Authorization", "Basic " + Base64.encode(username + ":" + password)); } connection.setDoOutput(!"GET".equals(requestMethod)); connection.setDoInput(true); return connection; } public long createElement(final OsmElement elem) throws MalformedURLException, ProtocolException, IOException { long osmId = -1; HttpURLConnection connection = null; InputStream in = null; // elem.addOrUpdateTag(createdByTag, createdByKey); try { connection = openConnectionForWriteAccess(getCreationUrl(elem), "PUT"); sendPayload(connection, new XmlSerializable() { @Override public void toXml(XmlSerializer serializer, Long changeSetId) throws IllegalArgumentException, IllegalStateException, IOException { startXml(serializer); elem.toXml(serializer, changeSetId); endXml(serializer); } }, changesetId); checkResponseCode(connection); in = connection.getInputStream(); try { osmId = Long.parseLong(readLine(in)); } catch (NumberFormatException e) { throw new OsmServerException(-1,"Server returned illegal element id " + e.getMessage()); } } finally { disconnect(connection); SavingHelper.close(in); } return osmId; } /** * Test if changeset is at least potentially still open. * @return */ public boolean hasOpenChangeset() { return changesetId != -1; } /** * Reset changeset id */ public void resetChangeset() { changesetId = -1; } /** * Open a new changeset. * @param comment Changeset comment. * @param source * @param imagery TODO * @throws MalformedURLException * @throws ProtocolException * @throws IOException */ public void openChangeset(final String comment, final String source, final String imagery) throws MalformedURLException, ProtocolException, IOException { long newChangesetId = -1; HttpURLConnection connection = null; InputStream in = null; if (changesetId != -1) { // potentially still open, check if really the case Changeset cs = getChangeset(changesetId); if (cs != null && cs.open) { Log.d(DEBUG_TAG,"Changeset #" + changesetId + " still open, reusing"); updateChangeset(changesetId, comment, source, imagery); return; } else { changesetId = -1; } } try { XmlSerializable xmlData = changeSetTags(comment, source, imagery); connection = openConnectionForWriteAccess(getCreateChangesetUrl(), "PUT"); sendPayload(connection, xmlData, changesetId); if (connection.getResponseCode() == -1) { //sometimes we get an invalid response-code the first time. disconnect(connection); connection = openConnectionForWriteAccess(getCreateChangesetUrl(), "PUT"); sendPayload(connection, xmlData, changesetId); } checkResponseCode(connection); in = connection.getInputStream(); try { newChangesetId = Long.parseLong(readLine(in)); } catch (NumberFormatException e) { throw new OsmServerException(-1,"Server returned illegal changeset id " + e.getMessage()); } } finally { disconnect(connection); SavingHelper.close(in); } changesetId = newChangesetId; } private XmlSerializable changeSetTags(final String comment, final String source, final String imagery) { return new XmlSerializable() { @Override public void toXml(XmlSerializer serializer, Long changeSetId) throws IllegalArgumentException, IllegalStateException, IOException { startXml(serializer); serializer.startTag("", "changeset"); serializer.startTag("", "tag"); serializer.attribute("", "k", "created_by"); serializer.attribute("", "v", generator); serializer.endTag("", "tag"); if (comment != null && comment.length() > 0) { serializer.startTag("", "tag"); serializer.attribute("", "k", "comment"); serializer.attribute("", "v", comment); serializer.endTag("", "tag"); } if (source != null && source.length() > 0) { serializer.startTag("", "tag"); serializer.attribute("", "k", "source"); serializer.attribute("", "v", source); serializer.endTag("", "tag"); } if (imagery != null && imagery.length() > 0) { serializer.startTag("", "tag"); serializer.attribute("", "k", "imagery_used"); serializer.attribute("", "v", imagery); serializer.endTag("", "tag"); } serializer.startTag("", "tag"); serializer.attribute("", "k", "locale"); serializer.attribute("", "v", Locale.getDefault().toString()); serializer.endTag("", "tag"); serializer.endTag("", "changeset"); endXml(serializer); } }; } /** * Close the current open changeset, will zap the stored id even if the closing fails, * this will force using a new changeset on the next upload * @throws MalformedURLException * @throws ProtocolException * @throws IOException */ public void closeChangeset() throws MalformedURLException, ProtocolException, IOException { HttpURLConnection connection = null; try { connection = openConnectionForWriteAccess(getCloseChangesetUrl(changesetId), "PUT"); checkResponseCode(connection); } finally { disconnect(connection); changesetId = -1; } } /** * Right now just what we need * @author simon * */ public class Changeset { public boolean open = false; } private Changeset getChangeset(long id) { Changeset result = null; HttpURLConnection connection = null; try { connection = openConnectionForWriteAccess(getChangesetUrl(changesetId), "GET"); checkResponseCode(connection); XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(connection.getInputStream(), null); int eventType; result = new Changeset(); while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG && "changeset".equals(tagName)) { result.open = parser.getAttributeValue(null, "open").equals("true"); Log.d(DEBUG_TAG,"Changeset #" + id + " is " + (result.open ? "open":"closed")); } } } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ProtocolException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (XmlPullParserException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { disconnect(connection); } return result; } private void updateChangeset(final long changesetId, final String comment, final String source, final String imagery) throws MalformedURLException, ProtocolException, IOException { HttpURLConnection connection = null; InputStream in = null; try { XmlSerializable xmlData = changeSetTags(comment, source, imagery); connection = openConnectionForWriteAccess(getChangesetUrl(changesetId), "PUT"); sendPayload(connection, xmlData, changesetId); checkResponseCode(connection); // ignore response for now } finally { disconnect(connection); SavingHelper.close(in); } } /** * @param connection * @throws IOException * @throws OsmException */ private void checkResponseCode(final HttpURLConnection connection) throws IOException, OsmException { checkResponseCode(connection, null); } /** * @param connection * @throws IOException * @throws OsmException */ private void checkResponseCode(final HttpURLConnection connection, final OsmElement e) throws IOException, OsmException { int responsecode = -1; if (connection == null ) { throw new OsmServerException(responsecode, "Unknown error"); } responsecode = connection.getResponseCode(); Log.d(DEBUG_TAG, "response code " + responsecode); if (responsecode == -1) throw new IOException("Invalid response from server"); if (responsecode != HttpURLConnection.HTTP_OK) { if (responsecode == HttpURLConnection.HTTP_GONE && e.getState()==OsmElement.STATE_DELETED) { //FIXME we tried to delete an already deleted element: log, but ignore, maybe it would be better to ask user Log.d(DEBUG_TAG, e.getOsmId() + " already deleted on server"); return; } throwOsmServerException(connection, e, responsecode); //TODO: happens the first time on some uploads. responseMessage=ErrorMessage="", works the second time } } /** * Upload edits in OCS format and process the server response * @param delegator reference to the StorageDelegator * @throws MalformedURLException * @throws ProtocolException * @throws IOException */ public void diffUpload(StorageDelegator delegator) throws MalformedURLException, ProtocolException, IOException { HttpURLConnection connection = null; InputStream in = null; try { connection = openConnectionForWriteAccess(getDiffUploadUrl(changesetId), "POST"); for (OsmElement elem : delegator.getApiStorage().getElements()) { if (elem.state != OsmElement.STATE_DELETED) { discardedTags.remove(elem); } } delegator.writeOsmChange(connection.getOutputStream(), changesetId, getCachedCapabilities().maxElementsInChangeset); processDiffUploadResult(delegator, connection, xmlParserFactory.newPullParser()); } catch (IllegalArgumentException e1) { throw new OsmException(e1.getMessage()); } catch (IllegalStateException e1) { throw new OsmException(e1.getMessage()); } catch (XmlPullParserException e1) { throw new OsmException(e1.getMessage()); } finally { disconnect(connection); SavingHelper.close(in); } } /** * These patterns are fairly, to very, unforgiving, hopefully API 0.7 will give the error codes back in a more structured way */ private static final Pattern ERROR_MESSAGE_CLOSED_CHANGESET = Pattern.compile("(?i)The changeset ([0-9]+) was closed at"); private static final Pattern ERROR_MESSAGE_VERSION_CONFLICT = Pattern.compile("(?i)Version mismatch: Provided ([0-9]+), server had: ([0-9]+) of (Node|Way|Relation) ([0-9]+)"); private static final Pattern ERROR_MESSAGE_DELETED = Pattern.compile("(?i)The (node|way|relation) with the id ([0-9]+) has already been deleted"); private static final Pattern ERROR_MESSAGE_PRECONDITION_STILL_USED = Pattern.compile("(?i)Precondition failed: (Node|Way) ([0-9]+) is still used by (way|relation)[s]? ([0-9]+).*"); private static final Pattern ERROR_MESSAGE_PRECONDITION_RELATION_RELATION = Pattern.compile("(?i)Precondition failed: The relation ([0-9]+) is used in relation ([0-9]+)."); /** * Process the results of uploading a diff to the API, here because it needs to manipulate the stored data * @param parser * @param in * @throws IOException */ private void processDiffUploadResult(StorageDelegator delegator, HttpURLConnection connection, XmlPullParser parser) throws IOException { Storage apiStorage = delegator.getApiStorage(); int code = connection.getResponseCode(); if (code == HttpURLConnection.HTTP_OK) { boolean rehash = false; // if ids are changed we need to rehash // storage try { parser.setInput(new BufferedInputStream(connection.getInputStream(), StreamUtils.IO_BUFFER_SIZE), null); int eventType; boolean inResponse = false; while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { String tagName = parser.getName(); if (inResponse) { String oldIdStr = parser.getAttributeValue(null, "old_id"); if (oldIdStr == null) { // must always be present Log.e(DEBUG_TAG, "oldId missing! tag " + tagName); continue; } long oldId = Long.parseLong(oldIdStr); String newIdStr = parser.getAttributeValue(null, "new_id"); String newVersionStr = parser.getAttributeValue(null, "new_version"); if ("node".equals(tagName) || "way".equals(tagName) || "relation".equals(tagName)) { OsmElement e = apiStorage.getOsmElement(tagName, oldId); if (e != null) { if (e.getState() == OsmElement.STATE_DELETED && newIdStr == null && newVersionStr == null) { if (!apiStorage.removeElement(e)) { Log.e(DEBUG_TAG, "Deleted " + e + " was already removed from local storage!"); } Log.w(DEBUG_TAG, e + " deleted in API"); delegator.dirty(); } else if (e.getState() == OsmElement.STATE_CREATED && oldId < 0 && newIdStr != null && newVersionStr != null) { long newId = Long.parseLong(newIdStr); int newVersion = Integer.parseInt(newVersionStr); if (newId > 0) { if (!apiStorage.removeElement(e)) { Log.e(DEBUG_TAG, "New " + e + " was already removed from api storage!"); } Log.w(DEBUG_TAG, "New " + e + " added to API"); e.setOsmId(newId); // id change requires rehash, so that removing works, remove first then set id e.setOsmVersion(newVersion); e.setState(OsmElement.STATE_UNCHANGED); delegator.dirty(); rehash = true; } else { Log.d(DEBUG_TAG, "Didn't get new ID: " + newId); } } else if (e.getState() == OsmElement.STATE_MODIFIED && oldId > 0 && newIdStr != null && newVersionStr != null) { long newId = Long.parseLong(newIdStr); int newVersion = Integer.parseInt(newVersionStr); if (newId == oldId && newVersion > 0) { if (!apiStorage.removeElement(e)) { Log.e(DEBUG_TAG, "Updated " + e + " was already removed from api storage!"); } e.setOsmVersion(newVersion); Log.w(DEBUG_TAG, e + " updated in API"); e.setState(OsmElement.STATE_UNCHANGED); } else { Log.d(DEBUG_TAG, "Didn't get new version: " + newVersion + " for " + newId); } delegator.dirty(); } else { Log.e(DEBUG_TAG, "Unkown start tag in result: " + tagName); } } else { // log crash or what Log.e(DEBUG_TAG, "" + e + " not found in api storage!"); } } } else if (eventType == XmlPullParser.START_TAG && "diffResult".equals(tagName)) { inResponse = true; } else { Log.e(DEBUG_TAG, "Unknown start tag: " + tagName); } } } if (rehash) { delegator.getCurrentStorage().rehash(); if (!apiStorage.isEmpty()) { // shouldn't happen apiStorage.rehash(); } } } catch (XmlPullParserException e) { throw new OsmException(e.toString()); } catch (NumberFormatException e) { throw new OsmException(e.toString()); } catch (IOException e) { throw new OsmException(e.toString()); } } else { String message = Server.readStream(connection.getErrorStream()); String responseMessage = connection.getResponseMessage(); Log.d(DEBUG_TAG, "Error code: " + code + " response: " + responseMessage + " message: " + message); if (code == HttpURLConnection.HTTP_CONFLICT) { // got conflict , possible messages see // http://wiki.openstreetmap.org/wiki/API_v0.6#Diff_upload:_POST_.2Fapi.2F0.6.2Fchangeset.2F.23id.2Fupload Matcher m = ERROR_MESSAGE_VERSION_CONFLICT.matcher(message); if (m.matches()) { String type = m.group(3); String idStr = m.group(4); generateException(apiStorage, type, idStr, code, responseMessage, message); } else { m = ERROR_MESSAGE_CLOSED_CHANGESET.matcher(message); if (m.matches()) { // note this should never happen, since we check // if the changeset is still open before upload throw new OsmServerException(HttpURLConnection.HTTP_BAD_REQUEST, code + "=\"" + responseMessage + "\" ErrorMessage: " + message); } } Log.e(DEBUG_TAG, "Code: " + code + " unknown error message: " + message); throw new OsmServerException(HttpURLConnection.HTTP_BAD_REQUEST, "Original error " + code + "=\"" + responseMessage + "\" ErrorMessage: " + message); } else if (code == HttpURLConnection.HTTP_GONE) { Matcher m = ERROR_MESSAGE_DELETED.matcher(message); if (m.matches()) { String type = m.group(1); String idStr = m.group(2); generateException(apiStorage, type, idStr, code, responseMessage, message); } } else if (code == HttpURLConnection.HTTP_PRECON_FAILED) { // Besides the messages parsed here, theoretically the following two messages could be returned: // Way #{id} requires the nodes with id in (#{missing_ids}), which either do not exist, or are not visible. // and // Relation with id #{id} cannot be saved due to #{element} with id #{element.id} // however it shouldn't be possible to create such situations with vespucci Matcher m = ERROR_MESSAGE_PRECONDITION_STILL_USED.matcher(message); if (m.matches()) { String type = m.group(1); String idStr = m.group(2); generateException(apiStorage, type, idStr, code, responseMessage, message); } else { m = ERROR_MESSAGE_PRECONDITION_RELATION_RELATION.matcher(message); if (m.matches()) { String idStr = m.group(1); generateException(apiStorage, "relation", idStr, code, responseMessage, message); } Log.e(DEBUG_TAG, "Unknown error message: " + message); } } throw new OsmServerException(code, message); } } private void generateException(Storage apiStorage, String type, String idStr, int code, String responseMessage, String message) throws OsmServerException { if (type != null && idStr != null) { long osmId = Long.parseLong(idStr); OsmElement e = apiStorage.getOsmElement(type.toLowerCase(Locale.US), osmId); if (e!=null) { throw new OsmServerException(code, e.getName(), e.getOsmId(), code + "=\"" + responseMessage + "\" ErrorMessage: " + message); } } Log.e(DEBUG_TAG, "Error message matched, but parsing failed: " + message); } private static String readStream(final InputStream in) { String res = ""; if (in != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(in), 8000); String line; try { while ((line = reader.readLine()) != null) { res += line; } } catch (IOException e) { Log.e(Server.class.getName() + ":readStream()", "Error in read-operation", e); } } return res; } private static String readLine(final InputStream in) { //TODO: Optimize? -> no Reader BufferedReader reader = new BufferedReader(new InputStreamReader(in), 9); String res = null; try { res = reader.readLine(); } catch (IOException e) { Log.e(DEBUG_TAG, "Problem reading", e); } return res; } private void startXml(XmlSerializer xmlSerializer) throws IllegalArgumentException, IllegalStateException, IOException { xmlSerializer.startDocument("UTF-8", null); xmlSerializer.startTag("", "osm"); xmlSerializer.attribute("", "version", version); xmlSerializer.attribute("", "generator", generator); } private void endXml(XmlSerializer xmlSerializer) throws IllegalArgumentException, IllegalStateException, IOException { xmlSerializer.endTag("", "osm"); xmlSerializer.endDocument(); } private void startChangeXml(XmlSerializer xmlSerializer, String action) throws IllegalArgumentException, IllegalStateException, IOException { xmlSerializer.startDocument("UTF-8", null); xmlSerializer.startTag("", "osmChange"); xmlSerializer.attribute("", "version", osmChangeVersion); xmlSerializer.attribute("", "generator", generator); xmlSerializer.startTag("", action); xmlSerializer.attribute("", "version", osmChangeVersion); xmlSerializer.attribute("", "generator", generator); } private void endChangeXml(XmlSerializer xmlSerializer, String action) throws IllegalArgumentException, IllegalStateException, IOException { xmlSerializer.endTag("", action); xmlSerializer.endTag("", "osmChange"); xmlSerializer.endDocument(); } private XmlSerializer getXmlSerializer() { try { XmlSerializer serializer = xmlParserFactory.newSerializer(); serializer.setPrefix("", ""); return serializer; } catch (IllegalArgumentException e) { Log.e(DEBUG_TAG, "Problem getting serializer", e); } catch (IllegalStateException e) { Log.e(DEBUG_TAG, "Problem getting serializer", e); } catch (IOException e) { Log.e(DEBUG_TAG, "Problem getting serializer", e); } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Problem getting serializer", e); } return null; } private URL getCreationUrl(final OsmElement elem) throws MalformedURLException { return new URL(getReadWriteUrl() + elem.getName() + "/create"); } private URL getCreateChangesetUrl() throws MalformedURLException { return new URL(getReadWriteUrl() + SERVER_CHANGESET_PATH + "create"); } private URL getCloseChangesetUrl(long changesetId) throws MalformedURLException { return new URL(getReadWriteUrl() + SERVER_CHANGESET_PATH + changesetId + "/close"); } private URL getChangesetUrl(long changesetId) throws MalformedURLException { return new URL(getReadWriteUrl() + SERVER_CHANGESET_PATH + changesetId); } private URL getUpdateUrl(final OsmElement elem) throws MalformedURLException { return new URL(getReadWriteUrl() + elem.getName() + "/" + elem.getOsmId()); } private URL getDeleteUrl(final OsmElement elem) throws MalformedURLException { //return getUpdateUrl(elem); return new URL(getReadWriteUrl() + SERVER_CHANGESET_PATH + changesetId + "/upload"); } private URL getDiffUploadUrl(long changeSetId) throws MalformedURLException { return new URL(getReadWriteUrl() + SERVER_CHANGESET_PATH + changeSetId + "/upload"); } private URL getUserDetailsUrl() throws MalformedURLException { return new URL(getReadWriteUrl() + "user/details"); } private URL getAddCommentUrl(@NonNull String noteId, @NonNull String comment) throws MalformedURLException { return new URL(getNotesUrl() + SERVER_NOTES_PATH + noteId + "/comment?text=" + comment); } private URL getNoteUrl(@NonNull String noteId) throws MalformedURLException { return new URL(getNotesReadOnlyUrl() + SERVER_NOTES_PATH + noteId); } /** * Return for now general read write API url as a string * @return */ private String getNotesUrl() { return serverURL; } /** * Return either the general read write API url as a string or a specific to notes one * @return */ private String getNotesReadOnlyUrl() { if (notesURL == null || "".equals(notesURL)) { return serverURL; } else { return notesURL; } } private URL getNotesForBox(long limit, @NonNull BoundingBox area) throws MalformedURLException { return new URL(getNotesReadOnlyUrl() + "notes?" + "limit=" + limit + "&" + "bbox=" + area.getLeft() / 1E7d + "," + area.getBottom() / 1E7d + "," + area.getRight() / 1E7d + "," + area.getTop() / 1E7d); } private URL getAddNoteUrl(double latitude, double longitude, @NonNull String comment) throws MalformedURLException { return new URL(getNotesUrl() + "notes?lat=" + latitude + "&lon=" + longitude + "&text=" + comment); } private URL getCloseNoteUrl(@NonNull String noteId) throws MalformedURLException { return new URL(getNotesUrl() + SERVER_NOTES_PATH + noteId + "/close"); } private URL getReopenNoteUrl(@NonNull String noteId) throws MalformedURLException { return new URL(getNotesUrl() + SERVER_NOTES_PATH + noteId + "/reopen"); } private URL getUploadTrackUrl() throws MalformedURLException { return new URL(getReadWriteUrl() + "gpx/create"); } private URL getCapabilitiesUrl() throws MalformedURLException { return getCapabilitiesUrl(getReadOnlyUrl()); } private URL getReadOnlyCapabilitiesUrl() throws MalformedURLException { return getCapabilitiesUrl(getReadWriteUrl()); } private URL getCapabilitiesUrl(String url) throws MalformedURLException { // need to strip version from serverURL int apiPos = url.indexOf(SERVER_API_PATH); if (apiPos > 0) { String noVersionURL = getReadWriteUrl().substring(0, apiPos) + SERVER_API_PATH; return new URL(noVersionURL + "capabilities"); } throw new MalformedURLException("Invalid API URL: " + getReadWriteUrl()); } /** * @return the read/write URL */ public String getReadWriteUrl() { return serverURL; } /** * Return the url as a string for a read only API if it exists otherwise the result is the same as for read/write * @return */ private String getReadOnlyUrl() { if (readonlyURL == null || "".equals(readonlyURL)) { return serverURL; } else { return readonlyURL; } } /** * @return the base URL, i.e. the url with the "/api/version/"-part stripped */ public static String getBaseUrl(String url) { return url.replaceAll("/api/[0-9]+(?:\\.[0-9]+)+/?$", "/"); } /** * @return the URL the OSM website, FIXME for now hardwired and a bit broken */ public String getWebsiteBaseUrl() { return getBaseUrl(getReadWriteUrl()).replace("api.", ""); } /* New Notes API * code mostly from old OSB implementation * the relevant API documentation is still in flux so this implementation may have issues */ /** * Perform an HTTP request to download up to limit bugs inside the specified area. * Blocks until the request is complete. * @param area Latitude/longitude *1E7 of area to download. * @return All the bugs in the given area. */ public Collection<Note> getNotesForBox(BoundingBox area, long limit) { Collection<Note> result = new ArrayList<Note>(); // http://openstreetbugs.schokokeks.org/api/0.1/getGPX?b=48&t=49&l=11&r=12&limit=100 try { Log.d(DEBUG_TAG, "getNotesForBox"); URL url = getNotesForBox(limit, area); HttpURLConnection con = (HttpURLConnection) url.openConnection(); boolean isServerGzipEnabled; //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { return new ArrayList<Note>(); //TODO Return empty list ... this is better than throwing an uncatched exception, but we should provide some user feedback // throw new UnexpectedRequestException(con); } InputStream is; if (isServerGzipEnabled) { is = new GZIPInputStream(con.getInputStream()); } else { is = con.getInputStream(); } XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(new BufferedInputStream(is, StreamUtils.IO_BUFFER_SIZE), null); int eventType; while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG && "note".equals(tagName)) { try { result.add(new Note(parser)); } catch (IOException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } catch (XmlPullParserException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } catch (NumberFormatException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } } } } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); return new ArrayList<Note>(); // empty list } catch (IOException e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); return new ArrayList<Note>(); // empty list } catch (OutOfMemoryError e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); // TODO ask the user to exit return new ArrayList<Note>(); // empty list } Log.d(DEBUG_TAG, "Read " + result.size() + " notes from input"); return result; } /** * Retrieve a single note * @param id * @return */ public Note getNote(long id) { Note result = null; // http://openstreetbugs.schokokeks.org/api/0.1/getGPX?b=48&t=49&l=11&r=12&limit=100 try { Log.d(DEBUG_TAG, "getNote"); URL url = getNoteUrl(Long.toString(id)); HttpURLConnection con = (HttpURLConnection) url.openConnection(); boolean isServerGzipEnabled; //--Start: header not yet send con.setReadTimeout(TIMEOUT); con.setConnectTimeout(TIMEOUT); con.setRequestProperty("Accept-Encoding", "gzip"); con.setRequestProperty("User-Agent", App.userAgent); //--Start: got response header isServerGzipEnabled = "gzip".equals(con.getHeaderField("Content-encoding")); if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { return null; //TODO Return empty list ... this is better than throwing an uncatched exception, but we should provide some user feedback // throw new UnexpectedRequestException(con); } InputStream is; if (isServerGzipEnabled) { is = new GZIPInputStream(con.getInputStream()); } else { is = con.getInputStream(); } XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(new BufferedInputStream(is, StreamUtils.IO_BUFFER_SIZE), null); int eventType; while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (eventType == XmlPullParser.START_TAG && "note".equals(tagName)) { try { result = new Note(parser); } catch (IOException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } catch (XmlPullParserException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } catch (NumberFormatException e) { // if the bug doesn't parse correctly, there's nothing // we can do about it - move on Log.e(DEBUG_TAG, "Problem parsing bug", e); } } } } catch (XmlPullParserException e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); return null; // empty list } catch (IOException e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); return null; // empty list } catch (OutOfMemoryError e) { Log.e(DEBUG_TAG, "Server.getNotesForBox:Exception", e); // TODO ask the user to exit return null; // empty list } return result; } //TODO rewrite to XML encoding (if supported) /** * Perform an HTTP request to add the specified comment to the specified bug. * Blocks until the request is complete. * @param bug The bug to add the comment to. * @param comment The comment to add to the bug. * @return true if the comment was successfully added. * @throws IOException * @throws OsmServerException * @throws XmlPullParserException */ public void addComment(Note bug, NoteComment comment) throws OsmServerException, IOException, XmlPullParserException { if (!bug.isNew()) { Log.d(DEBUG_TAG, "adding note comment" + bug.getId()); // http://openstreetbugs.schokokeks.org/api/0.1/editPOIexec?id=<Bug ID>&text=<Comment with author and date> HttpURLConnection connection = null; try { // setting text/xml here is a hack to stop signpost (the oAuth library) from trying to sign the body which will fail String encodedComment = URLEncoder.encode(comment.getText(), "UTF-8"); URL addCommentUrl = getAddCommentUrl(Long.toString(bug.getId()), encodedComment); connection = openConnectionForWriteAccess(addCommentUrl, "POST", "text/url"); OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), Charset .defaultCharset()); // out.write("text="+URLEncoder.encode(comment.getText(), "UTF-8")+ "\r\n"); out.flush(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throwOsmServerException(connection); } parseBug(bug, connection.getInputStream()); } finally { disconnect(connection); } } } //TODO rewrite to XML encoding /** * Perform an HTTP request to add the specified bug to the OpenStreetBugs database. * Blocks until the request is complete. * @param bug The bug to add. * @param comment The first comment for the bug. * @return true if the bug was successfully added. * @throws IOException * @throws OsmServerException * @throws XmlPullParserException */ public void addNote(Note bug, NoteComment comment) throws XmlPullParserException, OsmServerException, IOException { if (bug.isNew()) { Log.d(DEBUG_TAG, "adding note"); // http://openstreetbugs.schokokeks.org/api/0.1/addPOIexec?lat=<Latitude>&lon=<Longitude>&text=<Bug description with author and date>&format=<Output format> HttpURLConnection connection = null; try { // setting text/xml here is a hack to stop signpost (the oAuth library) from trying to sign the body which will fail String encodedComment = URLEncoder.encode(comment.getText(), "UTF-8"); URL addNoteUrl = getAddNoteUrl((bug.getLat() / 1E7d), (bug.getLon() / 1E7d), encodedComment); connection = openConnectionForWriteAccess(addNoteUrl, "POST", "text/xml"); OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), Charset .defaultCharset()); // out.write("text="+URLEncoder.encode(comment.getText(), "UTF-8") + "\r\n"); out.flush(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throwOsmServerException(connection); } parseBug(bug, connection.getInputStream()); } finally { disconnect(connection); } } } //TODO rewrite to XML encoding /** * Perform an HTTP request to close the specified bug. * Blocks until the request is complete. * @param bug The bug to close. * @return true if the bug was successfully closed. * @throws IOException * @throws OsmServerException * @throws XmlPullParserException */ public void closeNote(Note bug) throws OsmServerException, IOException, XmlPullParserException { if (!bug.isNew()) { Log.d(DEBUG_TAG, "closing note " + bug.getId()); HttpURLConnection connection = null; try { // setting text/xml here is a hack to stop signpost (the oAuth library) from trying to sign the body which will fail URL closeNoteUrl = getCloseNoteUrl(Long.toString(bug.getId())); connection = openConnectionForWriteAccess(closeNoteUrl, "POST", "text/xml"); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throwOsmServerException(connection); } parseBug(bug, connection.getInputStream()); } finally { disconnect(connection); } } } /** * Perform an HTTP request to reopen the specified bug. * Blocks until the request is complete. * @param bug The bug to close. * @return true if the bug was successfully closed. * @throws IOException * @throws OsmServerException * @throws XmlPullParserException */ public void reopenNote(Note bug) throws OsmServerException, IOException, XmlPullParserException { if (!bug.isNew()) { Log.d(DEBUG_TAG, "reopen note " + bug.getId()); HttpURLConnection connection = null; try { URL reopenNoteUrl = getReopenNoteUrl(Long.toString(bug.getId())); connection = openConnectionForWriteAccess(reopenNoteUrl, "POST", "text/xml"); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throwOsmServerException(connection); } parseBug(bug, connection.getInputStream()); } finally { disconnect(connection); } } } /** * Parse a single OSm note (bug) from an InputStream * @param bug bug to parse in to * @param inputStream the input * @throws IOException * @throws XmlPullParserException */ private void parseBug(@NonNull Note bug, @NonNull InputStream inputStream) throws IOException, XmlPullParserException { XmlPullParser parser = xmlParserFactory.newPullParser(); parser.setInput(new BufferedInputStream(inputStream, StreamUtils.IO_BUFFER_SIZE), null); bug.parseBug(parser); // replace contents with result from server App.getTaskStorage().setDirty(); } /** * GPS track API visibility/ */ public enum Visibility { PRIVATE, PUBLIC, TRACKABLE, IDENTIFIABLE } /** * Upload a GPS track in GPX format * @param track the track * @param description optional description * @param tags optional tags * @param visibility privacy/visibility setting * @throws MalformedURLException * @throws ProtocolException * @throws IOException * @throws IllegalArgumentException * @throws IllegalStateException * @throws XmlPullParserException */ public void uploadTrack(Track track, String description, String tags, Visibility visibility) throws MalformedURLException, ProtocolException, IOException, IllegalArgumentException, IllegalStateException, XmlPullParserException { HttpURLConnection connection = null; try { // String boundary="*VESPUCCI*"; String separator="--"+boundary+"\r\n"; connection = openConnectionForWriteAccess(getUploadTrackUrl(), "POST", "multipart/form-data;boundary="+boundary); OutputStream os = connection.getOutputStream(); OutputStreamWriter out = new OutputStreamWriter(os, Charset .defaultCharset()); out.write(separator); out.write("Content-Disposition: form-data; name=\"description\"\r\n\r\n"); out.write(description + "\r\n"); out.write(separator); out.write("Content-Disposition: form-data; name=\"tags\"\r\n\r\n"); out.write(tags + "\r\n"); out.write(separator); out.write("Content-Disposition: form-data; name=\"visibility\"\r\n\r\n"); out.write(visibility.name().toLowerCase(Locale.US) + "\r\n"); out.write(separator); String fileNamePart = DateFormatter.getFormattedString(DATE_PATTERN_GPX_TRACK_UPLOAD_SUGGESTED_FILE_NAME_PART); out.write("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileNamePart + ".gpx\"\r\n"); out.write("Content-Type: application/gpx+xml\r\n\r\n"); out.flush(); track.exportToGPX(os); os.flush(); out.write("\r\n"); out.write("--"+boundary+"--\r\n"); out.flush(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throwOsmServerException(connection); } } finally { disconnect(connection); } } /** * * @return true if we are using OAuth but have not retrieved the accesstoken yet */ public boolean needOAuthHandshake() { return oauth && ((accesstoken == null) || (accesstokensecret == null)) ; } /** * Override the oauth flag from the API configuration, only needed if inconsistent config * @param t */ public void setOAuth(boolean t) { oauth = t; } /** * * @return true if oauth is enabled */ public boolean getOAuth() { return oauth; } /** * Construct and throw an OsmServerException from the connection to the server * @param connection connection to server * @throws IOException * @throws OsmServerException */ private void throwOsmServerException(final HttpURLConnection connection) throws IOException, OsmServerException { throwOsmServerException(connection, null, connection.getResponseCode()); } /** * Construct and throw an OsmServerException from the connection to the server * @param connection connection connection to server * @param e the OSM element that the error was caused by * @param responsecode code returen from server * @throws IOException * @throws OsmServerException */ private void throwOsmServerException(final HttpURLConnection connection, final OsmElement e, int responsecode) throws IOException, OsmServerException { String responseMessage = connection.getResponseMessage(); if (responseMessage == null) { responseMessage = ""; } InputStream in = connection.getErrorStream(); if (e == null) { Log.d(DEBUG_TAG, "response message " + responseMessage); throw new OsmServerException(responsecode, readStream(in)); } else { throw new OsmServerException(responsecode, e.getName(), e.getOsmId(), readStream(in)); } } }