| package org.xmlrpc.android; |
| |
| import android.text.TextUtils; |
| import android.util.Base64; |
| import android.util.Xml; |
| |
| import org.wordpress.android.util.helpers.MediaFile; |
| import org.wordpress.android.util.AppLog; |
| import org.wordpress.android.util.AppLog.T; |
| import org.wordpress.android.util.StringUtils; |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.BufferedReader; |
| import java.io.DataInputStream; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.StringReader; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.SimpleTimeZone; |
| |
| class XMLRPCSerializer { |
| static final String TAG_NAME = "name"; |
| static final String TAG_MEMBER = "member"; |
| static final String TAG_VALUE = "value"; |
| static final String TAG_DATA = "data"; |
| |
| static final String TYPE_INT = "int"; |
| static final String TYPE_I4 = "i4"; |
| static final String TYPE_I8 = "i8"; |
| static final String TYPE_DOUBLE = "double"; |
| static final String TYPE_BOOLEAN = "boolean"; |
| static final String TYPE_STRING = "string"; |
| static final String TYPE_DATE_TIME_ISO8601 = "dateTime.iso8601"; |
| static final String TYPE_BASE64 = "base64"; |
| static final String TYPE_ARRAY = "array"; |
| static final String TYPE_STRUCT = "struct"; |
| |
| static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); |
| static Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); |
| |
| private static final XmlSerializer serializeTester; |
| |
| static { |
| serializeTester = Xml.newSerializer(); |
| try { |
| serializeTester.setOutput(new NullOutputStream(), "UTF-8"); |
| } catch (IllegalArgumentException e) { |
| AppLog.e(AppLog.T.EDITOR, "IllegalArgumentException setting test serializer output stream", e ); |
| } catch (IllegalStateException e) { |
| AppLog.e(AppLog.T.EDITOR, "IllegalStateException setting test serializer output stream", e ); |
| } catch (IOException e) { |
| AppLog.e(AppLog.T.EDITOR, "IOException setting test serializer output stream", e ); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| static void serialize(XmlSerializer serializer, Object object) throws IOException { |
| // check for scalar types: |
| if (object instanceof Integer || object instanceof Short || object instanceof Byte) { |
| serializer.startTag(null, TYPE_I4).text(object.toString()).endTag(null, TYPE_I4); |
| } else |
| if (object instanceof Long) { |
| // Note Long should be represented by a TYPE_I8 but the WordPress end point doesn't support <i8> tag |
| // Long usually represents IDs, so we convert them to string |
| serializer.startTag(null, TYPE_STRING).text(object.toString()).endTag(null, TYPE_STRING); |
| AppLog.w(T.API, "long type could be misinterpreted when sent to the WordPress XMLRPC end point"); |
| } else |
| if (object instanceof Double || object instanceof Float) { |
| serializer.startTag(null, TYPE_DOUBLE).text(object.toString()).endTag(null, TYPE_DOUBLE); |
| } else |
| if (object instanceof Boolean) { |
| Boolean bool = (Boolean) object; |
| String boolStr = bool.booleanValue() ? "1" : "0"; |
| serializer.startTag(null, TYPE_BOOLEAN).text(boolStr).endTag(null, TYPE_BOOLEAN); |
| } else |
| if (object instanceof String) { |
| serializer.startTag(null, TYPE_STRING).text(makeValidInputString((String) object)).endTag(null, TYPE_STRING); |
| } else |
| if (object instanceof Date || object instanceof Calendar) { |
| Date date = (Date) object; |
| SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); |
| dateFormat.setCalendar(cal); |
| String sDate = dateFormat.format(date); |
| serializer.startTag(null, TYPE_DATE_TIME_ISO8601).text(sDate).endTag(null, TYPE_DATE_TIME_ISO8601); |
| } else |
| if (object instanceof byte[] ){ |
| String value; |
| try { |
| value = Base64.encodeToString((byte[])object, Base64.DEFAULT); |
| serializer.startTag(null, TYPE_BASE64).text(value).endTag(null, TYPE_BASE64); |
| } catch (OutOfMemoryError e) { |
| throw new IOException("Out of memory"); |
| } |
| } |
| else if( object instanceof MediaFile ) { |
| //convert media file binary to base64 |
| serializer.startTag( null, "base64" ); |
| MediaFile mediaFile = (MediaFile) object; |
| InputStream inStream = new DataInputStream(new FileInputStream(mediaFile.getFilePath())); |
| byte[] buffer = new byte[3600];//you must use a 24bit multiple |
| int length = -1; |
| String chunk = null; |
| while ((length = inStream.read(buffer)) > 0) { |
| chunk = Base64.encodeToString(buffer, 0, length, Base64.DEFAULT); |
| serializer.text(chunk); |
| } |
| inStream.close(); |
| serializer.endTag(null, "base64"); |
| }else |
| if (object instanceof List<?>) { |
| serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); |
| List<Object> list = (List<Object>) object; |
| Iterator<Object> iter = list.iterator(); |
| while (iter.hasNext()) { |
| Object o = iter.next(); |
| serializer.startTag(null, TAG_VALUE); |
| serialize(serializer, o); |
| serializer.endTag(null, TAG_VALUE); |
| } |
| serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); |
| } else |
| if (object instanceof Object[]) { |
| serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); |
| Object[] objects = (Object[]) object; |
| for (int i=0; i<objects.length; i++) { |
| Object o = objects[i]; |
| serializer.startTag(null, TAG_VALUE); |
| serialize(serializer, o); |
| serializer.endTag(null, TAG_VALUE); |
| } |
| serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); |
| } else |
| if (object instanceof Map) { |
| serializer.startTag(null, TYPE_STRUCT); |
| Map<String, Object> map = (Map<String, Object>) object; |
| Iterator<Entry<String, Object>> iter = map.entrySet().iterator(); |
| while (iter.hasNext()) { |
| Entry<String, Object> entry = iter.next(); |
| String key = entry.getKey(); |
| Object value = entry.getValue(); |
| |
| serializer.startTag(null, TAG_MEMBER); |
| serializer.startTag(null, TAG_NAME).text(key).endTag(null, TAG_NAME); |
| serializer.startTag(null, TAG_VALUE); |
| serialize(serializer, value); |
| serializer.endTag(null, TAG_VALUE); |
| serializer.endTag(null, TAG_MEMBER); |
| } |
| serializer.endTag(null, TYPE_STRUCT); |
| } else { |
| throw new IOException("Cannot serialize " + object); |
| } |
| } |
| |
| private static final String makeValidInputString(final String input) throws IOException { |
| if (TextUtils.isEmpty(input)) |
| return ""; |
| |
| if (serializeTester == null) |
| return input; |
| |
| try { |
| // try to encode the string as-is, 99.9% of the time it's OK |
| serializeTester.text(input); |
| return input; |
| } catch (IllegalArgumentException e) { |
| // There are characters outside the XML unicode charset as specified by the XML 1.0 standard |
| // See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char |
| AppLog.e(AppLog.T.EDITOR, "There are characters outside the XML unicode charset as specified by the XML 1.0 standard", e ); |
| } |
| |
| // We need to do the following things: |
| // 1. Replace surrogates with HTML Entity. |
| // 2. Replace emoji with their textual versions (if available on WP) |
| // 3. Try to serialize the resulting string. |
| // 4. If it fails again, strip characters that are not allowed in XML 1.0 |
| final String noEmojiString = StringUtils.replaceUnicodeSurrogateBlocksWithHTMLEntities(input); |
| try { |
| serializeTester.text(noEmojiString); |
| return noEmojiString; |
| } catch (IllegalArgumentException e) { |
| AppLog.e(AppLog.T.EDITOR, "noEmojiString still contains characters outside the XML unicode charset as specified by the XML 1.0 standard", e ); |
| return StringUtils.stripNonValidXMLCharacters(noEmojiString); |
| } |
| } |
| |
| static Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException, NumberFormatException { |
| parser.require(XmlPullParser.START_TAG, null, TAG_VALUE); |
| |
| parser.nextTag(); |
| String typeNodeName = parser.getName(); |
| |
| Object obj; |
| if (typeNodeName.equals(TYPE_INT) || typeNodeName.equals(TYPE_I4)) { |
| String value = parser.nextText(); |
| try { |
| obj = Integer.parseInt(value); |
| } catch (NumberFormatException nfe) { |
| AppLog.w(T.API, "Server replied with an invalid 4 bytes int value, trying to parse it as 8 bytes long"); |
| obj = Long.parseLong(value); |
| } |
| } else |
| if (typeNodeName.equals(TYPE_I8)) { |
| String value = parser.nextText(); |
| obj = Long.parseLong(value); |
| } else |
| if (typeNodeName.equals(TYPE_DOUBLE)) { |
| String value = parser.nextText(); |
| obj = Double.parseDouble(value); |
| } else |
| if (typeNodeName.equals(TYPE_BOOLEAN)) { |
| String value = parser.nextText(); |
| obj = value.equals("1") ? Boolean.TRUE : Boolean.FALSE; |
| } else |
| if (typeNodeName.equals(TYPE_STRING)) { |
| obj = parser.nextText(); |
| } else |
| if (typeNodeName.equals(TYPE_DATE_TIME_ISO8601)) { |
| dateFormat.setCalendar(cal); |
| String value = parser.nextText(); |
| try { |
| obj = dateFormat.parseObject(value); |
| } catch (ParseException e) { |
| AppLog.e(T.API, e); |
| obj = value; |
| } |
| } else |
| if (typeNodeName.equals(TYPE_BASE64)) { |
| String value = parser.nextText(); |
| BufferedReader reader = new BufferedReader(new StringReader(value)); |
| String line; |
| StringBuffer sb = new StringBuffer(); |
| while ((line = reader.readLine()) != null) { |
| sb.append(line); |
| } |
| obj = Base64.decode(sb.toString(), Base64.DEFAULT); |
| } else |
| if (typeNodeName.equals(TYPE_ARRAY)) { |
| parser.nextTag(); // TAG_DATA (<data>) |
| parser.require(XmlPullParser.START_TAG, null, TAG_DATA); |
| |
| parser.nextTag(); |
| List<Object> list = new ArrayList<Object>(); |
| while (parser.getName().equals(TAG_VALUE)) { |
| list.add(deserialize(parser)); |
| parser.nextTag(); |
| } |
| parser.require(XmlPullParser.END_TAG, null, TAG_DATA); |
| parser.nextTag(); // TAG_ARRAY (</array>) |
| parser.require(XmlPullParser.END_TAG, null, TYPE_ARRAY); |
| obj = list.toArray(); |
| } else |
| if (typeNodeName.equals(TYPE_STRUCT)) { |
| parser.nextTag(); |
| Map<String, Object> map = new HashMap<String, Object>(); |
| while (parser.getName().equals(TAG_MEMBER)) { |
| String memberName = null; |
| Object memberValue = null; |
| while (true) { |
| parser.nextTag(); |
| String name = parser.getName(); |
| if (name.equals(TAG_NAME)) { |
| memberName = parser.nextText(); |
| } else |
| if (name.equals(TAG_VALUE)) { |
| memberValue = deserialize(parser); |
| } else { |
| break; |
| } |
| } |
| if (memberName != null && memberValue != null) { |
| map.put(memberName, memberValue); |
| } |
| parser.require(XmlPullParser.END_TAG, null, TAG_MEMBER); |
| parser.nextTag(); |
| } |
| parser.require(XmlPullParser.END_TAG, null, TYPE_STRUCT); |
| obj = map; |
| } else { |
| throw new IOException("Cannot deserialize " + parser.getName()); |
| } |
| parser.nextTag(); // TAG_VALUE (</value>) |
| parser.require(XmlPullParser.END_TAG, null, TAG_VALUE); |
| return obj; |
| } |
| } |