1 /*!
2    \file lib/gis/parser_json.c
3 
4    \brief GIS Library - converts the command line arguments into actinia JSON process
5    chain building blocks
6 
7    (C) 2018-2021 by the GRASS Development Team
8 
9    This program is free software under the GNU General Public License
10    (>=v2). Read the file COPYING that comes with GRASS for details.
11 
12    \author Soeren Gebbert
13  */
14 
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <grass/glocale.h>
19 #include <grass/gis.h>
20 
21 #include "parser_local_proto.h"
22 
23 void check_create_import_opts(struct Option *, char *, FILE *);
24 void check_create_export_opts(struct Option *, char *, FILE *);
25 char *check_mapset_in_layer_name(char *, int);
26 
27 /*!
28    \brief This function generates actinia JSON process chain building blocks
29    from the command line arguments that can be used in the actinia processing API.
30 
31    The following commands will create according JSON output:
32 
33    r.slope.aspect elevation="elevation@https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif" slope="slope+GTiff" aspect="aspect+GTiff" --json
34 
35     {
36       "module": "r.slope.aspect",
37       "id": "r.slope.aspect_1804289383",
38       "inputs":[
39          {"import_descr": {"source":"https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif", "type":"raster"},
40           "param": "elevation", "value": "elevation"},
41          {"param": "format", "value": "degrees"},
42          {"param": "precision", "value": "FCELL"},
43          {"param": "zscale", "value": "1.0"},
44          {"param": "min_slope", "value": "0.0"}
45        ],
46       "outputs":[
47          {"export": {"format":"GTiff", "type":"raster"},
48           "param": "slope", "value": "slope"},
49          {"export": {"format":"GTiff", "type":"raster"},
50           "param": "aspect", "value": "aspect"}
51        ]
52     }
53 
54    v.out.ascii input="hospitals@PERMANENT" output="myfile+TXT" --json
55 
56     {
57       "module": "v.out.ascii",
58       "id": "v.out.ascii_1804289383",
59       "inputs":[
60          {"param": "input", "value": "hospitals@PERMANENT"},
61          {"param": "layer", "value": "1"},
62          {"param": "type", "value": "point,line,boundary,centroid,area,face,kernel"},
63          {"param": "format", "value": "point"},
64          {"param": "separator", "value": "pipe"},
65          {"param": "precision", "value": "8"}
66        ],
67       "outputs":[
68          {"export": {"format":"TXT", "type":"file"},
69           "param": "output", "value": "$file::myfile"}
70        ]
71     }
72 
73    v.info map="hospitals@PERMANENT" -c --json
74 
75     {
76       "module": "v.info",
77       "id": "v.info_1804289383",
78       "flags":"c",
79       "inputs":[
80          {"param": "map", "value": "hospitals@PERMANENT"},
81          {"param": "layer", "value": "1"}
82        ]
83     }
84 
85 
86    A process chain has the following form
87 
88 {
89     'list': [{
90         'module': 'g.region',
91         'id': 'g_region_1',
92         'inputs': [{'import_descr': {'source': 'https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif',
93                                      'type': 'raster'},
94                     'param': 'raster',
95                     'value': 'elev_ned_30m_new'}],
96         'flags': 'p'
97         },
98         {
99             'module': 'r.slope.aspect',
100             'id': 'r_slope_aspect_1',
101             'inputs': [{'param': 'elevation',
102                         'value': 'elev_ned_30m_new'}],
103             'outputs': [{'export': {'format': 'GTiff',
104                                     'type': 'raster'},
105                          'param': 'slope',
106                          'value': 'elev_ned_30m_new_slope'}],
107             'flags': 'a'},
108         {
109             'module': 'r.univar',
110             'id': 'r_univar_1',
111             'inputs': [{"import_descr": {"source": "LT52170762005240COA00",
112                                          "type": "landsat",
113                                          "landsat_atcor": "dos1"},
114                         'param': 'map',
115                         'value': 'LT52170762005240COA00_dos1.1'}],
116             'stdout': {'id': 'stats', 'format': 'kv', 'delimiter': '='},
117             'flags': 'a'
118         },
119         {
120             'module': 'exporter',
121             'id': 'exporter_1',
122             'outputs': [{'export': {'format': 'GTiff',
123                                     'type': 'raster'},
124                          'param': 'map',
125                          'value': 'LT52170762005240COA00_dos1.1'}]
126         },
127         {
128             "id": "ascii_out",
129             "module": "r.out.ascii",
130             "inputs": [{"param": "input",
131                         "value": "elevation@PERMANENT"},
132                        {"param": "precision", "value": "0"}],
133             "stdout": {"id": "elev_1", "format": "table", "delimiter": " "},
134             "flags": "h"
135         },
136         {
137             "id": "ascii_export",
138             "module": "r.out.ascii",
139             "inputs": [{"param": "input",
140                         "value": "elevation@PERMANENT"}],
141             "outputs": [
142                 {"export": {"type": "file", "format": "TXT"},
143                  "param": "output",
144                  "value": "$file::out1"}
145             ]
146         },
147         {
148             "id": "raster_list",
149             "module": "g.list",
150             "inputs": [{"param": "type",
151                         "value": "raster"}],
152             "stdout": {"id": "raster", "format": "list", "delimiter": "\n"}
153         },
154         {
155             "module": "r.what",
156             "id": "r_what_1",
157             "verbose": True,
158             "flags": "nfic",
159             "inputs": [
160                 {
161                     "param": "map",
162                     "value": "landuse96_28m@PERMANENT"
163                 },
164                 {
165                     "param": "coordinates",
166                     "value": "633614.08,224125.12,632972.36,225382.87"
167                 },
168                 {
169                     "param": "null_value",
170                     "value": "null"
171                 },
172                 {
173                     "param": "separator",
174                     "value": "pipe"
175                 }
176             ],
177             "stdout": {"id": "sample", "format": "table", "delimiter": "|"}
178         }
179     ],
180     'webhooks': {'update': 'http://business-logic.company.com/api/v1/actinia-update-webhook',
181                  'finished': 'http://business-logic.company.com/api/v1/actinia-finished-webhook'},
182     'version': '1'
183 }
184 
185 */
G__json(void)186 char *G__json(void)
187 {
188     FILE *fp = stdout;
189 
190     /*FILE *fp = NULL; */
191     char *type;
192     char *file_name = NULL;
193     int c;
194     int random_int = rand();
195     int num_flags = 0;
196     int num_inputs = 0;
197     int num_outputs = 0;
198     int i = 0;
199 
200     char age[KEYLENGTH];
201     char element[KEYLENGTH];    /*cell, file, grid3, vector */
202     char desc[KEYLENGTH];
203 
204     file_name = G_tempfile();
205 
206     /* fprintf(stderr, "Filename: %s\n", file_name); */
207     fp = fopen(file_name, "w+");
208     if (fp == NULL) {
209         fprintf(stderr, "Unable to open temporary file <%s>\n", file_name);
210         exit(EXIT_FAILURE);
211     }
212 
213     if (st->n_flags) {
214         struct Flag *flag;
215 
216         for (flag = &st->first_flag; flag; flag = flag->next_flag) {
217             if (flag->answer)
218                 num_flags += 1;;
219         }
220     }
221 
222     /* Count input and output options */
223     if (st->n_opts) {
224         struct Option *opt;
225 
226         for (opt = &st->first_option; opt; opt = opt->next_opt) {
227             if (opt->answer) {
228                 if (opt->gisprompt) {
229                     G__split_gisprompt(opt->gisprompt, age, element, desc);
230                     /* fprintf(stderr, "age: %s element: %s desc: %s\n", age, element, desc); */
231                     if (G_strncasecmp("new", age, 3) == 0) {
232                         /*fprintf(fp, "new: %s\n", opt->gisprompt); */
233                         num_outputs += 1;
234                     }
235                     else {
236                         /*fprintf(fp, "%s\n", opt->gisprompt); */
237                         num_inputs += 1;
238                     }
239                 }
240                 else {
241                     num_inputs += 1;
242                 }
243             }
244         }
245     }
246 
247     fprintf(fp, "{\n");
248     fprintf(fp, "  \"module\": \"%s\",\n", G_program_name());
249     fprintf(fp, "  \"id\": \"%s_%i\"", G_program_name(), random_int);
250 
251     if (st->n_flags && num_flags > 0) {
252         struct Flag *flag;
253 
254         fprintf(fp, ",\n");
255         fprintf(fp, "  \"flags\":\"");
256 
257         for (flag = &st->first_flag; flag; flag = flag->next_flag) {
258             if (flag->answer)
259                 fprintf(fp, "%c", flag->key);
260         }
261         fprintf(fp, "\"");
262     }
263 
264     /* Print the input options
265      */
266     if (st->n_opts && num_inputs > 0) {
267         struct Option *opt;
268 
269         i = 0;
270         fprintf(fp, ",\n");
271         fprintf(fp, "  \"inputs\":[\n");
272         for (opt = &st->first_option; opt; opt = opt->next_opt) {
273             if (opt->gisprompt) {
274                 G__split_gisprompt(opt->gisprompt, age, element, desc);
275                 if (G_strncasecmp("new", age, 3) != 0) {
276                     if (opt->answer) {
277                         check_create_import_opts(opt, element, fp);
278                         i++;
279                         if (i < num_inputs) {
280                             fprintf(fp, ",\n");
281                         }
282                         else {
283                             fprintf(fp, "\n");
284                         }
285                     }
286                 }
287             }
288             else if (opt->answer) {
289                 /* Check for input options */
290                 fprintf(fp, "     {\"param\": \"%s\", ", opt->key);
291                 fprintf(fp, "\"value\": \"%s\"}", opt->answer);
292                 i++;
293                 if (i < num_inputs) {
294                     fprintf(fp, ",\n");
295                 }
296                 else {
297                     fprintf(fp, "\n");
298                 }
299             }
300         }
301         fprintf(fp, "   ]");
302     }
303 
304     /* Print the output options
305      */
306     if (st->n_opts && num_outputs > 0) {
307         struct Option *opt;
308 
309         i = 0;
310         fprintf(fp, ",\n");
311         fprintf(fp, "  \"outputs\":[\n");
312         for (opt = &st->first_option; opt; opt = opt->next_opt) {
313             if (opt->gisprompt) {
314                 G__split_gisprompt(opt->gisprompt, age, element, desc);
315                 if (G_strncasecmp("new", age, 3) == 0) {
316                     if (opt->answer) {
317                         check_create_export_opts(opt, element, fp);
318                         i++;
319                         if (i < num_outputs) {
320                             fprintf(fp, ",\n");
321                         }
322                         else {
323                             fprintf(fp, "\n");
324                         }
325                     }
326                 }
327             }
328         }
329         fprintf(fp, "   ]\n");
330     }
331 
332     fprintf(fp, "}\n");
333     fclose(fp);
334 
335     /* Print the file content to stdout */
336     fp = fopen(file_name, "r");
337     if (fp == NULL) {
338         fprintf(stderr, "Unable to open temporary file <%s>\n", file_name);
339         exit(EXIT_FAILURE);
340     }
341 
342     c = fgetc(fp);
343     while (c != EOF) {
344         fprintf(stdout, "%c", c);
345         c = fgetc(fp);
346     }
347     fclose(fp);
348 
349     return file_name;
350 }
351 
352 /* \brief Check the provided answer and generate the import statement
353    dependent on the element type (cell, vector, grid3, file)
354 
355    {'import_descr': {'source': 'https://storage.googleapis.com/graas-geodata/elev_ned_30m.tif',
356    'type': 'raster'},
357    'param': 'map',
358    'value': 'elevation'}
359  */
check_create_import_opts(struct Option * opt,char * element,FILE * fp)360 void check_create_import_opts(struct Option *opt, char *element, FILE * fp)
361 {
362     int i = 0, urlfound = 0;
363     int has_import = 0;
364     char **tokens;
365 
366     G_debug(2, "tokenize opt string: <%s> with '@'", opt->answer);
367     tokens = G_tokenize(opt->answer, "@");
368     while (tokens[i]) {
369         G_chop(tokens[i]);
370         i++;
371     }
372     if (i > 2)
373         G_fatal_error(_("Input string not understood: <%s>. Multiple '@' chars?"),
374                       opt->answer);
375 
376     if (i > 1) {
377         /* check if tokens[1] starts with an URL or name@mapset */
378         G_debug(2, "tokens[1]: <%s>", tokens[1]);
379         if (strncmp(tokens[1], "http://", 7) == 0 ||
380             strncmp(tokens[1], "https://", 8) == 0 ||
381             strncmp(tokens[1], "ftp://", 6) == 0) {
382             urlfound = 1;
383             G_debug(2, "URL found");
384         }
385         else {
386             urlfound = 0;
387             G_debug(2, "name@mapset found");
388         }
389     }
390 
391     fprintf(fp, "     {");
392 
393     if (i > 1 && urlfound == 1) {
394         if (G_strncasecmp("cell", element, 4) == 0) {
395             fprintf(fp,
396                     "\"import_descr\": {\"source\":\"%s\", \"type\":\"raster\"},\n      ",
397                     tokens[1]);
398             has_import = 1;
399         }
400         else if (G_strncasecmp("file", element, 4) == 0) {
401             fprintf(fp,
402                     "\"import_descr\": {\"source\":\"%s\", \"type\":\"file\"},\n      ",
403                     tokens[1]);
404             has_import = 1;
405         }
406         else if (G_strncasecmp("vector", element, 4) == 0) {
407             fprintf(fp,
408                     "\"import_descr\": {\"source\":\"%s\", \"type\":\"vector\"},\n      ",
409                     tokens[1]);
410             has_import = 1;
411         }
412     }
413 
414     fprintf(fp, "\"param\": \"%s\", ", opt->key);
415     /* In case of import the mapset must be removed always */
416     if (urlfound == 1) {
417         fprintf(fp, "\"value\": \"%s\"",
418                 check_mapset_in_layer_name(tokens[0], has_import));
419     }
420     else {
421         fprintf(fp, "\"value\": \"%s\"",
422                 check_mapset_in_layer_name(opt->answer, has_import));
423     };
424     fprintf(fp, "}");
425 
426     G_free_tokens(tokens);
427 }
428 
429 /* \brief Check the provided answer and generate the export statement
430    dependent on the element type (cell, vector, grid3, file)
431 
432    "outputs": [
433    {"export": {"type": "file", "format": "TXT"},
434    "param": "output",
435    "value": "$file::out1"},
436    {'export': {'format': 'GTiff', 'type': 'raster'},
437    'param': 'map',
438    'value': 'LT52170762005240COA00_dos1.1'}
439    ]
440  */
check_create_export_opts(struct Option * opt,char * element,FILE * fp)441 void check_create_export_opts(struct Option *opt, char *element, FILE * fp)
442 {
443     int i = 0;
444     int has_file_export = 0;
445     char **tokens;
446 
447     tokens = G_tokenize(opt->answer, "+");
448     while (tokens[i]) {
449         G_chop(tokens[i]);
450         i++;
451     }
452 
453     fprintf(fp, "     {");
454 
455     if (i > 1) {
456         if (G_strncasecmp("cell", element, 4) == 0) {
457             fprintf(fp,
458                     "\"export\": {\"format\":\"%s\", \"type\":\"raster\"},\n      ",
459                     tokens[1]);
460         }
461         else if (G_strncasecmp("file", element, 4) == 0) {
462             fprintf(fp,
463                     "\"export\": {\"format\":\"%s\", \"type\":\"file\"},\n      ",
464                     tokens[1]);
465             has_file_export = 1;
466         }
467         else if (G_strncasecmp("vector", element, 4) == 0) {
468             fprintf(fp,
469                     "\"export\": {\"format\":\"%s\", \"type\":\"vector\"},\n      ",
470                     tokens[1]);
471         }
472     }
473 
474     fprintf(fp, "\"param\": \"%s\", ", opt->key);
475     if (has_file_export == 1) {
476         fprintf(fp, "\"value\": \"$file::%s\"",
477                 check_mapset_in_layer_name(tokens[0], 1));
478     }
479     else {
480         fprintf(fp, "\"value\": \"%s\"",
481                 check_mapset_in_layer_name(tokens[0], 1));
482     }
483     fprintf(fp, "}");
484 
485     G_free_tokens(tokens);
486 }
487 
488 /*
489    \brief Check if the current mapset is present in the layer name and remove it
490 
491    The flag always_remove tells this function to always remove all mapset names.
492 
493    \return pointer to the layer name without the current mapset
494 */
check_mapset_in_layer_name(char * layer_name,int always_remove)495 char *check_mapset_in_layer_name(char *layer_name, int always_remove)
496 {
497     int i = 0;
498     char **tokens;
499     const char *mapset;
500 
501     mapset = G_mapset();
502 
503     tokens = G_tokenize(layer_name, "@");
504 
505     while (tokens[i]) {
506         G_chop(tokens[i]);
507         /* fprintf(stderr, "Token %i: %s\n", i, tokens[i]); */
508         i++;
509     }
510 
511     if (always_remove == 1)
512         return tokens[0];
513 
514     if (i > 1 && G_strcasecmp(mapset, tokens[1]) == 0)
515         return tokens[0];
516 
517     return layer_name;
518 }
519