GRASS 8 Programmer's Manual 8.6.0dev(2026)-1878fdfec5
Loading...
Searching...
No Matches
parser_json.c
Go to the documentation of this file.
1/*!
2 \file lib/gis/parser_json.c
3
4 \brief GIS Library - converts the command line arguments into actinia JSON
5 process chain building blocks
6
7 SPDX-FileCopyrightText: 2018-2021 GRASS Development Team
8 SPDX-License-Identifier: GPL-2.0-or-later
9
10 \author Soeren Gebbert
11 */
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <grass/glocale.h>
17#include <grass/gis.h>
18
19#include "parser_local_proto.h"
20
21void check_create_import_opts(struct Option *, char *, FILE *);
22void check_create_export_opts(struct Option *, char *, FILE *);
23char *check_mapset_in_layer_name(char *, int);
24static char *str_json_escape(const char *str);
25static char *str_replace_free_buffer(char *buffer, const char old_char,
26 const char *new_str);
27
28/*!
29 \brief This function generates actinia JSON process chain building blocks
30 from the command line arguments that can be used in the actinia processing
31 API.
32
33 The following commands will create according JSON output:
34
35 r.slope.aspect
36 elevation="elevation@https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif"
37 slope="slope+GTiff" aspect="aspect+GTiff" --json
38
39 {
40 "module": "r.slope.aspect",
41 "id": "r.slope.aspect_1804289383",
42 "inputs":[
43 {"import_descr":
44 {"source":"https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif",
45 "type":"raster"}, "param": "elevation", "value": "elevation"},
46 {"param": "format", "value": "degrees"},
47 {"param": "precision", "value": "FCELL"},
48 {"param": "zscale", "value": "1.0"},
49 {"param": "min_slope", "value": "0.0"}
50 ],
51 "outputs":[
52 {"export": {"format":"GTiff", "type":"raster"},
53 "param": "slope", "value": "slope"},
54 {"export": {"format":"GTiff", "type":"raster"},
55 "param": "aspect", "value": "aspect"}
56 ]
57 }
58
59 v.out.ascii input="hospitals@PERMANENT" output="myfile+TXT" --json
60
61 {
62 "module": "v.out.ascii",
63 "id": "v.out.ascii_1804289383",
64 "inputs":[
65 {"param": "input", "value": "hospitals@PERMANENT"},
66 {"param": "layer", "value": "1"},
67 {"param": "type", "value": "point,line,boundary,centroid,area,face,kernel"},
68 {"param": "format", "value": "point"},
69 {"param": "separator", "value": "pipe"},
70 {"param": "precision", "value": "8"}
71 ],
72 "outputs":[
73 {"export": {"format":"TXT", "type":"file"},
74 "param": "output", "value": "$file::myfile"}
75 ]
76 }
77
78 v.info map="hospitals@PERMANENT" -c --json
79
80 {
81 "module": "v.info",
82 "id": "v.info_1804289383",
83 "flags":"c",
84 "inputs":[
85 {"param": "map", "value": "hospitals@PERMANENT"},
86 {"param": "layer", "value": "1"}
87 ]
88 }
89
90
91 A process chain has the following form
92
93 {
94 'list': [{
95 'module': 'g.region',
96 'id': 'g_region_1',
97 'inputs': [{'import_descr': {'source':
98 'https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif', 'type':
99 'raster'}, 'param': 'raster', 'value': 'elev_ned_30m_new'}], 'flags': 'p'
100 },
101 {
102 'module': 'r.slope.aspect',
103 'id': 'r_slope_aspect_1',
104 'inputs': [{'param': 'elevation',
105 'value': 'elev_ned_30m_new'}],
106 'outputs': [{'export': {'format': 'GTiff',
107 'type': 'raster'},
108 'param': 'slope',
109 'value': 'elev_ned_30m_new_slope'}],
110 'flags': 'a'},
111 {
112 'module': 'r.univar',
113 'id': 'r_univar_1',
114 'inputs': [{"import_descr": {"source": "LT52170762005240COA00",
115 "type": "landsat",
116 "landsat_atcor": "dos1"},
117 'param': 'map',
118 'value': 'LT52170762005240COA00_dos1.1'}],
119 'stdout': {'id': 'stats', 'format': 'kv', 'delimiter': '='},
120 'flags': 'a'
121 },
122 {
123 'module': 'exporter',
124 'id': 'exporter_1',
125 'outputs': [{'export': {'format': 'GTiff',
126 'type': 'raster'},
127 'param': 'map',
128 'value': 'LT52170762005240COA00_dos1.1'}]
129 },
130 {
131 "id": "ascii_out",
132 "module": "r.out.ascii",
133 "inputs": [{"param": "input",
134 "value": "elevation@PERMANENT"},
135 {"param": "precision", "value": "0"}],
136 "stdout": {"id": "elev_1", "format": "table", "delimiter": " "},
137 "flags": "h"
138 },
139 {
140 "id": "ascii_export",
141 "module": "r.out.ascii",
142 "inputs": [{"param": "input",
143 "value": "elevation@PERMANENT"}],
144 "outputs": [
145 {"export": {"type": "file", "format": "TXT"},
146 "param": "output",
147 "value": "$file::out1"}
148 ]
149 },
150 {
151 "id": "raster_list",
152 "module": "g.list",
153 "inputs": [{"param": "type",
154 "value": "raster"}],
155 "stdout": {"id": "raster", "format": "list", "delimiter": "\n"}
156 },
157 {
158 "module": "r.what",
159 "id": "r_what_1",
160 "verbose": True,
161 "flags": "nfic",
162 "inputs": [
163 {
164 "param": "map",
165 "value": "landuse96_28m@PERMANENT"
166 },
167 {
168 "param": "coordinates",
169 "value": "633614.08,224125.12,632972.36,225382.87"
170 },
171 {
172 "param": "null_value",
173 "value": "null"
174 },
175 {
176 "param": "separator",
177 "value": "pipe"
178 }
179 ],
180 "stdout": {"id": "sample", "format": "table", "delimiter": "|"}
181 }
182 ],
183 'webhooks': {'update':
184 'http://business-logic.company.com/api/v1/actinia-update-webhook',
185 'finished':
186 'http://business-logic.company.com/api/v1/actinia-finished-webhook'},
187 'version': '1'
188 }
189
190 */
191char *G__json(void)
192{
193 FILE *fp = stdout;
194
195 /*FILE *fp = NULL; */
196 char *file_name = NULL;
197 int c;
198 int random_int = rand();
199 int num_flags = 0;
200 int num_inputs = 0;
201 int num_outputs = 0;
202 int i = 0;
203
204 char age[KEYLENGTH];
205 char element[KEYLENGTH]; /*cell, file, grid3, vector */
206 char desc[KEYLENGTH];
207
208 file_name = G_tempfile();
209
210 /* fprintf(stderr, "Filename: %s\n", file_name); */
211 fp = fopen(file_name, "w+");
212 if (fp == NULL) {
213 fprintf(stderr, "Unable to open temporary file <%s>\n", file_name);
215 }
216
217 if (st->n_flags) {
218 struct Flag *flag;
219
220 for (flag = &st->first_flag; flag; flag = flag->next_flag) {
221 if (flag->answer)
222 num_flags += 1;
223 ;
224 }
225 }
226
227 /* Count input and output options */
228 if (st->n_opts) {
229 struct Option *opt;
230
231 for (opt = &st->first_option; opt; opt = opt->next_opt) {
232 if (opt->answer) {
233 if (opt->gisprompt) {
234 G__split_gisprompt(opt->gisprompt, age, element, desc);
235 /* fprintf(stderr, "age: %s element: %s desc: %s\n", age,
236 * element, desc); */
237 if (G_strncasecmp("new", age, 3) == 0) {
238 /*fprintf(fp, "new: %s\n", opt->gisprompt); */
239 num_outputs += 1;
240 }
241 else {
242 /*fprintf(fp, "%s\n", opt->gisprompt); */
243 num_inputs += 1;
244 }
245 }
246 else {
247 num_inputs += 1;
248 }
249 }
250 }
251 }
252
253 fprintf(fp, "{\n");
254 fprintf(fp, " \"module\": \"%s\",\n", G_program_name());
255 fprintf(fp, " \"id\": \"%s_%i\"", G_program_name(), random_int);
256
257 if (st->n_flags && num_flags > 0) {
258 struct Flag *flag;
259
260 fprintf(fp, ",\n");
261 fprintf(fp, " \"flags\":\"");
262
263 for (flag = &st->first_flag; flag; flag = flag->next_flag) {
264 if (flag->answer)
265 fprintf(fp, "%c", flag->key);
266 }
267 fprintf(fp, "\"");
268 }
269
270 /* Print the input options
271 */
272 if (st->n_opts && num_inputs > 0) {
273 struct Option *opt;
274
275 i = 0;
276 fprintf(fp, ",\n");
277 fprintf(fp, " \"inputs\":[\n");
278 for (opt = &st->first_option; opt; opt = opt->next_opt) {
279 if (opt->gisprompt) {
280 G__split_gisprompt(opt->gisprompt, age, element, desc);
281 if (G_strncasecmp("new", age, 3) != 0) {
282 if (opt->answer) {
284 i++;
285 if (i < num_inputs) {
286 fprintf(fp, ",\n");
287 }
288 else {
289 fprintf(fp, "\n");
290 }
291 }
292 }
293 }
294 else if (opt->answer) {
295 /* Check for input options */
296 fprintf(fp, " {\"param\": \"%s\", ", opt->key);
297 char *escaped_value = str_json_escape(opt->answer);
298 fprintf(fp, "\"value\": \"%s\"}", escaped_value);
300 i++;
301 if (i < num_inputs) {
302 fprintf(fp, ",\n");
303 }
304 else {
305 fprintf(fp, "\n");
306 }
307 }
308 }
309 fprintf(fp, " ]");
310 }
311
312 /* Print the output options
313 */
314 if (st->n_opts && num_outputs > 0) {
315 struct Option *opt;
316
317 i = 0;
318 fprintf(fp, ",\n");
319 fprintf(fp, " \"outputs\":[\n");
320 for (opt = &st->first_option; opt; opt = opt->next_opt) {
321 if (opt->gisprompt) {
322 G__split_gisprompt(opt->gisprompt, age, element, desc);
323 if (G_strncasecmp("new", age, 3) == 0) {
324 if (opt->answer) {
326 i++;
327 if (i < num_outputs) {
328 fprintf(fp, ",\n");
329 }
330 else {
331 fprintf(fp, "\n");
332 }
333 }
334 }
335 }
336 }
337 fprintf(fp, " ]\n");
338 }
339
340 fprintf(fp, "}\n");
341 fclose(fp);
342
343 /* Print the file content to stdout */
344 fp = fopen(file_name, "r");
345 if (fp == NULL) {
346 fprintf(stderr, "Unable to open temporary file <%s>\n", file_name);
348 }
349
350 c = fgetc(fp);
351 while (c != EOF) {
352 fprintf(stdout, "%c", c);
353 c = fgetc(fp);
354 }
355 fclose(fp);
356
357 return file_name;
358}
359
360/* \brief Check the provided answer and generate the import statement
361 dependent on the element type (cell, vector, grid3, file)
362
363 {'import_descr': {'source':
364 'https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif', 'type':
365 'raster'}, 'param': 'map', 'value': 'elevation'}
366 */
368{
369 int i = 0, urlfound = 0;
370 int has_import = 0;
371 char **tokens;
372
373 G_debug(2, "tokenize opt string: <%s> with '@'", opt->answer);
374 tokens = G_tokenize(opt->answer, "@");
375 while (tokens[i]) {
376 G_chop(tokens[i]);
377 i++;
378 }
379 if (i > 2)
381 _("Input string not understood: <%s>. Multiple '@' chars?"),
382 opt->answer);
383
384 if (i > 1) {
385 /* check if tokens[1] starts with an URL or name@mapset */
386 G_debug(2, "tokens[1]: <%s>", tokens[1]);
387 if (strncmp(tokens[1], "http://", 7) == 0 ||
388 strncmp(tokens[1], "https://", 8) == 0 ||
389 strncmp(tokens[1], "ftp://", 6) == 0) {
390 urlfound = 1;
391 G_debug(2, "URL found");
392 }
393 else {
394 urlfound = 0;
395 G_debug(2, "name@mapset found");
396 }
397 }
398
399 fprintf(fp, " {");
400
401 if (i > 1 && urlfound == 1) {
402 if (G_strncasecmp("cell", element, 4) == 0) {
403 fprintf(fp,
404 "\"import_descr\": {\"source\":\"%s\", "
405 "\"type\":\"raster\"},\n ",
406 tokens[1]);
407 has_import = 1;
408 }
409 else if (G_strncasecmp("file", element, 4) == 0) {
410 fprintf(fp,
411 "\"import_descr\": {\"source\":\"%s\", "
412 "\"type\":\"file\"},\n ",
413 tokens[1]);
414 has_import = 1;
415 }
416 else if (G_strncasecmp("vector", element, 4) == 0) {
417 fprintf(fp,
418 "\"import_descr\": {\"source\":\"%s\", "
419 "\"type\":\"vector\"},\n ",
420 tokens[1]);
421 has_import = 1;
422 }
423 }
424
425 fprintf(fp, "\"param\": \"%s\", ", opt->key);
426 /* In case of import the mapset must be removed always */
427 if (urlfound == 1) {
428 char *escaped_value =
429 str_json_escape(check_mapset_in_layer_name(tokens[0], has_import));
430 fprintf(fp, "\"value\": \"%s\"", escaped_value);
432 }
433 else {
434 char *escaped_value = str_json_escape(
436 fprintf(fp, "\"value\": \"%s\"", escaped_value);
438 };
439 fprintf(fp, "}");
440
442}
443
444/* \brief Check the provided answer and generate the export statement
445 dependent on the element type (cell, vector, grid3, file)
446
447 "outputs": [
448 {"export": {"type": "file", "format": "TXT"},
449 "param": "output",
450 "value": "$file::out1"},
451 {'export': {'format': 'GTiff', 'type': 'raster'},
452 'param': 'map',
453 'value': 'LT52170762005240COA00_dos1.1'}
454 ]
455 */
457{
458 int i = 0;
459 int has_file_export = 0;
460 char **tokens;
461
462 tokens = G_tokenize(opt->answer, "+");
463 while (tokens[i]) {
464 G_chop(tokens[i]);
465 i++;
466 }
467
468 fprintf(fp, " {");
469
470 if (i > 1) {
471 if (G_strncasecmp("cell", element, 4) == 0) {
472 fprintf(
473 fp,
474 "\"export\": {\"format\":\"%s\", \"type\":\"raster\"},\n ",
475 tokens[1]);
476 }
477 else if (G_strncasecmp("file", element, 4) == 0) {
478 fprintf(
479 fp,
480 "\"export\": {\"format\":\"%s\", \"type\":\"file\"},\n ",
481 tokens[1]);
482 has_file_export = 1;
483 }
484 else if (G_strncasecmp("vector", element, 4) == 0) {
485 fprintf(
486 fp,
487 "\"export\": {\"format\":\"%s\", \"type\":\"vector\"},\n ",
488 tokens[1]);
489 }
490 }
491
492 fprintf(fp, "\"param\": \"%s\", ", opt->key);
493 if (has_file_export == 1) {
494 char *escaped_value =
495 str_json_escape(check_mapset_in_layer_name(tokens[0], 1));
496 fprintf(fp, "\"value\": \"$file::%s\"", escaped_value);
498 }
499 else {
500 char *escaped_value =
501 str_json_escape(check_mapset_in_layer_name(tokens[0], 1));
502 fprintf(fp, "\"value\": \"%s\"", escaped_value);
504 }
505 fprintf(fp, "}");
506
508}
509
510/*
511 \brief Check if the current mapset is present in the layer name and remove it
512
513 The flag always_remove tells this function to always remove all mapset names.
514
515 \return pointer to the layer name without the current mapset
516 */
517char *check_mapset_in_layer_name(char *layer_name, int always_remove)
518{
519 int i = 0;
520 char **tokens;
521 const char *mapset;
522
523 mapset = G_mapset();
524
525 tokens = G_tokenize(layer_name, "@");
526
527 while (tokens[i]) {
528 G_chop(tokens[i]);
529 /* fprintf(stderr, "Token %i: %s\n", i, tokens[i]); */
530 i++;
531 }
532
533 if (always_remove == 1)
534 return tokens[0];
535
536 if (i > 1 && G_strcasecmp(mapset, tokens[1]) == 0)
537 return tokens[0];
538
539 return layer_name;
540}
541
542/** \brief Replace *old_char* with *new_str* and free the *buffer* if needed
543 *
544 * Replaces *old_char* (one character) with *new_str* (a string) in *buffer*
545 * and returns the new string. If no replacements were made, returns pointer
546 * to the original *buffer* unchanged.
547 *
548 * In any case, returned string should be freed with G_free(), while
549 * the original *buffer* should not be freed. The *buffer* must be a heap
550 * allocated string.
551 *
552 * \param buffer the string to make replacements in
553 * \param old_char the character to replace
554 * \param new_str the replacement string
555 *
556 * \return a newly allocated string or *buffer* if no replacements were made
557 */
558static char *str_replace_free_buffer(char *buffer, const char old_char,
559 const char *new_str)
560{
561 // Check if the input string contains the character to replace to avoid
562 // allocation in G_str_replace.
563 if (strchr(buffer, old_char)) {
564 // The function takes strings, not chars.
565 char old_str[2] = {old_char, '\0'};
566 char *res = G_str_replace(buffer, old_str, new_str);
567 G_free(buffer);
568 return res;
569 }
570 return buffer;
571}
572
573/** \brief Escape a string for JSON.
574 *
575 * \param str the string to escape
576 *
577 * \return a newly allocated string that must be freed with G_free()
578 */
579static char *str_json_escape(const char *str)
580{
581 // Always duplicate the input string for code simplicity.
582 char *out = G_store(str);
583 // We test character presence to avoid duplicating the input string
584 // for every potential replacement.
585 // While formfeed and backspace are unlikely, we check for them to
586 // ensure valid JSON.
587 out = str_replace_free_buffer(out, '\\', "\\\\");
588 out = str_replace_free_buffer(out, '\r', "\\r");
589 out = str_replace_free_buffer(out, '\n', "\\n");
590 out = str_replace_free_buffer(out, '\t', "\\t");
591 out = str_replace_free_buffer(out, '\"', "\\\"");
592 out = str_replace_free_buffer(out, '\f', "\\f");
593 out = str_replace_free_buffer(out, '\b', "\\b");
594 return out;
595}
#define NULL
Definition ccmath.h:32
const char * G_program_name(void)
Return module name.
Definition progrm_nme.c:26
void G_free(void *)
Free allocated memory.
Definition gis/alloc.c:145
void void void void G_fatal_error(const char *,...) __attribute__((format(printf
char ** G_tokenize(const char *, const char *)
Tokenize string.
Definition gis/token.c:45
char * G_tempfile(void)
Returns a temporary file name.
Definition tempfile.c:60
void G_free_tokens(char **)
Free memory allocated to tokens.
Definition gis/token.c:195
int int G_strcasecmp(const char *, const char *)
String compare ignoring case (upper or lower)
Definition strings.c:45
char * G_chop(char *)
Chop leading and trailing white spaces.
Definition strings.c:330
char * G_store(const char *)
Copy string to allocated memory.
Definition strings.c:85
int G_strncasecmp(const char *, const char *, int)
String compare ignoring case (upper or lower) - limited number of characters.
Definition strings.c:67
char * G_str_replace(const char *, const char *, const char *)
Replace all occurrences of old_str in buffer with new_str.
Definition strings.c:187
int G_debug(int, const char *,...) __attribute__((format(printf
const char * G_mapset(void)
Get current mapset name.
Definition gis/mapset.c:31
#define _(str)
Definition glocale.h:10
struct state * st
Definition parser.c:102
void G__split_gisprompt(const char *gisprompt, char *age, char *element, char *desc)
Definition parser.c:1773
void check_create_import_opts(struct Option *, char *, FILE *)
void check_create_export_opts(struct Option *, char *, FILE *)
char * check_mapset_in_layer_name(char *, int)
char * G__json(void)
This function generates actinia JSON process chain building blocks from the command line arguments th...
Structure that stores flag info.
Definition gis.h:591
Structure that stores option information.
Definition gis.h:560