1 /* 2 * Copyright 2020 Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google LLC nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 package com.google.api.gax.httpjson; 31 32 import com.google.api.core.BetaApi; 33 import com.google.common.collect.ImmutableList; 34 import com.google.gson.JsonElement; 35 import com.google.gson.JsonParser; 36 import com.google.protobuf.InvalidProtocolBufferException; 37 import com.google.protobuf.Message; 38 import com.google.protobuf.TypeRegistry; 39 import com.google.protobuf.util.JsonFormat; 40 import com.google.protobuf.util.JsonFormat.Printer; 41 import java.io.IOException; 42 import java.io.Reader; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Map; 46 47 /** 48 * This class serializes/deserializes protobuf {@link Message} for REST interactions. It serializes 49 * requests protobuf messages into REST messages, splitting the message into the JSON request body, 50 * URL path parameters, and query parameters. It deserializes JSON responses into response protobuf 51 * message. 52 */ 53 @BetaApi 54 public class ProtoRestSerializer<RequestT extends Message> { 55 56 private final TypeRegistry registry; 57 ProtoRestSerializer(TypeRegistry registry)58 private ProtoRestSerializer(TypeRegistry registry) { 59 this.registry = registry; 60 } 61 62 /** Creates a new instance of ProtoRestSerializer. */ create()63 public static <RequestT extends Message> ProtoRestSerializer<RequestT> create() { 64 return create(TypeRegistry.getEmptyTypeRegistry()); 65 } 66 67 /** Creates a new instance of ProtoRestSerializer. */ create(TypeRegistry registry)68 static <RequestT extends Message> ProtoRestSerializer<RequestT> create(TypeRegistry registry) { 69 return new ProtoRestSerializer<>(registry); 70 } 71 72 /** 73 * Serializes the data from {@code message} to a JSON string. The implementation relies on 74 * protobuf native JSON formatter. 75 * 76 * @param message a message to serialize 77 * @param numericEnum a boolean flag that determine if enum values should be serialized to number 78 * or not 79 * @throws InvalidProtocolBufferException if failed to serialize the protobuf message to JSON 80 * format 81 */ toJson(Message message, boolean numericEnum)82 String toJson(Message message, boolean numericEnum) { 83 try { 84 Printer printer = JsonFormat.printer().usingTypeRegistry(registry); 85 if (numericEnum) { 86 return printer.printingEnumsAsInts().print(message); 87 } else { 88 return printer.print(message); 89 } 90 } catch (InvalidProtocolBufferException e) { 91 throw new RestSerializationException("Failed to serialize message to JSON", e); 92 } 93 } 94 95 /** 96 * Deserializes a {@code message} from an input stream to a protobuf message. 97 * 98 * @param json the input reader with a JSON-encoded message in it 99 * @param builder an empty builder for the specific {@code RequestT} message to serialize 100 * @throws RestSerializationException if failed to deserialize a protobuf message from the JSON 101 * stream 102 */ 103 @SuppressWarnings("unchecked") fromJson(Reader json, Message.Builder builder)104 RequestT fromJson(Reader json, Message.Builder builder) { 105 try { 106 JsonFormat.parser().usingTypeRegistry(registry).ignoringUnknownFields().merge(json, builder); 107 return (RequestT) builder.build(); 108 } catch (IOException e) { 109 throw new RestSerializationException("Failed to parse response message", e); 110 } 111 } 112 113 /** 114 * Puts a message field in {@code fields} map which will be used to populate URL path of a 115 * request. 116 * 117 * @param fields a map with serialized fields 118 * @param fieldName a field name 119 * @param fieldValue a field value 120 */ putPathParam(Map<String, String> fields, String fieldName, Object fieldValue)121 public void putPathParam(Map<String, String> fields, String fieldName, Object fieldValue) { 122 fields.put(fieldName, String.valueOf(fieldValue)); 123 } 124 putDecomposedMessageQueryParam( Map<String, List<String>> fields, String fieldName, JsonElement parsed)125 private void putDecomposedMessageQueryParam( 126 Map<String, List<String>> fields, String fieldName, JsonElement parsed) { 127 if (parsed.isJsonPrimitive() || parsed.isJsonNull()) { 128 putQueryParam(fields, fieldName, parsed.getAsString()); 129 } else if (parsed.isJsonArray()) { 130 for (JsonElement element : parsed.getAsJsonArray()) { 131 putDecomposedMessageQueryParam(fields, fieldName, element); 132 } 133 } else { 134 // it is a json object 135 for (String key : parsed.getAsJsonObject().keySet()) { 136 putDecomposedMessageQueryParam( 137 fields, String.format("%s.%s", fieldName, key), parsed.getAsJsonObject().get(key)); 138 } 139 } 140 } 141 142 /** 143 * Puts a message field in {@code fields} map which will be used to populate query parameters of a 144 * request. 145 * 146 * @param fields a map with serialized fields 147 * @param fieldName a field name 148 * @param fieldValue a field value 149 */ putQueryParam(Map<String, List<String>> fields, String fieldName, Object fieldValue)150 public void putQueryParam(Map<String, List<String>> fields, String fieldName, Object fieldValue) { 151 List<String> currentParamValueList = new ArrayList<>(); 152 List<Object> toProcess = 153 fieldValue instanceof List<?> ? (List<Object>) fieldValue : ImmutableList.of(fieldValue); 154 for (Object fieldValueItem : toProcess) { 155 if (fieldValueItem instanceof Message) { 156 String json = toJson(((Message) fieldValueItem), true); 157 JsonElement parsed = JsonParser.parseString(json); 158 putDecomposedMessageQueryParam(fields, fieldName, parsed); 159 } else { 160 currentParamValueList.add(String.valueOf(fieldValueItem)); 161 } 162 } 163 if (currentParamValueList.isEmpty()) { 164 // We try to avoid putting non-leaf level fields to the query params 165 return; 166 } 167 List<String> accumulativeParamValueList = fields.getOrDefault(fieldName, new ArrayList<>()); 168 accumulativeParamValueList.addAll(currentParamValueList); 169 fields.put(fieldName, accumulativeParamValueList); 170 } 171 172 /** 173 * Serializes a message to a request body in a form of JSON-encoded string. 174 * 175 * @param fieldName a name of a request message field this message belongs to 176 * @param fieldValue a field value to serialize 177 */ toBody(String fieldName, RequestT fieldValue)178 public String toBody(String fieldName, RequestT fieldValue) { 179 return toJson(fieldValue, false); 180 } 181 182 /** 183 * Serializes a message to a request body in a form of JSON-encoded string. 184 * 185 * @param fieldName a name of a request message field this message belongs to 186 * @param fieldValue a field value to serialize 187 * @param numericEnum a boolean flag that determine if enum values should be serialized to number 188 */ toBody(String fieldName, RequestT fieldValue, boolean numericEnum)189 public String toBody(String fieldName, RequestT fieldValue, boolean numericEnum) { 190 return toJson(fieldValue, numericEnum); 191 } 192 } 193