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