View Javadoc
1   package edu.jiangxin.apktoolbox.convert.protobuf.supervised;
2   
3   import com.google.protobuf.Descriptors;
4   import com.google.protobuf.DynamicMessage;
5   import com.google.protobuf.InvalidProtocolBufferException;
6   import com.google.protobuf.util.JsonFormat;
7   
8   import java.io.ByteArrayInputStream;
9   import java.io.ByteArrayOutputStream;
10  import java.io.IOException;
11  import java.io.UncheckedIOException;
12  import java.nio.file.Files;
13  import java.nio.file.Path;
14  import java.util.Comparator;
15  import java.util.Objects;
16  import java.util.Optional;
17  import java.util.zip.InflaterInputStream;
18  
19  /**
20   * API to decode binary protobuf messages to readable JSON, based on protobuf descriptors given as {@link
21   * DescriptorCache}.
22   * <p>
23   * Instances can be created using the static factory methods {@link #fromCache(DescriptorCache)} and {@link
24   * #fromEmptyCache()}. In order to add additional descriptors to the cache, it can be get using {@link #getCache()}.
25   * <p>
26   * Conversion can be done using one of the various {@code toJson} overloads, for example {@link #toJson(Path)}. Variants
27   * accepting a {@code String messageTypeName}, like {@link #toJson(Path, String)} will use a descriptor suitable for the
28   * given message type. Other variants will attempt to automatically find a best match among all available descriptors.
29   * <p>
30   * The conversion methods throw
31   * <ul>
32   *     <li>{@link UncheckedIOException} if an I/O error occurs during conversion,</li>
33   *     <li>{@link NoDescriptorFoundException} if no suitable descriptor could be found and</li>
34   *     <li>{@link UncheckedInvalidProtocolBufferException} if a message could not be parsed with the demanded descriptor.</li>
35   * </ul>
36   * <p>
37   * Usage example:
38   * <pre>{@code
39   * DescriptorCache cache = DescriptorCache.fromDirectory(Path.of("descriptorCache"));
40   * ProtoToJson protoToJson = ProtoToJson.fromCache(cache);
41   *
42   * String json = protoToJson.toJson(Path.of("someProtoMessage.message"));
43   * }</pre>
44   * Descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for example:
45   * <pre>{@code
46   * protoc --descriptor_set_out foo.desc foo.proto
47   * }</pre>
48   *
49   * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>}
50   */
51  public final class ProtoToJson {
52  	/**
53  	 * Creates an API from a given descriptor cache.
54  	 *
55  	 * @param cache The cache to use, not null
56  	 *
57  	 * @return The API instance
58  	 */
59  	public static ProtoToJson fromCache(final DescriptorCache cache) {
60  		Objects.requireNonNull(cache);
61  
62  		return new ProtoToJson(cache);
63  	}
64  
65  	/**
66  	 * Creates an API from an empty descriptor cache. The cache can be get using {@link #getCache()}.
67  	 *
68  	 * @return The API instance
69  	 */
70  	public static ProtoToJson fromEmptyCache() {
71  		return new ProtoToJson(DescriptorCache.emptyCache());
72  	}
73  
74  	/**
75  	 * Decompresses the given data using ZLib (see {@link InflaterInputStream}).
76  	 *
77  	 * @param compressed The data to decompress, not null
78  	 *
79  	 * @return The decompressed data
80  	 *
81  	 * @throws IOException If an I/O error occurs during the decompression or if the given data is not ZLib encoded
82  	 */
83  	private static byte[] decompressZLib(final byte[] compressed) throws IOException {
84  		Objects.requireNonNull(compressed);
85  		try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(compressed);
86  				final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
87  				final InflaterInputStream zLibOutputStream = new InflaterInputStream(inputStream)) {
88  			zLibOutputStream.transferTo(outputStream);
89  			return outputStream.toByteArray();
90  		}
91  	}
92  
93  	/**
94  	 * The cache used by the API, not null.
95  	 */
96  	private final DescriptorCache cache;
97  	/**
98  	 * The printer used to convert protobuf messages to JSOn, not null.
99  	 */
100 	private final JsonFormat.Printer printer = JsonFormat.printer();
101 
102 	/**
103 	 * Creates a new instance that uses the given descriptor cache.
104 	 *
105 	 * @param cache The cache to use, not null
106 	 */
107 	private ProtoToJson(final DescriptorCache cache) {
108 		this.cache = Objects.requireNonNull(cache);
109 	}
110 
111 	/**
112 	 * Gets the descriptor cache used by the API. For example, to add more descriptors to the cache.
113 	 *
114 	 * @return The cache used by the API
115 	 */
116 	public DescriptorCache getCache() {
117 		return cache;
118 	}
119 
120 	/**
121 	 * Converts the protobuf message given as a file to JSON.
122 	 * <p>
123 	 * The method will attempt to find a suitable descriptor to parse the message from the cache.
124 	 *
125 	 * @param messageFile Path to the protobuf message file, not null
126 	 *
127 	 * @return The JSON decoded message
128 	 *
129 	 * @throws NoDescriptorFoundException              If no suitable descriptor to parse the message could be found in
130 	 *                                                 the descriptor cache
131 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format
132 	 *                                                 expected by the descriptor
133 	 * @throws UncheckedIOException                    If an I/O error occurs during reading the file
134 	 */
135 	public String toJson(final Path messageFile) {
136 		Objects.requireNonNull(messageFile);
137 
138 		try {
139 			return toJson(Files.readAllBytes(messageFile), (String) null);
140 		} catch (final IOException e) {
141 			throw new UncheckedIOException(e);
142 		}
143 	}
144 
145 	/**
146 	 * Converts the given raw protobuf message to JSON.
147 	 * <p>
148 	 * The method will attempt to find a suitable descriptor to parse the message from the cache.
149 	 *
150 	 * @param messageRaw The raw protobuf message, not null
151 	 *
152 	 * @return The JSON decoded message
153 	 *
154 	 * @throws NoDescriptorFoundException              If no suitable descriptor to parse the message could be found in
155 	 *                                                 the descriptor cache
156 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format
157 	 *                                                 expected by the descriptor
158 	 */
159 	public String toJson(final byte[] messageRaw) {
160 		Objects.requireNonNull(messageRaw);
161 
162 		return toJson(messageRaw, (String) null);
163 	}
164 
165 	/**
166 	 * Converts the protobuf message given as a file to JSON.
167 	 * <p>
168 	 * If {@code messageTypeName} is null, it will attempt to find a suitable descriptor from the cache. Otherwise it
169 	 * will use a descriptor for the given message type.
170 	 *
171 	 * @param messageFile     Path to the protobuf message file, not null
172 	 * @param messageTypeName The name of the messages type or null if the API should pick a suitable descriptor itself
173 	 *
174 	 * @return The JSON decoded message
175 	 *
176 	 * @throws NoDescriptorFoundException              If no suitable descriptor to parse the message could be found in
177 	 *                                                 the descriptor cache
178 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format
179 	 *                                                 expected by the descriptor
180 	 * @throws UncheckedIOException                    If an I/O error occurs during reading the file
181 	 */
182 	public String toJson(final Path messageFile, final String messageTypeName) {
183 		Objects.requireNonNull(messageFile);
184 
185 		try {
186 			return toJson(Files.readAllBytes(messageFile), messageTypeName);
187 		} catch (final IOException e) {
188 			throw new UncheckedIOException(e);
189 		}
190 	}
191 
192 	/**
193 	 * Converts the given raw protobuf message to JSON.
194 	 * <p>
195 	 * If {@code messageTypeName} is null, it will attempt to find a suitable descriptor from the cache. Otherwise it
196 	 * will use a descriptor for the given message type.
197 	 *
198 	 * @param messageRaw      The raw protobuf message, not null
199 	 * @param messageTypeName The name of the messages type or null if the API should pick a suitable descriptor itself
200 	 *
201 	 * @return The JSON decoded message
202 	 *
203 	 * @throws NoDescriptorFoundException              If no suitable descriptor to parse the message could be found in
204 	 *                                                 the descriptor cache
205 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format
206 	 *                                                 expected by the descriptor
207 	 */
208 	@SuppressWarnings("WeakerAccess")
209 	public String toJson(final byte[] messageRaw, final String messageTypeName) {
210 		Objects.requireNonNull(messageRaw);
211 
212 		if (messageTypeName == null) {
213 			final Optional<DynamicMessage> message = parseWithBestMatchingDescriptor(messageRaw);
214 			return toJson(message.orElseThrow(NoDescriptorFoundException::new));
215 		}
216 
217 		final Optional<Descriptors.Descriptor> descriptor = cache.getByTypeName(messageTypeName);
218 		return toJson(messageRaw, descriptor.orElseThrow(() -> new NoDescriptorFoundException(messageTypeName)));
219 	}
220 
221 	/**
222 	 * Converts the given raw protobuf message to JSON using the given descriptor.
223 	 *
224 	 * @param messageRaw The raw protobuf message, not null
225 	 * @param descriptor The descriptor to use for parsing the message, not null
226 	 *
227 	 * @return The JSON decoded message
228 	 *
229 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format
230 	 *                                                 expected by the given descriptor
231 	 */
232 	@SuppressWarnings("WeakerAccess")
233 	public String toJson(final byte[] messageRaw, final Descriptors.Descriptor descriptor) {
234 		Objects.requireNonNull(messageRaw);
235 		Objects.requireNonNull(descriptor);
236 
237 		try {
238 			return toJson(DynamicMessage.parseFrom(descriptor, messageRaw));
239 		} catch (final InvalidProtocolBufferException e) {
240 			throw new UncheckedInvalidProtocolBufferException(e);
241 		}
242 	}
243 
244 	/**
245 	 * Converts the given protobuf message to JSON.
246 	 *
247 	 * @param message The protobuf message, not null
248 	 *
249 	 * @return The JSON decoded message
250 	 *
251 	 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format
252 	 */
253 	@SuppressWarnings("WeakerAccess")
254 	public String toJson(final DynamicMessage message) {
255 		Objects.requireNonNull(message);
256 
257 		try {
258 			return printer.print(message);
259 		} catch (final InvalidProtocolBufferException e) {
260 			throw new UncheckedInvalidProtocolBufferException(e);
261 		}
262 	}
263 
264 	/**
265 	 * Attempts to parse the given binary protobuf message with a suitable descriptor from the APIs descriptor cache.
266 	 *
267 	 * @param messageRaw The binary protobuf message to parse, not null
268 	 *
269 	 * @return The parsed protobuf message, if a suitable descriptor was found, otherwise empty
270 	 */
271 	private Optional<DynamicMessage> parseWithBestMatchingDescriptor(final byte[] messageRaw) {
272 		Objects.requireNonNull(messageRaw);
273 
274 		return cache.getDescriptors()
275 				.stream()
276 				.map(descriptor -> {
277 					try {
278 						return DynamicMessage.parseFrom(descriptor, messageRaw);
279 					} catch (final InvalidProtocolBufferException e) {
280 						//noinspection ReturnOfNull
281 						return null;
282 					}
283 				})
284 				.filter(Objects::nonNull)
285 				.min(Comparator.comparingInt(message -> message.getUnknownFields()
286 						.asMap()
287 						.size()));
288 	}
289 }