/
JsonUtils.java
411 lines (376 loc) · 14.3 KB
/
JsonUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
package org.opengis.cite.ogcapiprocesses10.util;
import static io.restassured.RestAssured.given;
import static io.restassured.http.Method.GET;
import static org.opengis.cite.ogcapiprocesses10.OgcApiProcesses10.GEOJSON_MIME_TYPE;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.Period;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.path.json.JsonPath;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
/**
* @author <a href="mailto:goltz@lat-lon.de">Lyn Goltz </a>
*/
public class JsonUtils {
private static ObjectMapper objectMapper;
private JsonUtils() {
}
/**
* Parses the id of the first feature from the passed json.
* @param collectionItemJson the json document containing the features, never
* <code>null</code>
* @return the parsed id, may be <code>null</code> if no feature could be found
*/
public static String parseFeatureId(JsonPath collectionItemJson) {
List<Map<String, Object>> features = collectionItemJson.get("features");
if (features == null)
return null;
for (Map<String, Object> feature : features) {
if (feature.containsKey("id"))
return feature.get("id").toString();
}
return null;
}
/**
* Parses the temporal extent from the passed collection.
* @param collection the collection containing the extent to parse, never
* <code>null</code>
* @return the parsed temporal extent, <code>null</code> if no extent exists
* @throws IllegalArgumentException if the number of items in the extent invalid
*
*/
public static TemporalExtent parseTemporalExtent(Map<String, Object> collection) {
Object extent = collection.get("extent");
if (extent == null || !(extent instanceof Map))
return null;
Object spatial = ((Map<String, Object>) extent).get("temporal");
if (spatial == null || !(spatial instanceof List))
return null;
List<Object> coords = (List<Object>) spatial;
if (coords.size() == 2) {
ZonedDateTime begin = parseAsDate((String) coords.get(0));
ZonedDateTime end = parseAsDate((String) coords.get(1));
return new TemporalExtent(begin, end);
}
throw new IllegalArgumentException("Temporal extent with " + coords.size() + " items is invalid");
}
/**
* Parses the passed string as ISO 8601 date.
* @param dateTime the dateTime to parse, never <code>null</code>
* @return the parsed date, never <code>null</code>
*/
public static ZonedDateTime parseAsDate(String dateTime) {
return ZonedDateTime.parse(dateTime);
}
/**
* Formats the passed string as ISO 8601 date. Example: "2018-02-12T23:20:50Z"
* @param dateTime the dateTime to format, never <code>null</code>
* @return the formatted date, never <code>null</code>
*/
public static String formatDate(ZonedDateTime dateTime) {
return DateTimeFormatter.ISO_INSTANT.format(dateTime);
}
/**
* Formats the passed string as ISO 8601 date. Example: "2018-02-12"
* @param date the dateTime to format, never <code>null</code>
* @return the formatted date, never <code>null</code>
*/
public static String formatDate(LocalDate date) {
return DateTimeFormatter.ISO_DATE.format(date);
}
/**
* Formats the passed string as a period using a start and end time. Example:
* "2018-02-12T00:00:00Z/2018-03-18T12:31:12Z"
* @param beginDateTime the begin dateTime to format, never <code>null</code>
* @param endDateTime the end dateTime to format, never <code>null</code>
* @return the formatted date, never <code>null</code>
*/
public static String formatDateRange(ZonedDateTime beginDateTime, ZonedDateTime endDateTime) {
return formatDate(beginDateTime) + "/" + formatDate(endDateTime);
}
/**
* Formats the passed string as a period using start time and a duration. Example:
* "2018-02-12T00:00:00Z/P1M6DT12H31M12S"
* @param beginDate the begin date to format, never <code>null</code>
* @param endDate the end date to format, never <code>null</code>
* @return the formatted date, never <code>null</code>
*/
public static String formatDateRangeWithDuration(LocalDate beginDate, LocalDate endDate) {
Period betweenDate = Period.between(beginDate, endDate);
return formatDate(beginDate) + "/" + betweenDate;
}
/**
* Parses the spatial extent from the passed collection.
* @param collection the collection containing the extent to parse, never
* <code>null</code>
* @return the parsed bbox, <code>null</code> if no extent exists
* @throws IllegalArgumentException if the number of items in the extent invalid
*
*/
public static BBox parseSpatialExtent(Map<String, Object> collection) {
Object extent = collection.get("extent");
if (extent == null || !(extent instanceof Map))
return null;
Object spatial = ((Map<String, Object>) extent).get("spatial");
if (spatial == null || !(spatial instanceof List))
return null;
List<Object> coords = (List<Object>) spatial;
if (coords.size() == 4) {
double minX = parseValueAsDouble(coords.get(0));
double minY = parseValueAsDouble(coords.get(1));
double maxX = parseValueAsDouble(coords.get(2));
double maxY = parseValueAsDouble(coords.get(3));
return new BBox(minX, minY, maxX, maxY);
}
else if (coords.size() == 6) {
throw new IllegalArgumentException(
"BBox with " + coords.size() + " coordinates is currently not supported");
}
throw new IllegalArgumentException("BBox with " + coords.size() + " coordinates is invalid");
}
/**
* Parses all links with 'type' of one of the passed mediaTypes and the 'rel' property
* with the passed value.
* @param links list of all links, never <code>null</code>
* @param mediaTypesToSupport a list of media types the links searched for should
* support, may be empty but never <code>null</code>
* @param expectedRel the expected value of the property 'rel', never
* <code>null</code>
* @return a list of links supporting one of the media types and with the expected
* 'rel' property, may be empty but never <code>null</code>
*/
public static List<Map<String, Object>> findLinksWithSupportedMediaTypeByRel(List<Map<String, Object>> links,
List<String> mediaTypesToSupport, String expectedRel) {
List<Map<String, Object>> alternateLinks = new ArrayList<>();
for (Map<String, Object> link : links) {
Object type = link.get("type");
Object rel = link.get("rel");
if (expectedRel.equals(rel) && isSupportedMediaType(type, mediaTypesToSupport))
alternateLinks.add(link);
}
return alternateLinks;
}
/**
* Parsing the media types which does not have a link woth property 'type' for.
* @param links list of links to search in, never <code>null</code>
* @param mediaTypesToSuppport a list of media types which should be supported, never
* <code>null</code>
* @return the media types which does not have a link for.
*/
public static List<String> findUnsupportedTypes(List<Map<String, Object>> links,
List<String> mediaTypesToSuppport) {
List<String> unsupportedType = new ArrayList<>();
for (String contentMediaType : mediaTypesToSuppport) {
boolean hasLinkForContentType = hasLinkForContentType(links, contentMediaType);
if (!hasLinkForContentType)
unsupportedType.add(contentMediaType);
}
return unsupportedType;
}
/**
* Parses the links without 'rel' or 'type' property.
* @param links list of links to search in, never <code>null</code>
* @param rels rels <code>null</code>
* @return the links without 'rel' or 'type' property
*/
public static List<String> findLinksWithoutRelOrType(List<Map<String, Object>> links, Set<String> rels) {
List<String> linksWithoutRelOrType = new ArrayList<>();
for (Map<String, Object> link : links) {
if (rels.contains(link.get("rel")) && !linkIncludesRelAndType(link))
linksWithoutRelOrType.add((String) link.get("href"));
}
return linksWithoutRelOrType;
}
/**
* Parses the link with 'rel=self'.
* @param links list of links to search in, never <code>null</code>
* @param expectedRel the expected value of the property 'rel', never
* <code>null</code>
* @return the link to itself or <code>null</code> if no such link exists
*/
public static Map<String, Object> findLinkByRel(List<Map<String, Object>> links, String expectedRel) {
if (links == null)
return null;
for (Map<String, Object> link : links) {
Object rel = link.get("rel");
if (expectedRel.equals(rel))
return link;
}
return null;
}
/**
* Checks if the passed link contains 'rel' and 'type' properties.
* @param link to check, never <code>null</code>
* @return <code>true</code> if the link contains 'rel' and 'type' properties,
* <code>false</code> otherwise
*/
public static boolean linkIncludesRelAndType(Map<String, Object> link) {
Object rel = link.get("rel");
Object type = link.get("type");
if (rel != null && type != null)
return true;
return false;
}
/**
* Checks if a property with the passed name exists in the jsonPath.
* @param propertyName name of the property to check, never <code>null</code>
* @param jsonPath to check, never <code>null</code>
* @return <code>true</code> if the property exists, <code>false</code> otherwise
*/
public static boolean hasProperty(String propertyName, JsonPath jsonPath) {
return jsonPath.get(propertyName) != null;
}
/**
* Collects the number of all returned features by iterating over all 'next' links and
* summarizing the size of features in 'features' array property.
* @param jsonPath the initial collection, never <code>null</code>
* @param maximumLimit the limit parameter value to use, if <= 0 the parameter is
* omitted
* @return the number of all returned features
* @throws URISyntaxException if the creation of a uri fails
*/
public static int collectNumberOfAllReturnedFeatures(JsonPath jsonPath, int maximumLimit)
throws URISyntaxException {
int numberOfAllReturnedFeatures = jsonPath.getList("features").size();
Map<String, Object> nextLink = findLinkByRel(jsonPath.getList("links"), "next");
while (nextLink != null) {
String nextUrl = (String) nextLink.get("href");
URI uri = new URI(nextUrl);
RequestSpecification accept = given().log().all().baseUri(nextUrl).accept(GEOJSON_MIME_TYPE);
String[] pairs = uri.getQuery().split("&");
String limitParamFromUri = null;
for (String pair : pairs) {
int idx = pair.indexOf("=");
String key = pair.substring(0, idx);
String value = pair.substring(idx + 1);
if ("limit".equals(key)) {
limitParamFromUri = value;
}
else {
accept.param(key, value);
}
}
if (maximumLimit > 0) {
accept.param("limit", maximumLimit);
}
else if (limitParamFromUri != null) {
accept.param("limit", limitParamFromUri);
}
Response response = accept.when().request(GET);
response.then().statusCode(200);
JsonPath nextJsonPath = response.jsonPath();
int features = nextJsonPath.getList("features").size();
if (features > 0) {
numberOfAllReturnedFeatures += features;
nextLink = findLinkByRel(nextJsonPath.getList("links"), "next");
}
else {
nextLink = null;
}
}
return numberOfAllReturnedFeatures;
}
/**
* Converts an inputstream to String using UTF-8 encoding.
* @param inputStream the inputstream
* @return the content of the inputstream as String
* @throws IOException if an I/O error occurs
*/
public static String inputStreamToString(InputStream inputStream) throws IOException {
StringWriter writer = new StringWriter();
String encoding = StandardCharsets.UTF_8.name();
IOUtils.copy(inputStream, writer, encoding);
return prettifyString(writer.toString());
}
public static String prettifyString(String string) {
ObjectMapper objectMapper = getObjectMapper();
try {
Object jsonObject = objectMapper.readValue(string, Object.class);
String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
return prettyJson;
}
catch (Exception e) {
// String was not JSON
return string;
}
}
private static ObjectMapper getObjectMapper() {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
}
return objectMapper;
}
private static boolean isSameMediaType(String mediaType1, String mediaType2) {
if (mediaType1.contains(";") || mediaType2.contains(";")) {
// media types are not case sensitive
String[] components1 = mediaType1.toLowerCase().split(";");
String[] components2 = mediaType2.toLowerCase().split(";");
// type and subtype must match
if (!components1[0].trim().equals(components2[0].trim()))
return false;
Set<String> parameters1 = new HashSet<>();
Set<String> parameters2 = new HashSet<>();
// normalize parameter values and compare them
for (int i = 1; i < components1.length; i++) {
String parameter = components1[i].trim().replace("\"", "");
if (!parameter.isEmpty())
parameters1.add(parameter);
}
for (int i = 1; i < components2.length; i++) {
String parameter = components2[i].trim().replace("\"", "");
if (!parameter.isEmpty())
parameters2.add(parameter);
}
if (parameters1.size() != parameters2.size())
return false;
if (!parameters1.containsAll(parameters2))
return false;
}
else if (!mediaType1.trim().equalsIgnoreCase(mediaType2.trim()))
return false;
return true;
}
private static boolean hasLinkForContentType(List<Map<String, Object>> alternateLinks, String mediaType) {
for (Map<String, Object> alternateLink : alternateLinks) {
Object type = alternateLink.get("type");
if (type instanceof String && isSameMediaType(mediaType, (String) type))
return true;
}
return false;
}
private static boolean isSupportedMediaType(Object type, List<String> mediaTypes) {
for (String mediaType : mediaTypes) {
if (type instanceof String && isSameMediaType(mediaType, (String) type))
return true;
}
return false;
}
private static double parseValueAsDouble(Object cords) {
if (cords instanceof Integer) {
return ((Integer) cords).doubleValue();
}
else if (cords instanceof Float) {
return ((Float) cords).doubleValue();
}
else if (cords instanceof Double) {
return (Double) cords;
}
else {
return Double.parseDouble(cords.toString());
}
}
}