/* * Copyright (C) 2009 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.polo.wire.xml; import com.google.polo.exception.PoloException; import com.google.polo.exception.ProtocolErrorException; import com.google.polo.json.JSONArray; import com.google.polo.json.JSONException; import com.google.polo.json.JSONObject; import com.google.polo.json.XML; import com.google.polo.pairing.PoloUtil; import com.google.polo.pairing.message.ConfigurationAckMessage; import com.google.polo.pairing.message.ConfigurationMessage; import com.google.polo.pairing.message.EncodingOption; import com.google.polo.pairing.message.EncodingOption.EncodingType; import com.google.polo.pairing.message.OptionsMessage; import com.google.polo.pairing.message.OptionsMessage.ProtocolRole; import com.google.polo.pairing.message.PairingRequestAckMessage; import com.google.polo.pairing.message.PairingRequestMessage; import com.google.polo.pairing.message.PoloMessage; import com.google.polo.pairing.message.PoloMessage.PoloMessageType; import com.google.polo.pairing.message.SecretAckMessage; import com.google.polo.pairing.message.SecretMessage; /** * A collection of methods to convert {@link PoloMessage}s to and from XML * format. * <p> * This wire format was specified by a third party; it uses a proprietary * 64-byte message header/delimiter, and message internals are inconsistent with * the protocol buffer in several places. */ public class XmlMessageBuilder { /* * Status types. * NOTE(mikey): These do not match the values defined by * OuterMessage.MessageType in polo.proto. */ public static final int STATUS_OK = 1; public static final int STATUS_ERROR = 2; /* * Key names for XML versions of messages. */ // OuterMessage XML key names private static final String OUTER_FIELD_TYPE = "msg_type"; private static final String OUTER_FIELD_STATUS = "status"; private static final String OUTER_FIELD_MSG_ID = "msg_id"; private static final String OUTER_FIELD_PAYLOAD = "pairing_msg"; // PairingRequestMessage XML key names private static final String PAIRING_REQUEST_FIELD_PROTOCOL_VERSION = "proto_version"; // OptionsMessage XML key names private static final String OPTIONS_FIELD_PREFERRED_ROLE = "pref_role"; private static final String OPTIONS_FIELD_OUTPUT_ENCODINGS = "out_encodings"; private static final String OPTIONS_FIELD_INPUT_ENCODINGS = "in_encodings"; // ConfigurationMessage XML key names private static final String CONFIG_FIELD_CLIENT_ROLE = "role"; // EncodingOption XML key names private static final String ENCODING_FIELD_TYPE = "type"; private static final String ENCODING_FIELD_SYMBOL_LENGTH = "min_length"; private static final String ENCODING_FIELD_MAX_LENGTH = "max_length"; private static final String ENCODING_SUBFIELD_ENCODING = "encoding"; // SecretMessage XML key names private static final String SECRET_FIELD_SECRET = "bytes"; // Payload container names private static final String MESSAGE_CONTAINER_NAME_PAIRING_REQUEST = "pairing_req"; private static final String MESSAGE_CONTAINER_NAME_PAIRING_REQUEST_ACK = "pairing_req_ack"; private static final String MESSAGE_CONTAINER_NAME_OPTIONS = "config_options"; private static final String MESSAGE_CONTAINER_NAME_CONFIG = "config"; private static final String MESSAGE_CONTAINER_NAME_SECRET = "secret"; private static final String PAIRING_REQUEST_FIELD_SERVICE_NAME = "svc_name"; private static final String PAIRING_REQUEST_FIELD_CLIENT_NAME = "client_name"; private static final String PAIRING_REQUEST_ACK_FIELD_SERVER_NAME = "server_name"; // // Encoding types -- these do not match polo.proto's enum. // public static final int ENCODING_TYPE_NUMERIC = 1; public static final int ENCODING_TYPE_HEXADECIMAL = 2; public static final int ENCODING_TYPE_ALPHANUMERIC = 3; public static final int ENCODING_TYPE_QRCODE = 4; /** * Cache of the last message id header value received. The value should be * copied to any response. */ private String mLastMessageId; public XmlMessageBuilder() { mLastMessageId = null; } /** * Builds a {@link PoloMessage} from the XML version of the outer message. * * @param outerXml the outermost XML string * @return a new {@link PoloMessage} * @throws PoloException on error parsing the message */ PoloMessage outerXMLToPoloMessage(String outerXml) throws PoloException { JSONObject outerMessage; try { outerMessage = XML.toJSONObject(outerXml); } catch (JSONException e) { throw new PoloException(e); } JSONObject payload; PoloMessageType messageType; try { payload = outerMessage.getJSONObject(OUTER_FIELD_PAYLOAD); int status = payload.getInt(OUTER_FIELD_STATUS); if (status != STATUS_OK) { throw new ProtocolErrorException("Peer reported an error."); } int msgIntVal = payload.getInt(OUTER_FIELD_TYPE); messageType = PoloMessageType.fromIntVal(msgIntVal); } catch (JSONException e) { throw new PoloException("Bad outer message.", e); } if (outerMessage.has("msg_id")) { try { mLastMessageId = outerMessage.getString("msg_id"); } catch (JSONException e) { } } else { mLastMessageId = null; } switch (messageType) { case PAIRING_REQUEST: return getPairingRequest(payload); case PAIRING_REQUEST_ACK: return getPairingRequestAck(payload); case OPTIONS: return getOptionsMessage(payload); case CONFIGURATION: return getConfigMessage(payload); case CONFIGURATION_ACK: return getConfigAckMessage(payload); case SECRET: return getSecretMessage(payload); case SECRET_ACK: return getSecretAckMessage(payload); default: return null; } } /* * Methods to convert XML inner messages to PoloMessage instances. * * NOTE(mikey): These methods are implemented in terms of JSONObject * as a convenient way to represent hierarchical key->(dict|list|value) * structures. * * Note that these methods are very similar to those found in * JsonWireAdapter. However, the XML wire format was specified with slight * differences compared to the protocol buffer definition. For example, * in the OptionsMessage, encodings are wrapped in an "<options>" container. * * Also, many fields names have slight differences compared to the names in * the protocol buffer (for example, in PairingRequestMessage, "service_name" * is called "svc_name".) */ /** * Generates a new {@link PairingRequestMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ PairingRequestMessage getPairingRequest(JSONObject body) throws PoloException { try { JSONObject jsonObj = body.getJSONObject( MESSAGE_CONTAINER_NAME_PAIRING_REQUEST); String serviceName = jsonObj.getString( PAIRING_REQUEST_FIELD_SERVICE_NAME); String clientName = null; if (jsonObj.has(PAIRING_REQUEST_FIELD_CLIENT_NAME)) { clientName = jsonObj.getString(PAIRING_REQUEST_FIELD_CLIENT_NAME); } return new PairingRequestMessage(serviceName, clientName); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } } /** * Generates a new {@link PairingRequestAckMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ PairingRequestAckMessage getPairingRequestAck(JSONObject body) throws PoloException { try { JSONObject jsonObj = body.getJSONObject( MESSAGE_CONTAINER_NAME_PAIRING_REQUEST_ACK); String serverName = null; if (jsonObj.has(PAIRING_REQUEST_ACK_FIELD_SERVER_NAME)) { serverName = jsonObj.getString(PAIRING_REQUEST_ACK_FIELD_SERVER_NAME); } return new PairingRequestAckMessage(serverName); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } } /** * Generates a new {@link OptionsMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ OptionsMessage getOptionsMessage(JSONObject body) throws PoloException { OptionsMessage options = new OptionsMessage(); JSONObject jsonOptions; try { jsonOptions = body.getJSONObject(MESSAGE_CONTAINER_NAME_OPTIONS); JSONObject inEnc = jsonOptions.getJSONObject( OPTIONS_FIELD_INPUT_ENCODINGS); JSONObject outEnc = jsonOptions.getJSONObject( OPTIONS_FIELD_OUTPUT_ENCODINGS); // Input encodings JSONArray inEncodings = new JSONArray(); try { inEncodings = inEnc.getJSONArray(ENCODING_SUBFIELD_ENCODING); } catch (JSONException e) { if (inEnc.has(ENCODING_SUBFIELD_ENCODING)) { JSONObject enc = inEnc.getJSONObject(ENCODING_SUBFIELD_ENCODING); inEncodings.put(enc); } } for (int i = 0; i < inEncodings.length(); i++) { JSONObject enc = inEncodings.getJSONObject(i); options.addInputEncoding(getEncodingOption(enc)); } // Output encodings JSONArray outEncodings = new JSONArray(); try { outEncodings = outEnc.getJSONArray(ENCODING_SUBFIELD_ENCODING); } catch (JSONException e) { if (outEnc.has(ENCODING_SUBFIELD_ENCODING)) { JSONObject enc = outEnc.getJSONObject(ENCODING_SUBFIELD_ENCODING); outEncodings.put(enc); } } for (int i = 0; i < outEncodings.length(); i++) { JSONObject enc = outEncodings.getJSONObject(i); options.addOutputEncoding(getEncodingOption(enc)); } // Role ProtocolRole role = ProtocolRole.fromIntVal( jsonOptions.getInt(OPTIONS_FIELD_PREFERRED_ROLE)); options.setProtocolRolePreference(role); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } return options; } /** * Generates a new {@link ConfigurationMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ ConfigurationMessage getConfigMessage(JSONObject body) throws PoloException { try { EncodingOption encoding = getEncodingOption( body.getJSONObject(MESSAGE_CONTAINER_NAME_CONFIG) .getJSONObject(ENCODING_SUBFIELD_ENCODING)); ProtocolRole role = ProtocolRole.fromIntVal( body.getJSONObject(MESSAGE_CONTAINER_NAME_CONFIG) .getInt(CONFIG_FIELD_CLIENT_ROLE)); return new ConfigurationMessage(encoding, role); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } } /** * Generates a new {@link ConfigurationAckMessage} from a JSON payload. * * @param body the JSON payload * @return the new message */ ConfigurationAckMessage getConfigAckMessage(JSONObject body) { return new ConfigurationAckMessage(); } /** * Generates a new {@link SecretMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ SecretMessage getSecretMessage(JSONObject body) throws PoloException { String secret; try { secret = body.getJSONObject(MESSAGE_CONTAINER_NAME_SECRET) .getString(SECRET_FIELD_SECRET); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } byte[] secretBytes = PoloUtil.hexStringToBytes(secret); return new SecretMessage(secretBytes); } /** * Generates a new {@link SecretAckMessage} from a JSON payload. * * @param body the JSON payload * @return the new message */ SecretAckMessage getSecretAckMessage(JSONObject body) { return new SecretAckMessage(null); } /** * Generates a new {@link EncodingOption} from a JSON sub-dictionary. * * @param option the JSON sub-dictionary describing the option * @return the new {@link EncodingOption} * @throws JSONException on error parsing the {@link JSONObject} */ EncodingOption getEncodingOption(JSONObject option) throws JSONException { int length = option.getInt(ENCODING_FIELD_SYMBOL_LENGTH); int intType = option.getInt(ENCODING_FIELD_TYPE); EncodingType type = encodingTypeFromIntValue(intType); return new EncodingOption(type, length); } /** * Converts a {@link PoloMessage} to an XML string. * * @param message the message to convert * @return the same message, as translated to XML */ public String poloMessageToXML(PoloMessage message) { try { if (message instanceof PairingRequestMessage) { return toXML((PairingRequestMessage) message); } else if (message instanceof PairingRequestAckMessage) { return toXML((PairingRequestAckMessage) message); } else if (message instanceof OptionsMessage) { return toXML((OptionsMessage) message); } else if (message instanceof ConfigurationMessage) { return toXML((ConfigurationMessage) message); } else if (message instanceof ConfigurationAckMessage) { return toXML((ConfigurationAckMessage) message); } else if (message instanceof SecretMessage) { return toXML((SecretMessage) message); } else if (message instanceof SecretAckMessage) { return toXML((SecretAckMessage) message); } return null; } catch (JSONException e) { e.printStackTrace(); return ""; } } /** * Generates a String corresponding to a full wire message (wrapped in * an outer message) for the given payload. */ public String getOuterXML(PoloMessage message, int status) { StringBuffer out = new StringBuffer(); out.append("<" + OUTER_FIELD_PAYLOAD + ">\n"); // status out.append("<" + OUTER_FIELD_STATUS + ">"); out.append(status); out.append("</" + OUTER_FIELD_STATUS + ">\n"); // msg_id (optional) if (mLastMessageId != null) { out.append("<" + OUTER_FIELD_MSG_ID + ">"); out.append(mLastMessageId); out.append("</" + OUTER_FIELD_MSG_ID + ">\n"); } // payload if (message != null) { int msgType = message.getType().getAsInt(); out.append("<" + OUTER_FIELD_TYPE + ">"); out.append(msgType); out.append("</" + OUTER_FIELD_TYPE + ">\n"); out.append(poloMessageToXML(message)); out.append("\n"); } out.append("</" + OUTER_FIELD_PAYLOAD + ">\n"); return out.toString(); } /** * Generates an error payload corresponding to an outer message with an * error code in the status field. The error code is determined by the type * of the exception. * * @param exception the {@link Exception} to use to determine the error * code * @return a string outer message * @throws PoloException on error building the message */ public String getErrorXML(Exception exception) throws PoloException { return getOuterXML(null, STATUS_ERROR); } /** * Translates a {@link PairingRequestMessage} to an XML string. * * @throws JSONException on error generating the {@link String}. */ String toXML(PairingRequestMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject pairingReq = new JSONObject(); jsonObj.put(MESSAGE_CONTAINER_NAME_PAIRING_REQUEST, pairingReq); pairingReq.put(PAIRING_REQUEST_FIELD_SERVICE_NAME, message.getServiceName()); if (message.hasClientName()) { pairingReq.put(PAIRING_REQUEST_FIELD_CLIENT_NAME, message.getServiceName()); } pairingReq.put(PAIRING_REQUEST_FIELD_PROTOCOL_VERSION, 1); return XML.toString(jsonObj); } /** * Translates a {@link PairingRequestAckMessage} to an XML string. * * @throws JSONException on error generating the {@link String}. */ String toXML(PairingRequestAckMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject pairingReq = new JSONObject(); jsonObj.put(MESSAGE_CONTAINER_NAME_PAIRING_REQUEST_ACK, pairingReq); if (message.hasServerName()) { jsonObj.put(PAIRING_REQUEST_ACK_FIELD_SERVER_NAME, message.getServerName()); } pairingReq.put(PAIRING_REQUEST_FIELD_PROTOCOL_VERSION, 1); return XML.toString(jsonObj); } /** * Translates a {@link OptionsMessage} to an XML string. * * @throws JSONException on error generating the {@link String}. */ String toXML(OptionsMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject options = new JSONObject(); JSONObject inEncs = new JSONObject(); JSONArray inEncsArray = new JSONArray(); for (EncodingOption encoding : message.getInputEncodingSet()) { inEncsArray.put(encodingToJson(encoding)); } inEncs.put(ENCODING_SUBFIELD_ENCODING, inEncsArray); options.put(OPTIONS_FIELD_INPUT_ENCODINGS, inEncs); JSONObject outEncs = new JSONObject(); JSONArray outEncsArray = new JSONArray(); for (EncodingOption encoding : message.getOutputEncodingSet()) { outEncsArray.put(encodingToJson(encoding)); } outEncs.put(ENCODING_SUBFIELD_ENCODING, outEncsArray); options.put(OPTIONS_FIELD_OUTPUT_ENCODINGS, outEncs); options.put(OPTIONS_FIELD_PREFERRED_ROLE, message.getProtocolRolePreference().ordinal()); jsonObj.put(MESSAGE_CONTAINER_NAME_OPTIONS, options); return XML.toString(jsonObj); } /** * Translates a {@link ConfigurationMessage} to an XML string. * * @throws JSONException on error generating the {@link String}. */ String toXML(ConfigurationMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject config = new JSONObject(); JSONObject encoding = encodingToJson(message.getEncoding()); config.put(ENCODING_SUBFIELD_ENCODING, encoding); config.put(CONFIG_FIELD_CLIENT_ROLE, message.getClientRole().ordinal()); jsonObj.put(MESSAGE_CONTAINER_NAME_CONFIG, config); return XML.toString(jsonObj); } /** * Translates a {@link ConfigurationAckMessage} to an XML string. */ String toXML(ConfigurationAckMessage message) { return ""; } /** * Translates a {@link SecretMessage} to an XML string. * * @throws JSONException on error generating the {@link String}. */ String toXML(SecretMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject secret = new JSONObject(); String bytesStr = PoloUtil.bytesToHexString(message.getSecret()); secret.put(SECRET_FIELD_SECRET, bytesStr); jsonObj.put(MESSAGE_CONTAINER_NAME_SECRET, secret); return XML.toString(jsonObj); } /** * Translates a {@link SecretAckMessage} to an XML string. */ String toXML(SecretAckMessage message) { return ""; } /** * Translates a {@link EncodingOption} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ JSONObject encodingToJson(EncodingOption encoding) throws JSONException { JSONObject result = new JSONObject(); int intType = encodingTypeToIntVal(encoding.getType()); result.put(ENCODING_FIELD_TYPE, intType); result.put(ENCODING_FIELD_SYMBOL_LENGTH, encoding.getSymbolLength()); result.put(ENCODING_FIELD_MAX_LENGTH, encoding.getSymbolLength()); return result; } /** * Converts an {@link EncodingType} to the numeric value used on the wire. * <p> * Note that in this implementation, the values used on the wire do not match * those returned by {@link EncodingType#getAsInt()}, hence the extra method. * * @param type the {@link EncodingType} * @return an integer representation */ private static int encodingTypeToIntVal(EncodingType type) { switch (type) { case ENCODING_ALPHANUMERIC: return ENCODING_TYPE_ALPHANUMERIC; case ENCODING_NUMERIC: return ENCODING_TYPE_NUMERIC; case ENCODING_HEXADECIMAL: return ENCODING_TYPE_HEXADECIMAL; case ENCODING_QRCODE: return ENCODING_TYPE_QRCODE; case ENCODING_UNKNOWN: default: return 0; } } /** * Converts a numeric value used on the wire to the corresponding * {@link EncodingType}. * <p> * Note that in this implementation, the values used on the wire do not match * those returned by {@link EncodingType#getAsInt()}, hence the extra method. * * @param intType the value used on the wire * @return the corresponding {@link EncodingType} */ private static EncodingType encodingTypeFromIntValue(int intType) { EncodingType type = EncodingType.ENCODING_UNKNOWN; switch (intType) { case ENCODING_TYPE_ALPHANUMERIC: type = EncodingType.ENCODING_ALPHANUMERIC; break; case ENCODING_TYPE_NUMERIC: type = EncodingType.ENCODING_NUMERIC; break; case ENCODING_TYPE_HEXADECIMAL: type = EncodingType.ENCODING_HEXADECIMAL; break; case ENCODING_TYPE_QRCODE: type = EncodingType.ENCODING_QRCODE; break; default: type = EncodingType.ENCODING_UNKNOWN; break; } return type; } }