/*
* 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;
}
}