diff options
author | Samuel Tardieu <sam@rfc1149.net> | 2013-04-07 13:35:30 +0200 |
---|---|---|
committer | Samuel Tardieu <sam@rfc1149.net> | 2013-04-07 13:46:01 +0200 |
commit | 29b9c9bcb06cb19f39ead0184b05ae8c96eda9cb (patch) | |
tree | 97fc1156426c0b623ff0a53de0c4fda8a7c89473 | |
parent | c1b68d1b4b5d73afe60dcbbe03e9d3fd12f428b0 (diff) | |
parent | 37c90405dff48b5996fc8103e071fcd9c54f7a21 (diff) | |
download | cgeo-29b9c9bcb06cb19f39ead0184b05ae8c96eda9cb.zip cgeo-29b9c9bcb06cb19f39ead0184b05ae8c96eda9cb.tar.gz cgeo-29b9c9bcb06cb19f39ead0184b05ae8c96eda9cb.tar.bz2 |
Merge branch 'release' into upstream
Conflicts:
main/src/cgeo/geocaching/export/GpxExport.java
-rw-r--r-- | main/src/cgeo/geocaching/export/GpxExport.java | 6 | ||||
-rw-r--r-- | main/src/cgeo/org/kxml2/io/KXmlSerializer.java | 605 | ||||
-rw-r--r-- | tests/src/cgeo/geocaching/export/ExportTest.java | 52 |
3 files changed, 660 insertions, 3 deletions
diff --git a/main/src/cgeo/geocaching/export/GpxExport.java b/main/src/cgeo/geocaching/export/GpxExport.java index c6f72fd..3e58276 100644 --- a/main/src/cgeo/geocaching/export/GpxExport.java +++ b/main/src/cgeo/geocaching/export/GpxExport.java @@ -15,6 +15,7 @@ import cgeo.geocaching.utils.AsyncTaskWithProgress; import cgeo.geocaching.utils.BaseUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.XmlUtils; +import cgeo.org.kxml2.io.KXmlSerializer; import org.apache.commons.lang3.StringUtils; import org.xmlpull.v1.XmlSerializer; @@ -26,7 +27,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Environment; -import android.util.Xml; import android.view.ContextThemeWrapper; import android.view.View; import android.widget.CheckBox; @@ -114,7 +114,7 @@ class GpxExport extends AbstractExport { return allGeocodes.toArray(new String[allGeocodes.size()]); } - private class ExportTask extends AsyncTaskWithProgress<String, File> { + protected class ExportTask extends AsyncTaskWithProgress<String, File> { private final Activity activity; private int countExported = 0; @@ -147,7 +147,7 @@ class GpxExport extends AbstractExport { final File exportLocation = new File(Settings.getGpxExportDir()); exportLocation.mkdirs(); - final XmlSerializer gpx = Xml.newSerializer(); + final XmlSerializer gpx = new KXmlSerializer(); writer = new FileWriter(exportFile); gpx.setOutput(writer); diff --git a/main/src/cgeo/org/kxml2/io/KXmlSerializer.java b/main/src/cgeo/org/kxml2/io/KXmlSerializer.java new file mode 100644 index 0000000..027ff53 --- /dev/null +++ b/main/src/cgeo/org/kxml2/io/KXmlSerializer.java @@ -0,0 +1,605 @@ +/* + * Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package cgeo.org.kxml2.io; + +import org.apache.commons.lang3.StringUtils; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Locale; + +public class KXmlSerializer implements XmlSerializer { + + // static final String UNDEFINED = ":"; + + // BEGIN android-added + /** size (in characters) for the write buffer */ + private static final int WRITE_BUFFER_SIZE = 500; + // END android-added + + // BEGIN android-changed + // (Guarantee that the writer is always buffered.) + private BufferedWriter writer; + // END android-changed + + private boolean pending; + private int auto; + private int depth; + + private String[] elementStack = new String[12]; + //nsp/prefix/name + private int[] nspCounts = new int[4]; + private String[] nspStack = new String[8]; + //prefix/nsp; both empty are "" + private boolean[] indent = new boolean[4]; + private boolean unicode; + private String encoding; + + private final void check(boolean close) throws IOException { + if (!pending) { + return; + } + + depth++; + pending = false; + + if (indent.length <= depth) { + boolean[] hlp = new boolean[depth + 4]; + System.arraycopy(indent, 0, hlp, 0, depth); + indent = hlp; + } + indent[depth] = indent[depth - 1]; + + for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++) { + writer.write(' '); + writer.write("xmlns"); + if (!StringUtils.isEmpty(nspStack[i * 2])) { + writer.write(':'); + writer.write(nspStack[i * 2]); + } + else if (StringUtils.isEmpty(getNamespace()) && !StringUtils.isEmpty(nspStack[i * 2 + 1])) { + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + } + writer.write("=\""); + writeEscaped(nspStack[i * 2 + 1], '"'); + writer.write('"'); + } + + if (nspCounts.length <= depth + 1) { + int[] hlp = new int[depth + 8]; + System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); + nspCounts = hlp; + } + + nspCounts[depth + 1] = nspCounts[depth]; + // nspCounts[depth + 2] = nspCounts[depth]; + + writer.write(close ? " />" : ">"); + } + + private final void writeEscaped(String s, int quot) throws IOException { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\n': + case '\r': + case '\t': + if (quot == -1) { + writer.write(c); + } else { + writer.write("&#"+((int) c)+';'); + } + break; + case '&': + writer.write("&"); + break; + case '>': + writer.write(">"); + break; + case '<': + writer.write("<"); + break; + default: + if (c == quot) { + writer.write(c == '"' ? """ : "'"); + break; + } + // BEGIN android-changed: refuse to output invalid characters + // See http://www.w3.org/TR/REC-xml/#charsets for definition. + // Corrected for c:geo to handle utf-16 codepoint surrogates correctly + // See http://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B10000_to_U.2B10FFFF + // Note: tab, newline, and carriage return have already been + // handled above. + // Check for lead surrogate + if (c >= 0xd800 && c <= 0xdbff) { + + if (i + 1 < s.length()) { + writer.write(s.substring(i, i + 1)); + i++; + break; + } + // if the lead surrogate is at the string end, it's not valid utf-16 + reportInvalidCharacter(c); + } + boolean valid = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); + if (!valid) { + reportInvalidCharacter(c); + } + if (unicode || c < 127) { + writer.write(c); + } else { + writer.write("&#" + ((int) c) + ";"); + } + // END android-changed + } + } + } + + // BEGIN android-added + private static void reportInvalidCharacter(char ch) { + throw new IllegalArgumentException("Illegal character (" + Integer.toHexString((int) ch) + ")"); + } + // END android-added + + /* + * private final void writeIndent() throws IOException { + * writer.write("\r\n"); + * for (int i = 0; i < depth; i++) + * writer.write(' '); + * } + */ + + public void docdecl(String dd) throws IOException { + writer.write("<!DOCTYPE"); + writer.write(dd); + writer.write(">"); + } + + public void endDocument() throws IOException { + while (depth > 0) { + endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); + } + flush(); + } + + public void entityRef(String name) throws IOException { + check(false); + writer.write('&'); + writer.write(name); + writer.write(';'); + } + + public boolean getFeature(String name) { + //return false; + return ("http://xmlpull.org/v1/doc/features.html#indent-output" + .equals( + name)) + ? indent[depth] + : false; + } + + public String getPrefix(String namespace, boolean create) { + try { + return getPrefix(namespace, false, create); + } catch (IOException e) { + throw new RuntimeException(e.toString()); + } + } + + private final String getPrefix( + String namespace, + boolean includeDefault, + boolean create) + throws IOException { + + for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0; i -= 2) { + if (nspStack[i + 1].equals(namespace) + && (includeDefault || !StringUtils.isEmpty(nspStack[i]))) { + String cand = nspStack[i]; + for (int j = i + 2; j < nspCounts[depth + 1] * 2; j++) { + if (nspStack[j].equals(cand)) { + cand = null; + break; + } + } + if (cand != null) { + return cand; + } + } + } + + if (!create) { + return null; + } + + String prefix; + + if (StringUtils.isEmpty(namespace)) { + prefix = ""; + } else { + do { + prefix = "n" + (auto++); + for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0; i -= 2) { + if (prefix.equals(nspStack[i])) { + prefix = null; + break; + } + } + } while (prefix == null); + } + + boolean p = pending; + pending = false; + setPrefix(prefix, namespace); + pending = p; + return prefix; + } + + public Object getProperty(String name) { + throw new RuntimeException("Unsupported property"); + } + + public void ignorableWhitespace(String s) + throws IOException { + text(s); + } + + public void setFeature(String name, boolean value) { + if ("http://xmlpull.org/v1/doc/features.html#indent-output" + .equals(name)) { + indent[depth] = value; + } else { + throw new RuntimeException("Unsupported Feature"); + } + } + + public void setProperty(String name, Object value) { + throw new RuntimeException( + "Unsupported Property:" + value); + } + + public void setPrefix(String prefix, String namespace) + throws IOException { + + check(false); + if (prefix == null) { + prefix = ""; + } + if (namespace == null) { + namespace = ""; + } + + String defined = getPrefix(namespace, true, false); + + // boil out if already defined + + if (prefix.equals(defined)) { + return; + } + + int pos = (nspCounts[depth + 1]++) << 1; + + if (nspStack.length < pos + 1) { + String[] hlp = new String[nspStack.length + 16]; + System.arraycopy(nspStack, 0, hlp, 0, pos); + nspStack = hlp; + } + + nspStack[pos++] = prefix; + nspStack[pos] = namespace; + } + + public void setOutput(Writer writer) { + // BEGIN android-changed + // Guarantee that the writer is always buffered. + if (writer instanceof BufferedWriter) { + this.writer = (BufferedWriter) writer; + } else { + this.writer = new BufferedWriter(writer, WRITE_BUFFER_SIZE); + } + // END android-changed + + // elementStack = new String[12]; //nsp/prefix/name + //nspCounts = new int[4]; + //nspStack = new String[8]; //prefix/nsp + //indent = new boolean[4]; + + nspCounts[0] = 2; + nspCounts[1] = 2; + nspStack[0] = ""; + nspStack[1] = ""; + nspStack[2] = "xml"; + nspStack[3] = "http://www.w3.org/XML/1998/namespace"; + pending = false; + auto = 0; + depth = 0; + + unicode = false; + } + + public void setOutput(OutputStream os, String encoding) + throws IOException { + if (os == null) { + throw new IllegalArgumentException("os == null"); + } + setOutput(encoding == null + ? new OutputStreamWriter(os) + : new OutputStreamWriter(os, encoding)); + this.encoding = encoding; + if (encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")) { + unicode = true; + } + } + + public void startDocument(String encoding, Boolean standalone) throws IOException { + writer.write("<?xml version='1.0' "); + + if (encoding != null) { + this.encoding = encoding; + if (encoding.toLowerCase(Locale.US).startsWith("utf")) { + unicode = true; + } + } + + if (this.encoding != null) { + writer.write("encoding='"); + writer.write(this.encoding); + writer.write("' "); + } + + if (standalone != null) { + writer.write("standalone='"); + writer.write( + standalone.booleanValue() ? "yes" : "no"); + writer.write("' "); + } + writer.write("?>"); + } + + public XmlSerializer startTag(String namespace, String name) + throws IOException { + check(false); + + // if (namespace == null) + // namespace = ""; + + if (indent[depth]) { + writer.write("\r\n"); + for (int i = 0; i < depth; i++) { + writer.write(" "); + } + } + + int esp = depth * 3; + + if (elementStack.length < esp + 3) { + String[] hlp = new String[elementStack.length + 12]; + System.arraycopy(elementStack, 0, hlp, 0, esp); + elementStack = hlp; + } + + String prefix = + namespace == null + ? "" + : getPrefix(namespace, true, true); + + if (namespace != null && StringUtils.isEmpty(namespace)) { + for (int i = nspCounts[depth]; i < nspCounts[depth + 1]; i++) { + if (StringUtils.isEmpty(nspStack[i * 2]) && !StringUtils.isEmpty(nspStack[i * 2 + 1])) { + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + } + } + } + + elementStack[esp++] = namespace; + elementStack[esp++] = prefix; + elementStack[esp] = name; + + writer.write('<'); + if (!StringUtils.isEmpty(prefix)) { + writer.write(prefix); + writer.write(':'); + } + + writer.write(name); + + pending = true; + + return this; + } + + public XmlSerializer attribute( + String namespace, + String name, + String value) + throws IOException { + if (!pending) { + throw new IllegalStateException("illegal position for attribute"); + } + + // int cnt = nspCounts[depth]; + + if (namespace == null) { + namespace = ""; + } + + // depth--; + // pending = false; + + String prefix = + StringUtils.isEmpty(namespace) + ? "" + : getPrefix(namespace, false, true); + + // pending = true; + // depth++; + + /* + * if (cnt != nspCounts[depth]) { + * writer.write(' '); + * writer.write("xmlns"); + * if (nspStack[cnt * 2] != null) { + * writer.write(':'); + * writer.write(nspStack[cnt * 2]); + * } + * writer.write("=\""); + * writeEscaped(nspStack[cnt * 2 + 1], '"'); + * writer.write('"'); + * } + */ + + writer.write(' '); + if (!StringUtils.isEmpty(prefix)) { + writer.write(prefix); + writer.write(':'); + } + writer.write(name); + writer.write('='); + char q = value.indexOf('"') == -1 ? '"' : '\''; + writer.write(q); + writeEscaped(value, q); + writer.write(q); + + return this; + } + + public void flush() throws IOException { + check(false); + writer.flush(); + } + + /* + * public void close() throws IOException { + * check(); + * writer.close(); + * } + */ + public XmlSerializer endTag(String namespace, String name) + throws IOException { + + if (!pending) + { + depth--; + // if (namespace == null) + // namespace = ""; + } + + if ((namespace == null + && elementStack[depth * 3] != null) + || (namespace != null + && !namespace.equals(elementStack[depth * 3])) + || !elementStack[depth * 3 + 2].equals(name)) { + throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start"); + } + + if (pending) { + check(true); + depth--; + } + else { + if (indent[depth + 1]) { + writer.write("\r\n"); + for (int i = 0; i < depth; i++) { + writer.write(" "); + } + } + + writer.write("</"); + String prefix = elementStack[depth * 3 + 1]; + if (!StringUtils.isEmpty(prefix)) { + writer.write(prefix); + writer.write(':'); + } + writer.write(name); + writer.write('>'); + } + + nspCounts[depth + 1] = nspCounts[depth]; + return this; + } + + public String getNamespace() { + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; + } + + public String getName() { + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; + } + + public int getDepth() { + return pending ? depth + 1 : depth; + } + + public XmlSerializer text(String text) throws IOException { + check(false); + indent[depth] = false; + writeEscaped(text, -1); + return this; + } + + public XmlSerializer text(char[] text, int start, int len) + throws IOException { + text(new String(text, start, len)); + return this; + } + + public void cdsect(String data) throws IOException { + check(false); + // BEGIN android-changed: ]]> is not allowed within a CDATA, + // so break and start a new one when necessary. + data = data.replace("]]>", "]]]]><![CDATA[>"); + char[] chars = data.toCharArray(); + // We also aren't allowed any invalid characters. + for (char ch : chars) { + boolean valid = (ch >= 0x20 && ch <= 0xd7ff) || + (ch == '\t' || ch == '\n' || ch == '\r') || + (ch >= 0xe000 && ch <= 0xfffd); + if (!valid) { + reportInvalidCharacter(ch); + } + } + writer.write("<![CDATA["); + writer.write(chars, 0, chars.length); + writer.write("]]>"); + // END android-changed + } + + public void comment(String comment) throws IOException { + check(false); + writer.write("<!--"); + writer.write(comment); + writer.write("-->"); + } + + public void processingInstruction(String pi) + throws IOException { + check(false); + writer.write("<?"); + writer.write(pi); + writer.write("?>"); + } +}
\ No newline at end of file diff --git a/tests/src/cgeo/geocaching/export/ExportTest.java b/tests/src/cgeo/geocaching/export/ExportTest.java index 22c6dd2..60d5716 100644 --- a/tests/src/cgeo/geocaching/export/ExportTest.java +++ b/tests/src/cgeo/geocaching/export/ExportTest.java @@ -3,7 +3,15 @@ package cgeo.geocaching.export; import cgeo.CGeoTestCase; import cgeo.geocaching.Geocache; import cgeo.geocaching.LogEntry; +import cgeo.geocaching.cgData; +import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LogType; +import cgeo.geocaching.geopoint.Geopoint; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; public class ExportTest extends CGeoTestCase { @@ -16,4 +24,48 @@ public class ExportTest extends CGeoTestCase { assertEquals("Non matching export " + logStr.toString(), "GCX1234,2012-11-18T13:20:20Z,Found it,\"Hidden in a tree\"\n", logStr.toString()); } + public static void testGpxExportSmilies() { + final Geocache cache = new Geocache(); + cache.setGeocode("GCX1234"); + cache.setCoords(new Geopoint("N 49 44.000 E 8 37.000")); + final LogEntry log = new LogEntry(1353244820000L, LogType.FOUND_IT, "Smile: \ud83d\ude0a"); + cache.getLogs().add(log); + cgData.saveCache(cache, LoadFlags.SAVE_ALL); + ArrayList<Geocache> exportList = new ArrayList<Geocache>(); + exportList.add(cache); + GpxExportTester gpxExport = new GpxExportTester(); + File result = null; + try { + result = gpxExport.testExportSync(exportList); + } catch (InterruptedException e) { + fail(e.getCause().toString()); + } catch (ExecutionException e) { + fail(e.getCause().toString()); + } finally { + cgData.removeCache(cache.getGeocode(), LoadFlags.REMOVE_ALL); + } + + assertNotNull(result); + + result.delete(); + } + + private static class GpxExportTester extends GpxExport { + + protected GpxExportTester() { + super(); + } + + public File testExportSync(List<Geocache> caches) throws InterruptedException, ExecutionException { + final ArrayList<String> geocodes = new ArrayList<String>(caches.size()); + for (final Geocache cache: caches) { + geocodes.add(cache.getGeocode()); + } + final ExportTask task = new ExportTask(null); + task.execute(geocodes.toArray(new String[geocodes.size()])); + return task.get(); + } + + } + } |