source: git/libpolys/resources/feResource.cc @ 8fee84

spielwiese
Last change on this file since 8fee84 was 8fee84, checked in by Oleksandr Motsak <motsak@…>, 12 years ago
libfindexec separation CHG: no need in 'ifdef HAVE_CONFIG_H' in omFindExec.c (not used in any configure anymore) CHG: separation/removal of omFindExec from omalloc & xalloc ADD: use new findexec library
  • Property mode set to 100644
File size: 19.5 KB
Line 
1/****************************************
2*  Computer Algebra System SINGULAR     *
3****************************************/
4/* $Id$ */
5/*
6* ABSTRACT: management of resources
7*/
8
9#include <stdlib.h>
10#include <unistd.h>
11#include <string.h>
12#include <unistd.h>
13#include <sys/param.h>
14
15#include "config.h"
16#include <misc/auxiliary.h>
17
18#include <findexec/omFindExec.h>
19
20#include "feResource.h"
21
22char* feArgv0 = NULL; 
23
24#ifdef AIX_4
25#define HAVE_PUTENV 1
26#endif
27
28#if defined(HPUX_10) || defined(HPUX_9)
29extern "C" int setenv(const char *name, const char *value, int overwrite);
30#endif
31
32
33#include <reporter/reporter.h>
34#if !defined(ESINGULAR) && !defined(TSINGULAR)
35#include <omalloc/omalloc.h>
36#else
37char* feResource(const char id, int warn = -1);
38char* feResource(const char* key, int warn = -1);
39#endif
40
41// define RESOURCE_DEBUG for chattering about resource management
42// #define RESOURCE_DEBUG
43
44#if defined(MAKE_DISTRIBUTION)
45#if defined(ix86_Win) && ! defined(__CYGWIN__)
46#define SINGULAR_DEFAULT_DIR "/Singular/"S_VERSION1
47#else // unix
48#define SINGULAR_DEFAULT_DIR "/usr/local/Singular/"S_VERSION1
49#endif
50#else // ! defined(MAKE_DISTRIBUTION)
51#define SINGULAR_DEFAULT_DIR S_ROOT_DIR
52#endif // defined(MAKE_DISTRIBUTION)
53
54/*****************************************************************
55 *
56 * Declarations: Data  structures
57 *
58 *****************************************************************/
59typedef enum {feResUndef = 0, feResBinary, feResDir, feResFile, feResUrl, feResPath} feResourceType;
60
61typedef struct feResourceConfig_s
62{
63  const char*           key;   // key to identify resource
64  const char            id;    // char id to identify resource
65  feResourceType  type;  // type of Resource
66  const char*           env;   // env variable to look for
67  const char*           fmt;   // format string -- see below for epxlaination
68  char*                 value; // what it was set to: may be changed
69} feResourceConfig_s;
70typedef feResourceConfig_s * feResourceConfig;
71
72// feSprintf transforms format strings as follows:
73// 1.) substrings of the form %c (c being a letter) are replaced by respective resource value
74// 2.) substrings of the form $string are replaced by value of resp. env variable
75
76// feCleanResource makes furthermore  the following transformations (except for URL resources)
77// 1.) '/' characters are replaced by respective directory - separators
78// 2.) ';' characters are replaced by respective path separators
79static feResourceConfig_s feResourceConfigs[] =
80{
81  {"SearchPath",    's', feResPath,  NULL,
82   "$SINGULARPATH;"
83   "%b/LIB;"
84   "%b/MOD;"
85   "%b/../lib/libpolys/MOD;"
86   "%b/../libexec/singular/MOD;"
87   "%r/LIB;"
88   "%r/../LIB;"
89   "%d/LIB;"
90   "%d/../LIB;"
91   "%b/gftables;"
92   "%b/../share/gftables;"
93   "%b/../share/singular/LIB;"
94   "%b/../factory/gftables;"
95   "%b/../../../factory/gftables;"
96   "%b/../../factory/gftables",
97   ""},
98  {"Singular",  'S',    feResBinary,"SINGULAR_EXECUTABLE",  "%d/"S_UNAME"/Singular",(char *)""},
99  {"BinDir",    'b',    feResDir,   "SINGULAR_BIN_DIR",     "%d/"S_UNAME,           (char *)""},
100  {"RootDir",   'r',    feResDir,   "SINGULAR_ROOT_DIR",    "%b/..",                (char *)""},
101  {"DefaultDir",'d',    feResDir,   "SINGULAR_DEFAULT_DIR",  SINGULAR_DEFAULT_DIR,  (char *)""},
102  {"InfoFile",  'i',    feResFile,  "SINGULAR_INFO_FILE",   "%r/info/singular.hlp", (char *)""},
103  {"IdxFile",   'x',    feResFile,  "SINGULAR_IDX_FILE",    "%r/doc/singular.idx",  (char *)""},
104  {"HtmlDir",   'h',    feResDir,   "SINGULAR_HTML_DIR",    "%r/html",              (char *)""},
105#ifdef ix86_Win
106  {"HtmlHelpFile",'C',  feResFile,  "SINGULAR_CHM_FILE",    "%r/doc/Manual.chm",    (char *)""},
107#endif
108  {"ManualUrl", 'u',    feResUrl,   "SINGULAR_URL",         "http://www.singular.uni-kl.de/Manual/"S_VERSION1,    (char *)""},
109  {"ExDir",     'm',    feResDir,   "SINGULAR_EXAMPLES_DIR","%r/examples",          (char *)""},
110  {"Path",      'p',    feResPath,  NULL,                   "%b;$PATH",             (char *)""},
111
112#ifdef ESINGULAR
113  {"emacs",     'E',    feResBinary,"ESINGULAR_EMACS",      "%b/emacs",             (char *)""},
114  {"xemacs",    'A',    feResBinary,"ESINGULAR_EMACS",      "%b/xemacs",            (char *)""},
115  {"SingularEmacs",'M', feResBinary,"ESINGULAR_SINGULAR",   "%b/Singular",          (char *)""},
116  {"EmacsLoad", 'l',    feResFile,  "ESINGULAR_EMACS_LOAD", "%e/.emacs-singular",   (char *)""},
117  {"EmacsDir",  'e',    feResDir,   "ESINGULAR_EMACS_DIR",  "%r/emacs",             (char *)""},
118#elif defined(TSINGULAR)
119  {"SingularXterm",'M', feResBinary,"TSINGULAR_SINGULAR",   "%b/Singular",          (char *)""},
120#ifdef ix86_Win
121  {"rxvt",      'X',    feResBinary,"RXVT",                 "%b/rxvt",              (char *)""},
122#else
123  {"xterm",     'X',    feResBinary,"XTERM",                "%b/xterm",             (char *)""},
124#endif
125#else
126  {"EmacsDir",  'e',    feResDir,   "SINGULAR_EMACS_DIR",   "%r/emacs",             (char *)""},
127#endif
128  {NULL, 0, feResUndef, NULL, NULL, NULL}, // must be the last record
129};
130
131
132/*****************************************************************
133 *
134 * Declarations: Local variables / functions
135 *
136 *****************************************************************/
137
138#define MAXRESOURCELEN 5*MAXPATHLEN
139
140static feResourceConfig feGetResourceConfig(const char id);
141static feResourceConfig feGetResourceConfig(const char* key);
142static char* feResource(feResourceConfig config, int warn);
143static char* feResourceDefault(feResourceConfig config);
144static char* feInitResource(feResourceConfig config, int warn);
145static char* feGetExpandedExecutable();
146static BOOLEAN feVerifyResourceValue(feResourceType type, char* value);
147static char* feCleanResourceValue(feResourceType type, char* value);
148static char* feCleanUpFile(char* fname);
149static char* feCleanUpPath(char* path);
150static void mystrcpy(char* d, char* s);
151static char* feSprintf(char* s, const char* fmt, int warn = -1);
152#if defined(ix86_Win) && defined(__GNUC__)
153// utility function of Cygwin32:
154extern "C" int cygwin32_posix_path_list_p (const char *path);
155#endif
156
157/*****************************************************************
158 *
159 * Public functions
160 *
161 *****************************************************************/
162char* feResource(const char* key, int warn)
163{
164  return feResource(feGetResourceConfig(key), warn);
165}
166
167char* feResource(const char id, int warn)
168{
169  return feResource(feGetResourceConfig(id), warn);
170}
171
172char* feGetResource(const char id)
173{
174  return feResource(feGetResourceConfig(id), -1);
175}
176
177char* feResourceDefault(const char id)
178{
179  return feResourceDefault(feGetResourceConfig(id));
180}
181
182char* feResourceDefault(const char* key)
183{
184  return feResourceDefault(feGetResourceConfig(key));
185}
186
187void feInitResources(const char* argv0)
188{
189#if defined(ix86_Win) && defined(__GNUC__)
190  if (cygwin32_posix_path_list_p (getenv("PATH")))
191    fePathSep = ':';
192#endif
193  if (argv0==NULL)
194  {
195    feArgv0 = (char*)omAlloc0(MAXPATHLEN+strlen("/Singular"));
196    getcwd(feArgv0, MAXPATHLEN);
197    strcpy(feArgv0+strlen(feArgv0),"/Singular");
198  }
199  else
200    feArgv0 = omStrDup(argv0);
201#ifdef RESOURCE_DEBUG
202  printf("feInitResources: entering with argv0=%s=\n", feArgv0);
203#endif
204  // init some Resources
205  feResource('b');
206  feResource('r');
207  // don't complain about stuff when initializing SingularPath
208  feResource('s',0);
209
210#if defined(HAVE_SETENV) || defined(HAVE_PUTENV)
211  char* path = feResource('p');
212#ifdef RESOURCE_DEBUG
213  printf("feInitResources: setting path with argv0=%s=\n", path);
214#endif
215#ifdef HAVE_PUTENV
216  if (path != NULL) { char *s=(char *)omAlloc0(strlen(path)+6);
217                      sprintf(s,"PATH=%s",path);
218                      putenv(s);
219                    }
220#else
221  if (path != NULL) setenv("PATH", path, 1);
222#endif
223#endif
224}
225
226void feReInitResources()
227{
228  int i = 0;
229  while (feResourceConfigs[i].key != NULL)
230  {
231    if (feResourceConfigs[i].value[0] != '\0')
232    {
233      if (feResourceConfigs[i].value != NULL)
234        omFree(feResourceConfigs[i].value);
235      feResourceConfigs[i].value = (char *)"";
236    }
237    i++;
238  }
239#ifdef RESOURCE_DEBUG
240  printf("feInitResources: entering with argv0=%s=\n", feArgv0);
241#endif
242  // init some Resources
243  feResource('b');
244  feResource('r');
245  // don't complain about stuff when initializing SingularPath
246  feResource('s',0);
247}
248
249/*****************************************************************
250 *
251 * Local functions
252 *
253 *****************************************************************/
254static feResourceConfig feGetResourceConfig(const char id)
255{
256  int i = 0;
257  while (feResourceConfigs[i].key != NULL)
258  {
259    if (feResourceConfigs[i].id == id) return &(feResourceConfigs[i]);
260    i++;
261  }
262  return NULL;
263}
264
265static feResourceConfig feGetResourceConfig(const char* key)
266{
267  int i = 0;
268  while (feResourceConfigs[i].key != NULL)
269  {
270    if (strcmp(feResourceConfigs[i].key, key) == 0)
271      return &(feResourceConfigs[i]);
272    i++;
273  }
274  return NULL;
275}
276
277static char* feResource(feResourceConfig config, int warn)
278{
279  if (config == NULL) return NULL;
280  if (config->value != NULL && *(config->value) != '\0') return config->value;
281  return feInitResource(config, warn);
282}
283
284static char* feResourceDefault(feResourceConfig config)
285{
286  if (config == NULL) return NULL;
287  char* value = (char*) omAlloc(MAXRESOURCELEN);
288  feSprintf(value, config->fmt, -1);
289  return value;
290}
291
292static char* feInitResource(feResourceConfig config, int warn)
293{
294  assume(config != NULL);
295#ifdef RESOURCE_DEBUG
296  printf("feInitResource: entering for %s\n", config->key);
297#endif
298
299  char value[MAXRESOURCELEN];
300  // now we have to work
301  // First, check Environment variable
302  if (config->env != NULL)
303  {
304    char* evalue = getenv(config->env);
305    if (evalue != NULL)
306    {
307#ifdef RESOURCE_DEBUG
308      printf("feInitResource: Found value from env:%s\n", evalue);
309#endif
310      strcpy(value, evalue);
311      if (config->type == feResBinary  // do not verify binaries
312          ||
313          feVerifyResourceValue(config->type,
314                                feCleanResourceValue(config->type, value)))
315      {
316#ifdef RESOURCE_DEBUG
317        printf("feInitResource: Set value of %s to =%s=\n", config->key, value);
318#endif
319        config->value = omStrDup(value);
320        return config->value;
321      }
322    }
323  }
324
325  *value = '\0';
326  // Special treatment of executable
327  if (config->id == 'S')
328  {
329    char* executable = feGetExpandedExecutable();
330    if (executable != NULL)
331    {
332#ifdef RESOURCE_DEBUG
333      printf("exec:%s\n", executable);
334#endif
335      strcpy(value, executable);
336#ifdef RESOURCE_DEBUG
337      printf("value:%s\n", value);
338#endif
339      omFree(executable);
340    }
341  }
342  // and bindir
343  else if (config->id == 'b')
344  {
345    char* executable = feResource('S');
346#ifdef RESOURCE_DEBUG
347      printf("feInitResource: Get %s from %s\n", config->key, executable);
348#endif
349    if (executable != NULL)
350    {
351      strcpy(value, executable);
352      executable = strrchr(value, DIR_SEP);
353      if (executable != NULL) *executable = '\0';
354    }
355  }
356
357#ifdef RESOURCE_DEBUG
358  printf("value:%s\n", value);
359#endif
360
361  if (*value == '\0' && config->fmt != NULL )
362  {
363    feSprintf(value, config->fmt, warn);
364  }
365  else if (config->fmt == NULL)
366  {
367    sprintf(value, "Wrong Resource Specification of %s", config->key);
368    dReportBug(value);
369    return NULL;
370  }
371
372  // Clean and verify
373  if (feVerifyResourceValue(config->type,
374                            feCleanResourceValue(config->type, value)))
375  {
376#ifdef RESOURCE_DEBUG
377    printf("feInitResource: Set value of %s to =%s=\n", config->key, value);
378#endif
379    config->value = omStrDup(value);
380    return config->value;
381  }
382  else if (config->type == feResBinary)
383  {
384    // for binaries, search through PATH once more
385    char* executable = omFindExec(config->key, value);
386    if (executable != NULL)
387    {
388      if (feVerifyResourceValue(config->type,
389                                feCleanResourceValue(config->type, value)))
390      {
391        config->value = omStrDup(value);
392#ifdef RESOURCE_DEBUG
393        printf("feInitResource: Set value of %s to =%s=\n", config->key, config->value);
394#endif
395        return config->value;
396      }
397    }
398  }
399
400  // issue warning if explicitely requested, or if
401  // this value is gotten for the first time
402  if (warn > 0 || (warn < 0 && config->value != NULL))
403  {
404    Warn("Could not get %s. ", config->key);
405    Warn("Either set environment variable %s to %s,",
406         config->env, config->key);
407    feSprintf(value, config->fmt, warn);
408    Warn("or make sure that %s is at %s", config->key, value);
409  }
410#ifdef RESOURCE_DEBUG
411      printf("feInitResource: Set value of %s to NULL", config->key);
412#endif
413  config->value = NULL;
414  return NULL;
415}
416
417static char* feGetExpandedExecutable()
418{
419  if (feArgv0 == NULL || *feArgv0 == '\0')
420  {
421    if (feArgv0 == NULL) dReportBug("feArgv0 == NULL");
422    else dReportBug("feArgv0 == ''");
423    return NULL;
424  }
425#ifdef ix86_Win // stupid WINNT sometimes gives you argv[0] within ""
426  if (*feArgv0 == '"')
427  {
428    int l = strlen(feArgv0);
429    if (feArgv0[l-1] == '"')
430    {
431      feArgv0[l-1] = '\0';
432      feArgv0++;
433    }
434  }
435#endif
436#ifdef RESOURCE_DEBUG
437  printf("feGetExpandedExecutable: calling find_exec with =%s=\n", feArgv0);
438#endif
439  char executable[MAXRESOURCELEN];
440  char* value = omFindExec(feArgv0, executable);
441#ifdef RESOURCE_DEBUG
442  printf("feGetExpandedExecutable: find_exec exited with =%s=%d\n", executable, access(executable, X_OK));
443#endif
444  if (value == NULL)
445  {
446    char message[MAXRESOURCELEN];
447    sprintf(message, "Could not get expanded executable from %s", feArgv0);
448    dReportBug(message);
449    return NULL;
450  }
451  return omStrDup(value);
452}
453
454
455static BOOLEAN feVerifyResourceValue(feResourceType type, char* value)
456{
457#ifdef RESOURCE_DEBUG
458  printf("feVerifyResourceValue: entering with =%s=\n", value);
459  printf("%d:%d\n", access(value, R_OK), access(value, X_OK));
460#endif
461  switch(type)
462  {
463      case feResUrl:
464      case feResPath:
465        return TRUE;
466
467      case feResFile:
468        return ! access(value, R_OK);
469
470      case feResBinary:
471      case feResDir:
472        return ! access(value, X_OK);
473
474      default:
475        return FALSE;
476  }
477}
478
479/*****************************************************************
480 *
481 * Cleaning/Transformations of resource values
482 *
483 *****************************************************************/
484
485static char* feCleanResourceValue(feResourceType type, char* value)
486{
487  if (value == NULL || *value == '\0') return value;
488#ifdef RESOURCE_DEBUG
489      printf("Clean value:%s\n", value);
490#endif
491#ifdef ix86_Win
492#ifdef RESOURCE_DEBUG
493      printf("Clean WINNT value:%s\n", value);
494#endif
495  if (type == feResBinary)
496  {
497    int l = strlen(value);
498    if (l < 4 || (strcmp(&value[l-4], ".exe") != 0 &&
499                  strcmp(&value[l-4], ".EXE") != 0))
500      strcat(value, ".exe");
501  }
502#endif
503  if (type == feResFile || type == feResBinary || type == feResDir)
504    return feCleanUpFile(value);
505  if (type == feResPath)
506    return feCleanUpPath(value);
507  return value;
508}
509
510static char* feCleanUpFile(char* fname)
511{
512  char* fn;
513
514#ifdef RESOURCE_DEBUG
515  printf("feCleanUpFile: entering with =%s=\n", fname);
516#endif
517  // Remove unnecessary .. and //
518  for (fn = fname; *fn != '\0'; fn++)
519  {
520    if (*fn == '/')
521    {
522      if (*(fn+1) == '\0')
523      {
524        if (fname != fn) *fn = '\0';
525        break;
526      }
527      if (*(fn + 1) == '/' && (fname != fn))
528      {
529        mystrcpy(fn, fn+1);
530        fn--;
531      }
532      else if (*(fn+1) == '.')
533      {
534        if (*(fn+2) == '.' && (*(fn + 3) == '/' || *(fn + 3) == '\0'))
535        {
536        #if 0
537        // this does not work: ./../../mmm will be changed to ./../mmm
538        // but we only want to change ././mmm to ./mmm
539          *fn = '\0';
540          s = strrchr(fname, '/');
541          if (s != NULL)
542          {
543            mystrcpy(s+1, fn + (*(fn + 3) != '\0' ? 4 : 3));
544            fn = s-1;
545          }
546          else
547          {
548            *fn = '/';
549          }
550        #endif
551        }
552        else if (*(fn+2) == '/' || *(fn+2) == '\0')
553        {
554          mystrcpy(fn+1, fn+3);
555          fn--;
556        }
557      }
558    }
559  }
560
561#ifdef RESOURCE_DEBUG
562  printf("feCleanUpFile: leaving with =%s=\n", fname);
563#endif
564  return fname;
565}
566
567// remove duplicates dir resp. those which do not exist
568static char* feCleanUpPath(char* path)
569{
570#ifdef RESOURCE_DEBUG
571  printf("feCleanUpPath: entering with: =%s=\n", path);
572#endif
573  if (path == NULL) return path;
574
575  int n_comps = 1, i, j;
576  char* opath = path;
577  char** path_comps;
578
579  for (; *path != '\0'; path++)
580  {
581    if (*path == fePathSep) n_comps++;
582    else if (*path == ';')
583    {
584      *path = fePathSep;
585      n_comps++;
586    }
587  }
588
589  path_comps = (char**) omAlloc(n_comps*sizeof(char*));
590  path_comps[0]=opath;
591  path=opath;
592  i = 1;
593
594  if (i < n_comps)
595  {
596    while (1)
597    {
598      if (*path == fePathSep)
599      {
600        *path = '\0';
601        path_comps[i] = path+1;
602        i++;
603        if (i == n_comps) break;
604      }
605      path++;
606    }
607  }
608
609  for (i=0; i<n_comps; i++)
610    path_comps[i] = feCleanUpFile(path_comps[i]);
611#ifdef RESOURCE_DEBUG
612  PrintS("feCleanUpPath: after CleanUpName: ");
613  for (i=0; i<n_comps; i++)
614    Print("%s:", path_comps[i]);
615  Print("\n");
616#endif
617
618  for (i=0; i<n_comps;)
619  {
620#ifdef RESOURCE_DEBUG
621    if (access(path_comps[i], X_OK | R_OK))
622      Print("feCleanUpPath: remove %d:%s -- can not access\n", i, path_comps[i]);
623#endif
624    if ( ! access(path_comps[i], X_OK | R_OK))
625    {
626      // x- permission is granted -- we assume that it is a dir
627      for (j=0; j<i; j++)
628      {
629        if (strcmp(path_comps[j], path_comps[i]) == 0)
630        {
631          // found a duplicate
632#ifdef RESOURCE_DEBUG
633          Print("feCleanUpPath: remove %d:%s -- equal to %d:%s\n", j, path_comps[j], i, path_comps[i]);
634#endif
635          j = i+1;
636          break;
637        }
638      }
639      if (j == i)
640      {
641        i++;
642        continue;
643      }
644    }
645    // now we can either not access or found a duplicate
646    path_comps[i] = NULL;
647    for (j=i+1; j<n_comps; j++)
648        path_comps[j-1] = path_comps[j];
649    n_comps--;
650  }
651
652
653  // assemble everything again
654  for (path=opath, i=0;i<n_comps-1;i++)
655  {
656    mystrcpy(path, path_comps[i]);
657    path += strlen(path);
658    *path = fePathSep;
659    path++;
660  }
661  if (n_comps)
662  {
663    mystrcpy(path, path_comps[i]);
664  }
665  else
666  {
667    *opath = '\0';
668  }
669  omFree(path_comps);
670#ifdef RESOURCE_DEBUG
671  Print("feCleanUpPath: leaving with path=%s=\n", opath);
672#endif
673  return opath;
674}
675
676// strcpy where source and destination may overlap
677static void mystrcpy(char* d, char* s)
678{
679  assume(d != NULL && s != NULL);
680  while (*s != '\0')
681  {
682    *d = *s;
683    d++;
684    s++;
685  }
686  *d = '\0';
687}
688
689/*****************************************************************
690 *
691 * feSprintf
692 *
693 *****************************************************************/
694static char* feSprintf(char* s, const char* fmt, int warn)
695{
696  char* s_in = s;
697  if (fmt == NULL) return NULL;
698
699  while (*fmt != '\0')
700  {
701    *s = *fmt;
702
703    if (*fmt == '%' && *(fmt + 1) != '\0')
704    {
705      fmt++;
706      char* r = feResource(*fmt, warn);
707      if (r != NULL)
708      {
709        strcpy(s, r);
710        s += strlen(r) - 1;
711      }
712      else
713      {
714        s++;
715        *s = *fmt;
716      }
717    }
718    else if (*fmt == '$' && *(fmt + 1) != '\0')
719    {
720      fmt++;
721      char* v = s + 1;
722      while (*fmt == '_' ||
723             (*fmt >= 'A' && *fmt <= 'Z') ||
724             (*fmt >= 'a' && *fmt <= 'z'))
725      {
726        *v = *fmt;
727        v++;
728        fmt++;
729      }
730      fmt--;
731      *v = '\0';
732      v = getenv(s + 1);
733      if (v != NULL) strcpy(s, v);
734      s += strlen(s) - 1;
735    }
736    s++;
737    fmt++;
738  }
739  *s = '\0';
740  return s_in;
741}
742
743void feStringAppendResources(int warn)
744{
745  int i = 0;
746  char* r;
747  StringAppend("%-10s:\t%s\n", "argv[0]", feArgv0);
748  while (feResourceConfigs[i].key != NULL)
749  {
750    r = feResource(feResourceConfigs[i].key, warn);
751    StringAppend("%-10s:\t%s\n", feResourceConfigs[i].key,
752                 (r != NULL ? r : ""));
753    i++;
754  }
755}
Note: See TracBrowser for help on using the repository browser.