/* * 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.json; import com.google.polo.exception.BadSecretException; import com.google.polo.exception.NoConfigurationException; 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.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; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; /** * A collection of methods to convert {@link PoloMessage}s to and from JSON * format. * <p> * Messages are based on the descriptions found in the file polo.proto. This * mimics the field name and message compositions found in that file. */ public class JsonMessageBuilder { public static final int PROTOCOL_VERSION = 1; /* * Status types. These match the values defined by OuterMessage.MessageType * in polo.proto. */ public static final int STATUS_OK = 200; public static final int STATUS_ERROR = 400; public static final int STATUS_BAD_CONFIGURATION = 401; public static final int STATUS_BAD_SECRET = 403; /* * Key names for JSON versions of messages. */ // OuterMessage JSON key names private static final String OUTER_FIELD_PAYLOAD = "payload"; private static final String OUTER_FIELD_TYPE = "type"; private static final String OUTER_FIELD_STATUS = "status"; private static final String OUTER_FIELD_PROTOCOL_VERSION = "protocol_version"; // PairingRequestMessage JSON key names private static final String PAIRING_REQUEST_FIELD_SERVICE_NAME = "service_name"; private static final String PAIRING_REQUEST_FIELD_CLIENT_NAME = "client_name"; // PairingRequestAckMessage JSON key names private static final String PAIRING_REQUEST_ACK_FIELD_SERVER_NAME = "server_name"; // OptionsMessage JSON key names private static final String OPTIONS_FIELD_PREFERRED_ROLE = "preferred_role"; private static final String OPTIONS_FIELD_OUTPUT_ENCODINGS = "output_encodings"; private static final String OPTIONS_FIELD_INPUT_ENCODINGS = "input_encodings"; // ConfigurationMessage JSON key names private static final String CONFIG_FIELD_CLIENT_ROLE = "client_role"; private static final String CONFIG_FIELD_ENCODING = "encoding"; // EncodingOption JSON key names private static final String ENCODING_FIELD_TYPE = "type"; private static final String ENCODING_FIELD_SYMBOL_LENGTH = "symbol_length"; // SecretMessage JSON key names private static final String SECRET_FIELD_SECRET = "secret"; // SecretAckMessage JSON key names private static final String SECRET_ACK_FIELD_SECRET = "secret"; /** * Builds a {@link PoloMessage} from the JSON version of the outer message. * * @param outerMessage a {@link JSONObject} corresponding to the * outermost wire message * @return a new {@link PoloMessage} * @throws PoloException on error parsing the {@link JSONObject} */ public static PoloMessage outerJsonToPoloMessage(JSONObject outerMessage) throws PoloException { JSONObject payload; int status; PoloMessageType messageType; try { status = outerMessage.getInt(OUTER_FIELD_STATUS); if (status != STATUS_OK) { throw new ProtocolErrorException("Peer reported an error."); } payload = outerMessage.getJSONObject(OUTER_FIELD_PAYLOAD); int msgIntVal = outerMessage.getInt(OUTER_FIELD_TYPE); messageType = PoloMessageType.fromIntVal(msgIntVal); } catch (JSONException e) { throw new PoloException("Bad outer message.", e); } 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 JSON messages to PoloMessage instances // /** * 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} */ static PairingRequestMessage getPairingRequest(JSONObject body) throws PoloException { try { String serviceName = body.getString(PAIRING_REQUEST_FIELD_SERVICE_NAME); String clientName = null; if (body.has(PAIRING_REQUEST_FIELD_CLIENT_NAME)) { clientName = body.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 */ static PairingRequestAckMessage getPairingRequestAck(JSONObject body) throws PoloException { try { String serverName = null; if (body.has(PAIRING_REQUEST_ACK_FIELD_SERVER_NAME)) { serverName = body.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} */ static OptionsMessage getOptionsMessage(JSONObject body) throws PoloException { OptionsMessage options = new OptionsMessage(); try { // Input encodings JSONArray inEncodings = new JSONArray(); try { if (body.has(OPTIONS_FIELD_INPUT_ENCODINGS)) { inEncodings = body.getJSONArray(OPTIONS_FIELD_INPUT_ENCODINGS); } } catch (JSONException e) { throw new PoloException("Bad input encodings", e); } for (int i = 0; i < inEncodings.length(); i++) { JSONObject enc = inEncodings.getJSONObject(i); options.addInputEncoding(getEncodingOption(enc)); } // Output encodings JSONArray outEncodings = new JSONArray(); try { if (body.has(OPTIONS_FIELD_OUTPUT_ENCODINGS)) { outEncodings = body.getJSONArray(OPTIONS_FIELD_OUTPUT_ENCODINGS); } } catch (JSONException e) { throw new PoloException("Bad output encodings", e); } for (int i = 0; i < outEncodings.length(); i++) { JSONObject enc = outEncodings.getJSONObject(i); options.addOutputEncoding(getEncodingOption(enc)); } // Role ProtocolRole role = ProtocolRole.fromIntVal( body.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} */ static ConfigurationMessage getConfigMessage(JSONObject body) throws PoloException { try { EncodingOption encoding = getEncodingOption( body.getJSONObject(CONFIG_FIELD_ENCODING)); ProtocolRole role = ProtocolRole.fromIntVal( body.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 */ static 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} */ static SecretMessage getSecretMessage(JSONObject body) throws PoloException { try { byte[] secretBytes = Base64.decode( body.getString(SECRET_FIELD_SECRET).getBytes()); return new SecretMessage(secretBytes); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } } /** * Generates a new {@link SecretAckMessage} from a JSON payload. * * @param body the JSON payload * @return the new message * @throws PoloException on error parsing the {@link JSONObject} */ static SecretAckMessage getSecretAckMessage(JSONObject body) throws PoloException { try { byte[] secretBytes = Base64.decode( body.getString(SECRET_ACK_FIELD_SECRET).getBytes()); return new SecretAckMessage(secretBytes); } catch (JSONException e) { throw new PoloException("Malformed message.", e); } } /** * 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} */ static EncodingOption getEncodingOption(JSONObject option) throws JSONException { int length = option.getInt(ENCODING_FIELD_SYMBOL_LENGTH); int intType = option.getInt(ENCODING_FIELD_TYPE); EncodingType type = EncodingType.fromIntVal(intType); return new EncodingOption(type, length); } /** * Converts a {@link PoloMessage} to a {@link JSONObject} * * @param message the message to convert * @return the same message, as translated to JSON * @throws PoloException if the message could not be generated */ public static JSONObject poloMessageToJson(PoloMessage message) throws PoloException { try { if (message instanceof PairingRequestMessage) { return toJson((PairingRequestMessage) message); } else if (message instanceof PairingRequestAckMessage) { return toJson((PairingRequestAckMessage) message); } else if (message instanceof OptionsMessage) { return toJson((OptionsMessage) message); } else if (message instanceof ConfigurationMessage) { return toJson((ConfigurationMessage) message); } else if (message instanceof ConfigurationAckMessage) { return toJson((ConfigurationAckMessage) message); } else if (message instanceof SecretMessage) { return toJson((SecretMessage) message); } else if (message instanceof SecretAckMessage) { return toJson((SecretAckMessage) message); } } catch (JSONException e) { throw new PoloException("Error generating message.", e); } throw new PoloException("Unknown PoloMessage type."); } /** * Generates a JSONObject corresponding to a full wire message (wrapped in * an outer message) for the given payload. * * @param message the payload to wrap * @return a {@link JSONObject} corresponding to the complete * wire message * @throws PoloException on error building the {@link JSONObject} */ public static JSONObject getOuterJson(PoloMessage message) throws PoloException { JSONObject out = new JSONObject(); int msgType = message.getType().getAsInt(); JSONObject innerJson = poloMessageToJson(message); try { out.put(OUTER_FIELD_PROTOCOL_VERSION, PROTOCOL_VERSION); out.put(OUTER_FIELD_STATUS, STATUS_OK); out.put(OUTER_FIELD_TYPE, msgType); out.put(OUTER_FIELD_PAYLOAD, innerJson); } catch (JSONException e) { throw new PoloException("Error serializing outer message", e); } return out; } /** * Generates a {@link JSONObject} corresponding to a wire 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 {@link JSONObject} corresponding to the complete * wire message * @throws PoloException on error building the {@link JSONObject} */ public static JSONObject getErrorJson(Exception exception) throws PoloException { JSONObject out = new JSONObject(); int errorStatus = STATUS_ERROR; if (exception instanceof NoConfigurationException) { errorStatus = STATUS_BAD_CONFIGURATION; } else if (exception instanceof BadSecretException) { errorStatus = STATUS_BAD_SECRET; } try { out.put(OUTER_FIELD_PROTOCOL_VERSION, PROTOCOL_VERSION); out.put(OUTER_FIELD_STATUS, errorStatus); } catch (JSONException e) { throw new PoloException("Error serializing outer message", e); } return out; } /** * Translates a {@link PairingRequestMessage} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(PairingRequestMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); jsonObj.put(PAIRING_REQUEST_FIELD_SERVICE_NAME, message.getServiceName()); if (message.hasClientName()) { jsonObj.put(PAIRING_REQUEST_FIELD_CLIENT_NAME, message.getClientName()); } return jsonObj; } /** * Translates a {@link PairingRequestAckMessage} to a {@link JSONObject}. * @throws JSONException */ static JSONObject toJson(PairingRequestAckMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); if (message.hasServerName()) { jsonObj.put(PAIRING_REQUEST_ACK_FIELD_SERVER_NAME, message.getServerName()); } return jsonObj; } /** * Translates a {@link OptionsMessage} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(OptionsMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONArray inEncsArray = new JSONArray(); for (EncodingOption encoding : message.getInputEncodingSet()) { inEncsArray.put(toJson(encoding)); } jsonObj.put(OPTIONS_FIELD_INPUT_ENCODINGS, inEncsArray); JSONArray outEncsArray = new JSONArray(); for (EncodingOption encoding : message.getOutputEncodingSet()) { outEncsArray.put(toJson(encoding)); } jsonObj.put(OPTIONS_FIELD_OUTPUT_ENCODINGS, outEncsArray); int intRole = message.getProtocolRolePreference().getAsInt(); jsonObj.put(OPTIONS_FIELD_PREFERRED_ROLE, intRole); return jsonObj; } /** * Translates a {@link ConfigurationMessage} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(ConfigurationMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); JSONObject encoding = toJson(message.getEncoding()); jsonObj.put(CONFIG_FIELD_ENCODING, encoding); int intRole = message.getClientRole().getAsInt(); jsonObj.put(CONFIG_FIELD_CLIENT_ROLE, intRole); return jsonObj; } /** * Translates a {@link ConfigurationAckMessage} to a {@link JSONObject}. */ static JSONObject toJson(ConfigurationAckMessage message) { return new JSONObject(); } /** * Translates a {@link SecretMessage} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(SecretMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); String bytesStr; String charsetName = Charset.defaultCharset().name(); try { bytesStr = new String(Base64.encode(message.getSecret(), charsetName)); } catch (UnsupportedEncodingException e) { // Should never happen. bytesStr = ""; } jsonObj.put(SECRET_FIELD_SECRET, bytesStr); return jsonObj; } /** * Translates a {@link SecretAckMessage} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(SecretAckMessage message) throws JSONException { JSONObject jsonObj = new JSONObject(); String bytesStr; String charsetName = Charset.defaultCharset().name(); try { bytesStr = new String(Base64.encode(message.getSecret(), charsetName)); } catch (UnsupportedEncodingException e) { // Should never happen. bytesStr = ""; } jsonObj.put(SECRET_ACK_FIELD_SECRET, bytesStr); return jsonObj; } /** * Translates a {@link EncodingOption} to a {@link JSONObject}. * * @throws JSONException on error generating the {@link JSONObject} */ static JSONObject toJson(EncodingOption encoding) throws JSONException { JSONObject result = new JSONObject(); int intType = encoding.getType().getAsInt(); result.put(ENCODING_FIELD_TYPE, intType); result.put(ENCODING_FIELD_SYMBOL_LENGTH, encoding.getSymbolLength()); return result; } }