source: trunk/ARB_GDE/GDE_ParseMenu.cxx

Last change on this file was 18866, checked in by westram, 2 years ago
  • fix failing unit-test (was broken by [18864])
  • [hack] force rebuild+exec of unittest after menu changes.
  • explain motivation for menu unittest.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 22.7 KB
Line 
1#include "GDE_proto.h"
2
3#include <aw_window.hxx>
4#include <MultiFileReader.h>
5#include <arb_file.h>
6
7#include <cctype>
8
9/*
10  Copyright (c) 1989, University of Illinois board of trustees.  All rights
11  reserved.  Written by Steven Smith at the Center for Prokaryote Genome
12  Analysis.  Design and implementation guidance by Dr. Gary Olsen and Dr.
13  Carl Woese.
14
15  Copyright (c) 1990,1991,1992 Steven Smith at the Harvard Genome Laboratory.
16  All rights reserved.
17
18  Changed to fit into ARB by ARB development team.
19*/
20
21
22inline bool only_whitespace(const char *line) {
23    size_t white = strspn(line, " \t");
24    return line[white] == 0; // only 0 after leading whitespace
25}
26
27static char *readableItemname(const GmenuItem& i) {
28    return GBS_global_string_copy("%s/%s", i.parent_menu->label, i.label);
29}
30
31inline __ATTR__NORETURN void throwError(const char *msg) {
32    throw string(msg);
33}
34
35static __ATTR__NORETURN void throwParseError(const char *msg, const LineReader& file) {
36    fprintf(stderr, "\n%s:%zu: %s\n", file.getFilename().c_str(), file.getLineNumber(), msg);
37    fflush(stderr);
38    throwError(msg);
39}
40
41static __ATTR__NORETURN void throwItemError(const GmenuItem& i, const char *error, const LineReader& file) {
42    char       *itemName = readableItemname(i);
43    const char *msg      = GBS_global_string("[Above this line] Invalid item '%s' defined: %s", itemName, error);
44    free(itemName);
45    throwParseError(msg, file);
46}
47
48static void CheckItemConsistency(const GmenuItem *item, const LineReader& file) {
49    // (incomplete) consistency check.
50    // bailing out with ItemError() here, will make unit-test and arb-startup fail!
51
52    if (item) {
53        const GmenuItem& I = *item;
54        if (I.seqtype != '-' && I.numinputs<1) {
55            // Such an item would create a window where alignment/species selection is present,
56            // but no sequence export will take place.
57            //
58            // Pressing 'GO' would result in failure or deadlock.
59            throwItemError(I, "item defines seqtype ('seqtype:' <> '-'), but is lacking input-specification ('in:')", file);
60        }
61        if (I.seqtype == '-' && I.numinputs>0) {
62            // Such an item would create a window where alignment/species selection has no GUI-elements,
63            // but sequences are exported (generating a corrupt sequence file)
64            //
65            // Pressing 'GO' would result in failure.
66            throwItemError(I, "item defines no seqtype ('seqtype:' = '-'), but defines input-specification ('in:')", file);
67        }
68    }
69}
70
71#define THROW_IF_NO(ptr,name) do { if (!ptr) throwParseError(GBS_global_string("'%s' used w/o '" name "'", head), in); } while(0)
72
73#define THROW_IF_NO_MENU()   THROW_IF_NO(thismenu, "menu")
74#define THROW_IF_NO_ITEM()   THROW_IF_NO(thisitem, "item")
75#define THROW_IF_NO_ARG()    THROW_IF_NO(thisarg, "arg")
76#define THROW_IF_NO_INPUT()  THROW_IF_NO(thisinput, "in")
77#define THROW_IF_NO_OUTPUT() THROW_IF_NO(thisoutput, "out")
78
79// --------------------------------------------------------------------------------
80
81inline const char *truncate_4000(const char *str) {
82    const int   BUFSIZE = 4000;
83    static char buffer[BUFSIZE];
84    strcpy_truncate(buffer, str, BUFSIZE);
85    return buffer;
86}
87
88inline void trim(char *str) {
89    int s = 0;
90    int d = 0;
91
92    while (isspace(str[s])) ++s;
93    while (str[s]) str[d++] = str[s++];
94
95    str[d] = 0;
96    while (d>0 && isspace(str[d-1])) str[--d] = 0;
97}
98
99static void splitEntry(const char *input, char *head, char *tail) {
100    /*! Split "this:that[:the_other]" into: "this" and "that[:the_other]"
101     */
102    const char *colon = strchr(input, ':');
103    if (colon) {
104        int len   = colon-input;
105        memcpy(head, input, len);
106        head[len] = 0;
107
108        strcpy(tail, colon+1);
109
110        trim(tail);
111    }
112    else {
113        strcpy(head, input);
114    }
115    trim(head);
116}
117
118// --------------------------------------------------------------------------------
119
120static void ParseMenus(LineReader& in) {
121    /*  parses menus via LineReader (contains ALL found menu-files) and
122     *  assemble an internal representation of the menu/menu-item hierarchy.
123     *
124     *  please document changes in ../HELP_SOURCE/source/gde_menus.hlp
125     */
126
127    memset((char*)&menu[0], 0, sizeof(Gmenu)*GDEMAXMENU);
128
129    int curarg    = 0;
130    int curinput  = 0;
131    int curoutput = 0;
132
133    Gmenu        *thismenu   = NULp;
134    GmenuItem    *thisitem   = NULp;
135    GmenuItemArg *thisarg    = NULp;
136    GfileFormat  *thisinput  = NULp;
137    GfileFormat  *thisoutput = NULp;
138
139    bool thismenu_firstOccurrence = true;
140
141    int  j;
142    char temp[GBUFSIZ];
143    char head[GBUFSIZ];
144    char tail[GBUFSIZ];
145
146    string lineStr;
147
148    while (in.getLine(lineStr)) {
149        gde_assert(lineStr.length()<GBUFSIZ); // otherwise buffer usage may fail
150
151        const char *in_line = lineStr.c_str();
152        if (in_line[0] == '#' || (in_line[0] && in_line[1] == '#')) {
153            ; // skip line
154        }
155        else if (only_whitespace(in_line)) {
156            ; // skip line
157        }
158        else {
159            splitEntry(in_line, head, temp);
160
161            // menu: chooses menu to use (may occur multiple times with same label!)
162            if (strcmp(head, "menu") == 0) {
163                int curmenu = -1;
164                for (j=0; j<num_menus && curmenu == -1; j++) {
165                    if (strcmp(temp, menu[j].label) == 0) curmenu=j;
166                }
167
168                thismenu_firstOccurrence = curmenu == -1;
169
170                // If menu not found, make a new one
171                if (thismenu_firstOccurrence) {
172                    curmenu               = num_menus++;
173                    thismenu              = &menu[curmenu];
174                    thismenu->label       = ARB_strdup(temp);
175                    thismenu->numitems    = 0;
176                    thismenu->active_mask = AWM_ALL;
177                }
178                else {
179                    thismenu = &menu[curmenu];
180                }
181
182                CheckItemConsistency(thisitem, in);
183                thisitem   = NULp;
184                thisarg    = NULp;
185                thisoutput = NULp;
186                thisinput  = NULp;
187            }
188            else if (strcmp(head, "menumask") == 0) {
189                THROW_IF_NO_MENU();
190                AW_active wanted_mask = strcmp("expert", temp) == 0 ? AWM_EXP : AWM_ALL;
191                if (!thismenu_firstOccurrence && thismenu->active_mask != wanted_mask) {
192                    throwParseError(GBS_global_string("menumask has inconsistent definitions (in different definitions of menu '%s')", thismenu->label), in);
193                }
194                thismenu->active_mask = wanted_mask;
195            }
196            else if (strcmp(head, "menumeta") == 0) {
197                THROW_IF_NO_MENU();
198                char wanted_meta = temp[0];
199                if (!thismenu_firstOccurrence && thismenu->meta != wanted_meta) {
200                    if (wanted_meta != 0) {
201                        if (thismenu->meta != 0) {
202                            throwParseError(GBS_global_string("menumeta has inconsistent definitions (in different definitions of menu '%s')", thismenu->label), in);
203                        }
204                        else {
205                            thismenu->meta = wanted_meta;
206                        }
207                    }
208                }
209                else {
210                    thismenu->meta = wanted_meta;
211                }
212            }
213            // item: chooses menu item to use
214            else if (strcmp(head, "item") == 0) {
215                CheckItemConsistency(thisitem, in);
216
217                THROW_IF_NO_MENU();
218
219                curarg    = -1;
220                curinput  = -1;
221                curoutput = -1;
222
223                int curitem = thismenu->numitems++;
224
225                // Resize the item list for this menu (add one item)
226                if (curitem == 0) {
227                    ARB_alloc(thismenu->item, 1);
228                }
229                else {
230                    ARB_realloc(thismenu->item, thismenu->numitems);
231                }
232
233                thisitem = &(thismenu->item[curitem]);
234
235                thisitem->numargs    = 0;
236                thisitem->numoutputs = 0;
237                thisitem->numinputs  = 0;
238                thisitem->label      = ARB_strdup(temp);
239                thisitem->method     = NULp;
240                thisitem->input      = NULp;
241                thisitem->output     = NULp;
242                thisitem->arg        = NULp;
243                thisitem->meta       = '\0';
244                thisitem->seqtype    = '-';   // no default sequence export
245                thisitem->aligned    = false;
246                thisitem->help       = NULp;
247
248                thisitem->parent_menu = thismenu;
249                thisitem->aws         = NULp; // no window opened yet
250                thisitem->active_mask = AWM_ALL;
251                thisitem->popup       = NULp;
252
253                for (int i = 0; i<curitem; ++i) {
254                    if (strcmp(thismenu->item[i].label, thisitem->label) == 0) {
255                        throwParseError(GBS_global_string("Duplicated item label '%s'", thisitem->label), in);
256                    }
257                }
258
259                thisarg = NULp;
260            }
261
262            // itemmethod: generic command line generated by this item
263            else if (strcmp(head, "itemmethod") == 0) {
264                THROW_IF_NO_ITEM();
265                ARB_calloc(thisitem->method, strlen(temp)+1);
266
267                {
268                    char *to = thisitem->method;
269                    char *from = temp;
270                    char last = 0;
271                    char c;
272
273                    do {
274                        c = *from++;
275                        if (c == '@' && last == '@') {
276                            // replace "@@" with "'"
277                            // [WHY_USE_DOUBLE_AT]
278                            // - cant use 1 single quote  ("'"). Things inside will not be preprocessed correctly.
279                            // - cant use 2 single quotes ("''") any longer. clang fails on OSX.
280                            to[-1] = '\'';
281                        }
282                        else {
283                            *to++ = c;
284                        }
285                        last = c;
286                    }
287                    while (c!=0);
288                }
289
290            }
291            // Help file
292            else if (strcmp(head, "itemhelp") == 0) {
293                THROW_IF_NO_ITEM();
294                if (thisitem->help) throwParseError("duplicated 'itemhelp' entry", in);
295                thisitem->help = GBS_string_eval(temp, "*.help=agde_*1.hlp");
296            }
297            // Meta key equiv
298            else if (strcmp(head, "itemmeta") == 0) {
299                THROW_IF_NO_ITEM();
300                if (thisitem->meta) throwParseError("duplicated 'itemmeta' entry", in);
301                thisitem->meta = temp[0];
302            }
303            else if (strcmp(head, "itemmask") == 0) {
304                THROW_IF_NO_ITEM();
305                if (strcmp("expert", temp) == 0) thisitem->active_mask = AWM_EXP;
306            }
307            // Sequence type restriction
308            else if (strcmp(head, "seqtype") == 0) {
309                THROW_IF_NO_ITEM();
310                thisitem->seqtype = toupper(temp[0]);
311                /* 'A' -> amino acids,
312                 * 'N' -> nucleotides,
313                 * '-' -> don't select sequences,
314                 * otherwise any alignment
315                 */
316            }
317            /* arg: defines the symbol for a command line argument.
318             *      this is used for substitution into the itemmethod
319             *      definition.
320             */
321
322            else if (strcmp(head, "arg") == 0) {
323                THROW_IF_NO_ITEM();
324
325                curarg = thisitem->numargs++;
326                ARB_recalloc(thisitem->arg, curarg, thisitem->numargs);
327
328                thisarg = &(thisitem->arg[curarg]);
329
330                thisarg->symbol      = ARB_strdup(temp);
331                thisarg->type        = 0;
332                thisarg->min         = 0.0;
333                thisarg->max         = 0.0;
334                thisarg->numchoices  = 0;
335                thisarg->choice      = NULp;
336                thisarg->textvalue   = NULp;
337                thisarg->ivalue      = 0;
338                thisarg->fvalue      = 0.0;
339                thisarg->label       = NULp;
340                thisarg->active_mask = AWM_ALL;
341            }
342            // argtype: Defines the type of argument (menu,chooser, text, slider)
343            else if (strcmp(head, "argtype") == 0) {
344                THROW_IF_NO_ARG();
345                int arglen = -1;
346                if (strncmp(temp, "text", (arglen = 4)) == 0) {
347                    thisarg->type         = TEXTFIELD;
348                    freedup(thisarg->textvalue, "");
349
350                    if (temp[arglen] == 0) thisarg->textwidth = TEXTFIELDWIDTH; // only 'text'
351                    else {
352                        if (temp[arglen] != '(' || temp[strlen(temp)-1] != ')') {
353                            sprintf(head, "Unknown argtype '%s' -- syntax: text(width) e.g. text(20)", truncate_4000(temp));
354                            throwParseError(head, in);
355                        }
356                        thisarg->textwidth = atoi(temp+arglen+1);
357                        if (thisarg->textwidth<1) {
358                            sprintf(head, "Illegal textwidth specified in '%s'", truncate_4000(temp));
359                            throwParseError(head, in);
360                        }
361                    }
362                }
363                else if (strcmp(temp, "choice_list") == 0) thisarg->type = CHOICE_LIST;
364                else if (strcmp(temp, "choice_menu") == 0) thisarg->type = CHOICE_MENU;
365                else if (strcmp(temp, "chooser")     == 0) thisarg->type = CHOOSER;
366                else if (strcmp(temp, "filename")    == 0) {
367                    thisarg->type = FILE_SELECTOR;
368                    freedup(thisarg->textvalue, "");
369                }
370                else if (strcmp(temp, "sai")         == 0) thisarg->type = CHOICE_SAI;
371                else if (strcmp(temp, "slider")      == 0) thisarg->type = SLIDER;
372                else if (strcmp(temp, "tree")        == 0) thisarg->type = CHOICE_TREE;
373                else if (strcmp(temp, "weights")     == 0) thisarg->type = CHOICE_WEIGHTS;
374                else {
375                    sprintf(head, "Unknown argtype '%s'", truncate_4000(temp));
376                    throwParseError(head, in);
377                }
378            }
379            /* argtext: The default text value of the symbol.
380             *          $argument is replaced by this value if it is not
381             *           changed in the dialog box by the user.
382             */
383            else if (strcmp(head, "argtext") == 0) {
384                THROW_IF_NO_ARG();
385                freedup(thisarg->textvalue, temp);
386            }
387            /* arglabel: Text label displayed in the dialog box for
388             *           this argument. It should be a descriptive label.
389             */
390            else if (strcmp(head, "arglabel") == 0) {
391                THROW_IF_NO_ARG();
392                thisarg->label = GBS_string_eval(temp, "\\\\n=\\n");
393            }
394            /* Argument choice values use the following notation:
395             *
396             * argchoice:Displayed value:Method
397             *
398             * Where "Displayed value" is the label displayed in the dialog box
399             * and "Method" is the value passed back on the command line.
400             */
401            else if (strcmp(head, "argchoice") == 0) {
402                THROW_IF_NO_ARG();
403                splitEntry(temp, head, tail);
404
405                int curchoice = thisarg->numchoices++;
406                ARB_recalloc(thisarg->choice, curchoice, thisarg->numchoices);
407
408                thisarg->choice[curchoice].label  = ARB_strdup(head);
409                thisarg->choice[curchoice].method = ARB_strdup(tail);
410            }
411            // argmin: Minimum value for a slider
412            else if (strcmp(head, "argmin") == 0) {
413                THROW_IF_NO_ARG();
414                (void)sscanf(temp, "%lf", &(thisarg->min));
415            }
416            // argmax: Maximum value for a slider
417            else if (strcmp(head, "argmax") == 0) {
418                THROW_IF_NO_ARG();
419                (void)sscanf(temp, "%lf", &(thisarg->max));
420            }
421            // argvalue: default value for a slider
422            else if (strcmp(head, "argvalue") == 0) {
423                THROW_IF_NO_ARG();
424                if (thisarg->type == TEXT) {
425                    freedup(thisarg->textvalue, temp);
426                }
427                else {
428                    (void)sscanf(temp, "%lf", &(thisarg->fvalue));
429                    thisarg->ivalue = (int) thisarg->fvalue;
430                }
431            }
432            else if (strcmp(head, "argmask") == 0) {
433                THROW_IF_NO_ARG();
434                if (strcmp("expert", temp) == 0) thisarg->active_mask = AWM_EXP;
435            }
436            // in: Input file description
437            else if (strcmp(head, "in") == 0) {
438                THROW_IF_NO_ITEM();
439
440                curinput = (thisitem->numinputs)++;
441                ARB_recalloc(thisitem->input, curinput, thisitem->numinputs);
442
443                thisinput = &(thisitem->input)[curinput];
444
445                thisinput->save     = false;
446                thisinput->format   = 0;
447                thisinput->symbol   = ARB_strdup(temp);
448                thisinput->name     = NULp;
449                thisinput->typeinfo = BASIC_TYPEINFO;
450            }
451            else if (strcmp(head, "informat") == 0) {
452                THROW_IF_NO_INPUT();
453                if (Find(temp, "genbank")) thisinput->format   = GENBANK;
454                else if (Find(temp, "flat")) thisinput->format = NA_FLAT;
455                else throwParseError(GBS_global_string("Unknown informat '%s' (allowed 'genbank' or 'flat')", temp), in);
456            }
457            else if (strcmp(head, "insave") == 0) {
458                THROW_IF_NO_INPUT();
459                thisinput->save = true;
460            }
461            else if (strcmp(head, "intyped") == 0) {
462                THROW_IF_NO_INPUT();
463                if (Find(temp, "detailed")) thisinput->typeinfo   = DETAILED_TYPEINFO;
464                else if (Find(temp, "basic")) thisinput->typeinfo = BASIC_TYPEINFO;
465                else throwParseError(GBS_global_string("Unknown value '%s' for 'intyped' (known: 'detailed', 'basic')", temp), in);
466            }
467            // out: Output file description
468            else if (strcmp(head, "out") == 0) {
469                THROW_IF_NO_ITEM();
470
471                curoutput = (thisitem->numoutputs)++;
472                ARB_recalloc(thisitem->output, curoutput, thisitem->numoutputs);
473
474                thisoutput = &(thisitem->output)[curoutput];
475
476                thisoutput->save   = false;
477                thisoutput->format = 0;
478                thisoutput->symbol = ARB_strdup(temp);
479                thisoutput->name   = NULp;
480            }
481            else if (strcmp(head, "outformat") == 0) {
482                THROW_IF_NO_OUTPUT();
483                if (Find(temp, "genbank")) thisoutput->format   = GENBANK;
484                else if (Find(temp, "gde")) thisoutput->format  = GDE;
485                else if (Find(temp, "flat")) thisoutput->format = NA_FLAT;
486                else throwParseError(GBS_global_string("Unknown outformat '%s' (allowed 'genbank', 'gde' or 'flat')", temp), in);
487            }
488            else if (strcmp(head, "outaligned") == 0) {
489                THROW_IF_NO_OUTPUT();
490                if (Find(temp, "yes")) thisitem->aligned = true;
491                else throwParseError(GBS_global_string("Unknown outaligned '%s' (allowed 'yes' or skip entry)", temp), in);
492            }
493            else if (strcmp(head, "outsave") == 0) {
494                THROW_IF_NO_OUTPUT();
495                thisoutput->save = true;
496            }
497            else {
498                throwParseError(GBS_global_string("No known GDE-menu-command found (line='%s')", in_line), in);
499            }
500        }
501    }
502
503    CheckItemConsistency(thisitem, in);
504
505    gde_assert(num_menus>0); // if this fails, the file arb.menu contained no menus (maybe file has zero size)
506}
507
508GB_ERROR LoadMenus() {
509    /*! Load menu config files
510     *
511     * loads all '*.menu' from "$ARBHOME/lib/gde" and "$ARB_PROP/gde"
512     */
513
514    GB_ERROR error = NULp;
515    StrArray files;
516    {
517        char *user_menu_dir = ARB_strdup(GB_path_in_arbprop("gde"));
518
519        if (!GB_is_directory(user_menu_dir)) {
520            error = GB_create_directory(user_menu_dir);
521        }
522        gde_assert(!GB_have_error());
523
524        if (!error) {
525            GBS_read_dir(files, user_menu_dir, "/\\.menu$/");
526            GBS_read_dir(files, GB_path_in_ARBLIB("gde"), "/\\.menu$/");
527            error = GB_incur_error();
528        }
529
530        free(user_menu_dir);
531    }
532
533    if (!error) {
534        MultiFileReader menus(files);
535        error = menus.get_error();
536        if (!error) {
537            try {
538                ParseMenus(menus);
539            }
540            catch (const string& err) {
541                error = GBS_static_string(err.c_str());
542            }
543        }
544    }
545
546    if (error) error = GBS_global_string("Error while loading menus: %s", error);
547    return error;
548}
549
550bool Find(const char *target, const char *key) {
551    // Search the target string for the given key
552    return strstr(target, key) ? true : false;
553}
554
555int Find2(const char *target, const char *key) {
556    /* Like Find(), but returns the index of the leftmost
557     * occurrence, and -1 if not found.
558     */
559    const char *found = strstr(target, key);
560    return found ? int(found-target) : -1;
561}
562
563// --------------------------------------------------------------------------------
564
565#ifdef UNIT_TESTS
566#ifndef TEST_UNIT_H
567#include <test_unit.h>
568#endif
569
570void TEST_load_menu() {
571    // very basic test: just detects failing assertions, crashes and errors
572
573    gb_getenv_hook old = GB_install_getenv_hook(arb_test::fakeenv);
574    {
575        // ../UNIT_TESTER/run/homefake/
576
577        TEST_EXPECT_NO_ERROR(LoadMenus());
578
579        // basic check of loaded data (needs to be adapted if menus change):
580        TEST_EXPECT_EQUAL(num_menus, 12);
581
582        string menus;
583        string menuitems;
584        for (int m = 0; m<num_menus; ++m) {
585            menus = menus + menu[m].label + ";";
586            menuitems += GBS_global_string("%i;", menu[m].numitems);
587        }
588
589        // if any of the following names changes = >
590        // you also need to change the title passed to the corresponding call to GDE_load_menu
591        // (otherwise a runtime error occurs)
592
593        TEST_EXPECT_EQUAL(menus,
594                          "Import;Export;Print;Align;SAI;Incremental phylogeny;Phylogeny Distance Matrix;"
595                          "Phylogeny max. parsimony;Phylogeny max. Likelihood EXP;Phylogeny max. Likelihood;Phylogeny (Other);User;");
596
597        TEST_EXPECT_EQUAL(menuitems, "3;1;2;10;1;1;3;2;1;8;6;0;");
598    }
599    TEST_EXPECT_EQUAL((void*)arb_test::fakeenv, (void*)GB_install_getenv_hook(old));
600}
601
602#endif // UNIT_TESTS
603
604// --------------------------------------------------------------------------------
605
Note: See TracBrowser for help on using the repository browser.