• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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