source: branches/profile/SL/AW_NAME/AW_rename.cxx

Last change on this file was 12398, checked in by westram, 10 years ago
  • GBS_read_arb_tcp()
    • handle exported errors at callers
    • changed error message to 'No such entry'
    • related to [12396]
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 24.0 KB
Line 
1// =============================================================== //
2//                                                                 //
3//   File      : AW_rename.cxx                                     //
4//   Purpose   :                                                   //
5//                                                                 //
6//   Institute of Microbiology (Technical University Munich)       //
7//   http://www.arb-home.de/                                       //
8//                                                                 //
9// =============================================================== //
10
11#include "AW_rename.hxx"
12
13#include <aw_awars.hxx>
14#include <aw_window.hxx>
15#include <aw_root.hxx>
16#include <aw_question.hxx>
17#include <aw_msg.hxx>
18#include <arb_progress.h>
19
20#include <names_client.h>
21#include <servercntrl.h>
22#include <client.h>
23
24#include <cctype>
25#include <ctime>
26
27static const char *get_addid(GBDATA *gb_main) {
28    GB_transaction ta(gb_main);
29    GBDATA *gb_addid = GB_search(gb_main, AWAR_NAMESERVER_ADDID, GB_FIND);
30    return gb_addid ? GB_read_char_pntr(gb_addid) : 0;
31}
32
33static GB_ERROR set_addid(GBDATA *gb_main, const char *addid) {
34    GB_ERROR error    = GB_push_transaction(gb_main);
35    if (!error) error = GBT_write_string(gb_main, AWAR_NAMESERVER_ADDID, addid ? addid : "");
36    return GB_end_transaction(gb_main, error);
37}
38
39const char *AW_get_nameserver_addid(GBDATA *gb_main) {
40    // return the additional field used for nameserver connection
41    const char *addid = get_addid(gb_main);
42
43    aw_assert(addid); // the current DB has no entry AWAR_NAMESERVER_ADDID! (programmers error)
44    if (!addid) addid = ""; // NDEBUG fallback
45
46    return addid;
47}
48
49GB_ERROR AW_select_nameserver(GBDATA *gb_main, GBDATA *gb_other_main) {
50    // if entry AWAR_NAMESERVER_ADDID isn't defined yet, try to detect a reasonable value
51    // from arb_tcp.dat. Ask user if multiple servers are defined.
52    //
53    // if gb_other_main is defined try to use value from there.
54
55    const char *addid   = get_addid(gb_main);
56    GB_ERROR    error   = 0;
57
58    if (!addid && gb_other_main && gb_other_main != gb_main) {
59        // look whether main DB has a defined addid
60        addid = get_addid(gb_other_main);
61        set_addid(gb_main, addid);
62    }
63
64    if (!addid) {
65        const char * const *nameservers = GBS_get_arb_tcp_entries("ARB_NAME_SERVER*");
66
67        if (!nameservers) error = GB_await_error();
68        else {
69            int serverCount = 0;
70
71            for (int c = 0; nameservers[c]; c++) serverCount++;
72
73            if (serverCount == 0) {
74                error = GBS_global_string("No nameserver defined.");
75            }
76            else {
77                char **fieldNames = (char **)malloc(serverCount*sizeof(*fieldNames));
78                for (int c = 0; c<serverCount; c++) {
79                    const char *ipport = GBS_read_arb_tcp(nameservers[c]);
80                    if (!ipport) {
81                        error = GB_await_error();
82                        fieldNames[c] = NULL;
83                    }
84                    else {
85                        fieldNames[c] = nulldup(GBS_scan_arb_tcp_param(ipport, "-f"));      // may return 0
86
87                        // parameter -f contains default value (e.g. '-fstart=1')
88                        if (fieldNames[c]) {
89                            char *equal = strchr(fieldNames[c], '=');
90                            if (equal) equal[0] = 0;
91                        }
92                    }
93                }
94
95                if (!error) {
96                    if (serverCount == 1) { // exactly 1 server defined -> don't ask
97                        error = set_addid(gb_main, fieldNames[0]);
98                    }
99                    else { // let the user select which nameserver to use
100                        aw_assert(serverCount>1);
101
102                        int         len     = serverCount; // commas+0term
103                        const char *nofield = "None (only 'acc')";
104
105                        for (int c = 0; c<serverCount; c++) {
106                            if (fieldNames[c]) len += strlen(fieldNames[c]);
107                            else len += strlen(nofield);
108                        }
109
110                        char *buttons = (char*)malloc(len);
111                        buttons[0]    = 0;
112                        for (int c = 0; c<serverCount; c++) {
113                            if (c) strcat(buttons, ",");
114                            strcat(buttons, fieldNames[c] ? fieldNames[c] : nofield);
115                        }
116
117                        int answer = aw_question("nameserv_select",
118                                                 "Select if and which additional DB field you want to use",
119                                                 buttons, false, "namesadmin.hlp");
120
121                        error = set_addid(gb_main, fieldNames[answer]);
122
123                        free(buttons);
124                    }
125                }
126
127                for (int c = 0; c<serverCount; c++) free(fieldNames[c]);
128                free(fieldNames);
129            }
130        }
131    }
132
133    return error;
134}
135
136
137// ------------------------------------
138//      class NameServerConnection
139
140class NameServerConnection {
141    aisc_com   *link;
142    T_AN_LOCAL  locs;
143    T_AN_MAIN   com;
144    int         persistent;     // if true -> connection will not be closed
145    time_t      linktime;       // time, when link has been established
146
147    int init_local_com_names()
148    {
149        if (!link) return 1;    //!* create and init local com structure **
150        if (aisc_create(link, AN_MAIN, com,
151                        MAIN_LOCAL, AN_LOCAL, locs,
152                        LOCAL_WHOAMI, "i bin der arb_tree",
153                        NULL)) {
154            return 1;
155        }
156        return 0;
157    }
158
159    NameServerConnection(const NameServerConnection& other);
160    NameServerConnection& operator=(const NameServerConnection& /* other */);
161
162    GB_ERROR reconnect(GBDATA *gb_main) { // reconnect ignoring consistency
163        int old_persistent = persistent;
164
165        printf("Reconnecting name server\n");
166
167        persistent = 0; // otherwise disconnect() won't disconnect
168        disconnect();
169        persistent = old_persistent; // restore previous persistence
170
171        return connect(gb_main);
172    }
173
174    char *fieldUsedByServer(GB_ERROR& err) {
175        char *field = 0;
176        if (aisc_get(link, AN_MAIN, com,
177                     MAIN_ADD_FIELD, &field,
178                     NULL)) {
179            err = "Connection Problems with the NAME_SERVER";
180            aw_assert(field == 0);
181        }
182        return field;
183    }
184
185    GB_ERROR expectServerUsesField(const char *expected_field) {
186        GB_ERROR  err          = 0;
187        char     *server_field = fieldUsedByServer(err);
188
189        if (!err && strcmp(expected_field, server_field) != 0) {
190            err = GBS_global_string("Additional field doesn't match (expected='%s', server uses='%s')", expected_field, server_field);
191        }
192        free(server_field);
193        return err;
194    }
195
196public:
197
198    NameServerConnection() {
199        link       = 0;
200        locs.clear();
201        com.clear();
202        persistent = 0;
203    }
204    virtual ~NameServerConnection() {
205        aw_assert(persistent == 0); // forgot to remove persistence ?
206        disconnect();
207    }
208
209    GB_ERROR connect(GBDATA *gb_main) {
210        aw_assert(!GB_have_error());
211
212        arb_progress::show_comment("Connecting to name server");
213
214        GB_ERROR err = 0;
215        if (!link) {
216            const char *add_field = AW_get_nameserver_addid(gb_main);
217            const char *server_id = GBS_nameserver_tag(add_field);
218
219            err = arb_look_and_start_server(AISC_MAGIC_NUMBER, server_id);
220
221            if (!err) {
222                const char *ipport = GBS_read_arb_tcp(server_id);
223                if (!ipport) err = GB_await_error();
224                else {
225                    link     = aisc_open(ipport, com, AISC_MAGIC_NUMBER, &err);
226                    linktime = time(0);
227
228                    if (!err) {
229                        if (init_local_com_names()) {
230                            err = GBS_global_string("Can't connect %s %s", server_id, ipport);
231                        }
232                        else {
233                            err = expectServerUsesField(add_field);
234                        }
235                    }
236                }
237            }
238        }
239        else {
240            long linkAge     = int(time(0)-linktime);
241            bool doReconnect = false;
242
243#if defined(DEBUG) && 0
244            // print information about name-server link age
245            static long lastage = -1;
246            if (linkAge != lastage) {
247                printf("Age of NameServerConnection: %li\n", linkAge);
248                lastage = linkAge;
249            }
250#endif // DEBUG
251
252            if (linkAge > (5*60)) { // perform a reconnect after 5 minutes
253                // Reason : The pipe to the name server breaks after some time
254                doReconnect = true;
255            }
256            else {
257                const char *add_field = AW_get_nameserver_addid(gb_main);
258                GB_ERROR    error     = expectServerUsesField(add_field);
259
260                if (error) {
261                    printf("Error: %s\n", error);
262                    doReconnect = true;
263                }
264            }
265
266            if (doReconnect) {
267                err = reconnect(gb_main);
268            }
269        }
270        aw_assert(!GB_have_error());
271        return err;
272    }
273
274    void disconnect() {
275        if (persistent == 0) {
276            if (link) {
277                aisc_close(link, com);
278                locs.clear();
279                com.clear();
280            }
281            link = 0;
282        }
283    }
284
285    void persistence(bool persist) {
286        if (persist) {
287            ++persistent;
288        }
289        else {
290            --persistent;
291            if (persistent <= 0) {
292                persistent = 0;
293                disconnect();
294            }
295        }
296    }
297
298
299    aisc_com *getLink() { return link; }
300    const T_AN_LOCAL& getLocs() const { return locs; } 
301};
302
303static NameServerConnection name_server;
304
305PersistentNameServerConnection::PersistentNameServerConnection() {
306    name_server.persistence(true);
307}
308PersistentNameServerConnection::~PersistentNameServerConnection() {
309    name_server.persistence(false);
310}
311
312// --------------------------------------------------------------------------------
313
314GB_ERROR AW_test_nameserver(GBDATA *gb_main) {
315    return name_server.connect(gb_main);
316}
317
318// --------------------------------------------------------------------------------
319
320GB_ERROR AWTC_generate_one_name(GBDATA *gb_main, const char *full_name, const char *acc, const char *addid, char*& new_name) {
321    // create a unique short name for 'full_name'
322    // the result is written into 'new_name' (as malloc-copy)
323    // if fails: GB_ERROR!=0 && new_name==0
324    // acc and addid may be 0
325
326    new_name = 0;
327    if (!acc) acc = "";
328
329    arb_progress progress("Generating species ID");
330
331    GB_ERROR err = name_server.connect(gb_main);
332    if (err) return err;
333
334    static char *shrt = 0;
335    if (strlen(full_name)) {
336        if (aisc_nput(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
337                      LOCAL_FULL_NAME,  full_name,
338                      LOCAL_ACCESSION,  acc,
339                      LOCAL_ADDID,      addid ? addid : "",
340                      LOCAL_ADVICE,     "",
341                      NULL)) {
342            err = "Connection Problems with the NAME_SERVER";
343        }
344        if (aisc_get(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
345                     LOCAL_GET_SHORT,   &shrt,
346                     NULL)) {
347            err = "Connection Problems with the NAME_SERVER";
348        }
349    }
350
351    if (err) {
352        free(shrt);
353    }
354    else {
355        if (shrt) {
356            new_name = shrt;
357            shrt = 0;
358        }
359        else {
360            err = GB_export_errorf("Failed to generate species ID (shortname) for '%s'", full_name);
361        }
362    }
363
364    name_server.disconnect();
365
366    return err;
367}
368
369GB_ERROR AWTC_recreate_name(GBDATA *gb_species) {
370    GBDATA       *gb_main = GB_get_root(gb_species);
371    arb_progress  progress("Recreating species ID");
372    GB_ERROR      error   = name_server.connect(gb_main);
373    if (!error) {
374        const char *add_field = AW_get_nameserver_addid(gb_main);
375        char       *ali_name  = GBT_get_default_alignment(gb_main);
376
377        GBDATA *gb_name      = GB_entry(gb_species, "name");
378        GBDATA *gb_full_name = GB_entry(gb_species, "full_name");
379        GBDATA *gb_acc       = GBT_gen_accession_number(gb_species, ali_name);
380        GBDATA *gb_addfield  = add_field[0] ? GB_entry(gb_species, add_field) : 0;
381
382        char *name      = gb_name ?     GB_read_string   (gb_name)     : strdup("");
383        char *full_name = gb_full_name ? GB_read_string  (gb_full_name) : strdup("");
384        char *acc       = gb_acc ?      GB_read_string   (gb_acc)      : strdup("");
385        char *addid     = gb_addfield ? GB_read_as_string(gb_addfield) : strdup("");
386
387        long   deleted = 0;
388        char *shrt    = 0;
389
390        if (aisc_nput(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
391                      LOCAL_FULL_NAME,  full_name,
392                      LOCAL_ACCESSION,  acc,
393                      LOCAL_ADDID,      addid,
394                      LOCAL_ADVICE,     "",
395                      NULL) != 0 ||
396            aisc_get(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
397                     LOCAL_DEL_SHORT,   &deleted,
398                     NULL)  != 0 ||
399            aisc_get(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
400                     LOCAL_GET_SHORT,   &shrt,
401                     NULL)  != 0)
402        {
403            error = "Connection Problems with the NAME_SERVER";
404        }
405        name_server.disconnect();
406
407        if (!error) {
408            GBT_begin_rename_session(gb_main, 0);
409            error = GBT_rename_species(name, shrt, true);
410            if (error) {
411                if (GBT_find_species(gb_main, shrt)) { // it was a rename error
412                    int done = 0;
413                    error    = 0;
414                    for (int count = 2; !done && !error && count<10; count++) {
415                        const char *other_short = GBS_global_string("%s.%i", shrt, count);
416                        if (!GBT_find_species(gb_main, other_short)) {
417                            error            = GBT_rename_species(name, other_short, true);
418                            if (!error) done = 1;
419                        }
420                    }
421
422                    if (!done && !error) {
423                        error = "Failed to regenerate species ID. Please use 'Species/Synchronize IDs'";
424                    }
425                }
426            }
427
428            if (error) GBT_abort_rename_session();
429            else error = GBT_commit_rename_session();
430        }
431
432        free(shrt);
433        free(addid);
434        free(acc);
435        free(full_name);
436        free(name);
437    }
438
439    return error;
440}
441
442char *AWTC_create_numbered_suffix(GB_HASH *species_name_hash, const char *shortname, GB_ERROR& warning) {
443    char *newshort = 0;
444    if (GBS_read_hash(species_name_hash, shortname)) {
445        int i;
446        newshort = (char *)GB_calloc(sizeof(char), strlen(shortname)+20);
447        for (i = 1; ; i++) {
448            sprintf(newshort, "%s.%i", shortname, i);
449            if (!GBS_read_hash(species_name_hash, newshort))break;
450        }
451
452        warning =
453            "There are duplicated species!\n"
454            "The IDs of these species ('name') contain a '.' character followed by a number.\n"
455            "We strongly recommend you try understand and solve this problem\n"
456            "(see HELP in 'Species/Synchronize IDs' window)";
457    }
458    return newshort;
459}
460
461GB_ERROR AWTC_pars_names(GBDATA *gb_main, bool *isWarningPtr) {
462    // rename species according to name_server
463    // 'isWarning' is set to true, in case of duplicates-warning
464
465    arb_progress gen_progress("Generating new names", 2);
466    GB_ERROR     err       = name_server.connect(gb_main);
467    bool         isWarning = false;
468
469    if (!err) {
470        err = GBT_begin_rename_session(gb_main, 1);
471        if (!err) {
472            char     *ali_name = GBT_get_default_alignment(gb_main);
473            long      spcount  = GBT_get_species_count(gb_main);
474            GB_HASH  *hash     = GBS_create_hash(spcount, GB_IGNORE_CASE);
475            GB_ERROR  warning  = 0;
476
477            if (spcount) {
478                arb_progress progress("Renaming species", spcount);
479                const char *add_field = AW_get_nameserver_addid(gb_main);
480
481                for (GBDATA *gb_species = GBT_first_species(gb_main);
482                     gb_species && !err;
483                     gb_species = GBT_next_species(gb_species))
484                {
485                    GBDATA *gb_name      = GB_entry(gb_species, "name");
486                    GBDATA *gb_full_name = GB_entry(gb_species, "full_name");
487                    GBDATA *gb_acc       = GBT_gen_accession_number(gb_species, ali_name);
488                    GBDATA *gb_addfield  = add_field[0] ? GB_entry(gb_species, add_field) : 0;
489
490                    char *name      = gb_name      ? GB_read_string   (gb_name)     : strdup("");
491                    char *full_name = gb_full_name ? GB_read_string   (gb_full_name) : strdup("");
492                    char *acc       = gb_acc       ? GB_read_string   (gb_acc)      : strdup("");
493                    char *addid     = gb_addfield  ? GB_read_as_string(gb_addfield) : strdup(""); // empty value will be set to default by nameserver
494
495                    char *shrt = 0;
496
497                    if (full_name[0] || acc[0] || addid[0]) {
498                        if (aisc_nput(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
499                                      LOCAL_FULL_NAME,  full_name,
500                                      LOCAL_ACCESSION,  acc,
501                                      LOCAL_ADDID,      addid,
502                                      LOCAL_ADVICE,     name,
503                                      NULL)) {
504                            err = "Connection Problems with the NAME_SERVER";
505                        }
506                        if (aisc_get(name_server.getLink(), AN_LOCAL, name_server.getLocs(),
507                                     LOCAL_GET_SHORT,   &shrt,
508                                     NULL)) {
509                            err = "Connection Problems with the NAME_SERVER";
510                        }
511                    }
512                    else {
513                        shrt = strdup(name);
514                    }
515                    if (!err) {
516                        char *newshrt = AWTC_create_numbered_suffix(hash, shrt, warning);
517                        if (newshrt) freeset(shrt, newshrt);
518
519                        GBS_incr_hash(hash, shrt);
520                        err = GBT_rename_species(name, shrt, true);
521                    }
522
523                    free(shrt);
524                    free(addid);
525                    free(acc);
526                    free(full_name);
527                    free(name);
528
529                    progress.inc_and_check_user_abort(err);
530                }
531            }
532            else {
533                gen_progress.sub_progress_skipped(); // trigger skipped subcounter
534            }
535
536            if (err) GBT_abort_rename_session();
537            else err = GBT_commit_rename_session();
538
539            GBS_free_hash(hash);
540            free(ali_name);
541
542            if (!err) {
543                err = warning;
544                if (warning) isWarning = true;
545            }
546        }
547        name_server.disconnect();
548    }
549
550    if (isWarningPtr) *isWarningPtr = isWarning;
551    gen_progress.done(); // needed if err
552
553    return err;
554}
555
556
557static void awt_rename_cb(AW_window *aww, GBDATA *gb_main) {
558    GB_ERROR error = AWTC_pars_names(gb_main);
559    if (error) aw_message(error);
560    aww->get_root()->awar(AWAR_TREE_REFRESH)->touch();
561}
562
563
564AW_window *AWTC_create_rename_window(AW_root *root, GBDATA *gb_main) {
565    AW_window_simple *aws = new AW_window_simple;
566    aws->init(root, "AUTORENAME_SPECIES", "Synchronize species IDs");
567
568    aws->load_xfig("awtc/autoren.fig");
569
570    aws->callback((AW_CB0)AW_POPDOWN);
571    aws->at("close");
572    aws->create_button("CLOSE", "CLOSE", "C");
573
574    aws->callback(makeHelpCallback("sp_rename.hlp"));
575    aws->at("help");
576    aws->create_button("HELP", "HELP", "H");
577
578    aws->at("go");
579    aws->highlight();
580    aws->callback(makeWindowCallback(awt_rename_cb, gb_main));
581    aws->create_button("GO", "GO", "G");
582
583    return (AW_window *)aws;
584}
585
586UniqueNameDetector::UniqueNameDetector(GBDATA *gb_item_data, int additionalEntries) {
587    hash = GBS_create_hash(GB_number_of_subentries(gb_item_data)+additionalEntries, GB_IGNORE_CASE);
588
589    for (GBDATA *gb_item = GB_child(gb_item_data); gb_item; gb_item = GB_nextChild(gb_item)) {
590        GBDATA *gb_name = GB_entry(gb_item, "name");
591        if (gb_name) { // item has name -> insert to hash
592            GBS_write_hash(hash, GB_read_char_pntr(gb_name), 1);
593        }
594    }
595}
596
597UniqueNameDetector::~UniqueNameDetector() { GBS_free_hash(hash); }
598
599static char *makeUniqueShortName(const char *prefix, UniqueNameDetector& existing) {
600    // generates a non-existing short-name (name starts with prefix)
601    //
602    // returns NULL if it fails
603
604    char *result     = 0;
605    int   prefix_len = strlen(prefix);
606
607    aw_assert(prefix_len<8); // prefix has to be shorter than 8 chars!
608    if (prefix_len<8) {
609        const int max_nums[8] = { 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10 };
610        static int next_try[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
611
612        int  max_num = max_nums[prefix_len];
613        char short_name[9];
614        strcpy(short_name, prefix);
615
616        char *dig_pos = short_name+prefix_len;
617        int   num     = next_try[prefix_len];
618        int   stop    = num ? num-1 : max_num;
619
620        while (num != stop) {
621            sprintf(dig_pos, "%i", num);
622            ++num;
623            if (!existing.name_known(short_name)) {
624                result = strdup(short_name);
625                break;
626            }
627            if (num == max_num && stop != max_num) num = 0;
628        }
629        if (num == max_num) num = 0;
630        next_try[prefix_len] = num;
631    }
632    return result;
633}
634
635char *AWTC_makeUniqueShortName(const char *prefix, UniqueNameDetector& existingNames) {
636    // generates a unique species name from prefix
637    // (prefix will be fillup with zero digits and then shortened down to first char)
638    //
639    // returns NULL if failed (and exports error)
640
641    int  len = strlen(prefix);
642    char p[9];
643    strncpy(p, prefix, 8);
644
645    if (len>8) len = 8;
646    else {
647        if (len == 0) p[len++] = 'x'; // don't use digit as first character
648        while (len<8) p[len++] = '0';
649    }
650
651    p[len] = 0;
652
653    char *result = 0;
654
655    for (int l = len-1; l>0 && !result; --l) {
656        p[l]   = 0;
657        result = makeUniqueShortName(p, existingNames);
658    }
659
660    aw_assert(!result || strlen(result) <= 8);
661    if (!result) GB_export_errorf("Failed to create unique species ID (prefix='%s')", prefix);
662
663    return result;
664}
665
666char *AWTC_generate_random_name(UniqueNameDetector& existingNames) {
667    char *new_species_name = 0;
668    char  short_name[9];
669    int   count            = 10000;
670
671    short_name[8] = 0;
672    while (count--) {
673        short_name[0] = 'a'+GB_random(26); // first character has to be alpha
674
675        for (int x=1; x<8; ++x) {
676            int r = GB_random(36); // rest may be alphanumeric
677            short_name[x] = r<10 ? ('0'+r) : ('a'+r-10);
678        }
679
680        if (!existingNames.name_known(short_name)) {
681            new_species_name = strdup(short_name);
682            break;
683        }
684    }
685
686    if (!new_species_name) {
687        aw_message("Failed to generate a random name - retrying (this might hang forever)");
688        return AWTC_generate_random_name(existingNames);
689    }
690
691    return new_species_name;
692}
693
694int AWTC_name_quality(const char *short_name) {
695    // result 0 = ok for external tools
696    //        1 = ok for ARB
697    //        2 = not ok
698
699    int len         = -1;
700    int alnum_count = 0;
701    int ascii_count = 0;
702
703    while (char c = short_name[++len]) {
704        alnum_count += (isalnum(c) != 0);
705        ascii_count += (c > 32 && c < 127);
706    }
707
708    if (len>0) {
709        if (len <= 8) {
710            if (len == alnum_count) return 0; // ok for external programs
711        }
712        if (len == ascii_count) return 1; // ok for ARB
713    }
714    return 2; // not ok
715}
Note: See TracBrowser for help on using the repository browser.