[Bio] / Sprout / CustomAttributes.pm Repository:
ViewVC logotype

Diff of /Sprout/CustomAttributes.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1.3, Thu Nov 9 21:19:53 2006 UTC revision 1.7, Wed Nov 15 12:04:05 2006 UTC
# Line 4  Line 4 
4    
5      require Exporter;      require Exporter;
6      use ERDB;      use ERDB;
7      @ISA = qw(Exporter ERDB);      @ISA = qw(ERDB);
     @EXPORT = qw(GetAttributes AddAttribute DeleteAttribute ChangeAttribute MatchSqlPattern);  
8      use strict;      use strict;
9      use Tracer;      use Tracer;
     use FIG;  
10      use ERDBLoad;      use ERDBLoad;
11    
12  =head1 Custom SEED Attribute Manager  =head1 Custom SEED Attribute Manager
# Line 22  Line 20 
20    
21  The full suite of ERDB retrieval capabilities is provided. In addition,  The full suite of ERDB retrieval capabilities is provided. In addition,
22  custom methods are provided specific to this application. To get all  custom methods are provided specific to this application. To get all
23  the values of the attribute C<essential> in the B<Feature> entity, you  the values of the attribute C<essential> in a specified B<Feature>, you
24  would code  would code
25    
26      my @values = $attrDB->GetAttributeValues($fid, Feature => 'essential');      my @values = $attrDB->GetAttributes([Feature => $fid], 'essential');
27    
28  where I<$fid> contains the ID of the desired feature. Each attribute has  where I<$fid> contains the ID of the desired feature. Each attribute has
29  an alternate index to allow searching for attributes by value.  an alternate index to allow searching for attributes by value.
# Line 78  Line 76 
76    
77  =back  =back
78    
79    The DBD file is critical, and must have reasonable contents before we can
80    begin using the system. In the old system, attributes were only provided
81    for Genomes and Features, so the initial XML file was the following.
82    
83        <Database>
84          <Title>SEED Custom Attribute Database</Title>
85          <Entities>
86            <Entity name="Feature" keyType="id-string">
87              <Notes>A [i]feature[/i] is a part of the genome
88              that is of special interest. Features may be spread
89              across multiple contigs of a genome, but never across
90              more than one genome. Features can be assigned to roles
91              via spreadsheet cells, and are the targets of
92              annotation.</Notes>
93            </Entity>
94            <Entity name="Genome" keyType="name-string">
95              <Notes>A [i]genome[/i] describes a particular individual
96              organism's DNA.</Notes>
97            </Entity>
98          </Entities>
99        </Database>
100    
101    It is not necessary to put any tables into the database; however, you should
102    run
103    
104        AttrDBRefresh
105    
106    periodically to insure it has the correct Genomes and Features in it. When
107    converting from the old system, use
108    
109        AttrDBRefresh -migrate
110    
111    to initialize the database and migrate the legacy data. You should only need
112    to do that once.
113    
114  =head2 Implementation Note  =head2 Implementation Note
115    
116  The L</Refresh> method reloads the entities in the database. If new  The L</Refresh> method reloads the entities in the database. If new
# Line 124  Line 157 
157      return $retVal;      return $retVal;
158  }  }
159    
 =head3 GetAttributeValues  
   
 C<< my @values = $attrDB->GetAttributeValues($id, $entityName => $attributeName); >>  
   
 Return all the values of the specified attribute for the specified entity instance.  
 A list of vaues will be returned. If the entity instance does not exist or the  
 attribute has no values, an empty list will be returned. If the attribute name  
 does not exist, an SQL error will occur.  
   
 A typical invocation would look like this:  
   
     my @values = $sttrDB->GetAttributeValues($fid, Feature => 'essential');  
   
 Here the user is asking for the values of the C<essential> attribute for the  
 B<Feature> with the specified ID. If the identified feature is not essential,  
 the list returned will be empty. If it is essential, then one or more values  
 will be returned that describe the essentiality.  
   
 =over 4  
   
 =item id  
   
 ID of the desired entity instance. This identifies the specific object to  
 be interrogated for attribute values.  
   
 =item entityName  
   
 Name of the entity. This identifies the the type of the object to be  
 interrogated for attribute values.  
   
 =item attributeName  
   
 Name of the desired attribute.  
   
 =item RETURN  
   
 Returns zero or more strings, each representing a value of the named attribute  
 for the specified entity instance.  
   
 =back  
   
 =cut  
   
 sub GetAttributeValues {  
     # Get the parameters.  
     my ($self, $id, $entityName, $attributeName) = @_;  
     # Get the data.  
     my @retVal = $self->GetEntityValues($entityName, $id, ["$entityName($attributeName)"]);  
     # Return the result.  
     return @retVal;  
 }  
   
160  =head3 StoreAttributeKey  =head3 StoreAttributeKey
161    
162  C<< my $attrDB = CustomAttributes::StoreAttributeKey($entityName, $attributeName, $type, $notes); >>  C<< my $attrDB = CustomAttributes::StoreAttributeKey($entityName, $attributeName, $type, $notes); >>
# Line 525  Line 506 
506                                                      -default => 1)                                                      -default => 1)
507                                     ),                                     ),
508                            );                            );
509      # Now the two buttons: UPDATE and DELETE.      # Now the three buttons: UPDATE, SHOW, and DELETE.
510      push @retVal, $cgi->Tr($cgi->th("&nbsp;"),      push @retVal, $cgi->Tr($cgi->th("&nbsp;"),
511                             $cgi->td({align => 'center'},                             $cgi->td({align => 'center'},
512                                      $cgi->submit(-name => 'Delete', -value => 'DELETE') . " " .                                      $cgi->submit(-name => 'Delete', -value => 'DELETE') . " " .
513                                      $cgi->submit(-name => 'Store',  -value => 'STORE')                                      $cgi->submit(-name => 'Store',  -value => 'STORE') . " " .
514                                        $cgi->submit(-name => 'Show',   -value => 'SHOW')
515                                     )                                     )
516                            );                            );
517      # Close the table and the form.      # Close the table and the form.
# Line 714  Line 696 
696    
697  =head3 MatchSqlPattern  =head3 MatchSqlPattern
698    
699  C<< my $matched = MatchSqlPattern($value, $pattern); >>  C<< my $matched = CustomAttributes::MatchSqlPattern($value, $pattern); >>
700    
701  Determine whether or not a specified value matches an SQL pattern. An SQL  Determine whether or not a specified value matches an SQL pattern. An SQL
702  pattern has two wild card characters: C<%> that matches multiple characters,  pattern has two wild card characters: C<%> that matches multiple characters,
# Line 726  Line 708 
708    
709  =item value  =item value
710    
711  Value to be matched against the pattern. Note that an undefined value will  Value to be matched against the pattern. Note that an undefined or empty
712  not match anything.  value will not match anything.
713    
714  =item pattern  =item pattern
715    
716  SQL pattern against which to match the value. An undefined pattern will  SQL pattern against which to match the value. An undefined or empty pattern will
717  match everything.  match everything.
718    
719  =item RETURN  =item RETURN
# Line 748  Line 730 
730      # Declare the return variable.      # Declare the return variable.
731      my $retVal;      my $retVal;
732      # Insure we have a pattern.      # Insure we have a pattern.
733      if (! defined($pattern)) {      if (! defined($pattern) || $pattern eq "") {
734          $retVal = 1;          $retVal = 1;
735      } else {      } else {
736          # Break the pattern into pieces around the wildcard characters. Because we          # Break the pattern into pieces around the wildcard characters. Because we
# Line 873  Line 855 
855                  # Get the key, value, and URL. We ignore the first element because that's the                  # Get the key, value, and URL. We ignore the first element because that's the
856                  # object ID, and we already know the object ID.                  # object ID, and we already know the object ID.
857                  my (undef, $key, $value, $url) = @{$dataTuple};                  my (undef, $key, $value, $url) = @{$dataTuple};
858                    # Remove the buggy "1" for $url.
859                    if ($url eq "1") {
860                        $url = undef;
861                    }
862                  # Only proceed if this is not an old key.                  # Only proceed if this is not an old key.
863                  if (! $myOldKeys->{$key}) {                  if (! $myOldKeys->{$key}) {
864                      # See if we've run into this key before.                      # See if we've run into this key before.
# Line 930  Line 916 
916      Trace("Migration complete.") if T(2);      Trace("Migration complete.") if T(2);
917  }  }
918    
919    =head3 ComputeObjectTypeFromID
920    
921    C<< my ($entityName, $id) = CustomAttributes::ComputeObjectTypeFromID($objectID); >>
922    
923    This method will compute the entity type corresponding to a specified object ID.
924    If the object ID begins with C<fig|>, it is presumed to be a feature ID. If it
925    is all digits with a single period, it is presumed to by a genome ID. Otherwise,
926    it must be a list reference. In this last case the first list element will be
927    taken as the entity type and the second will be taken as the actual ID.
928    
929    =over 4
930    
931    =item objectID
932    
933    Object ID to examine.
934    
935    =item RETURN
936    
937    Returns a 2-element list consisting of the entity type followed by the specified ID.
938    
939    =back
940    
941    =cut
942    
943    sub ComputeObjectTypeFromID {
944        # Get the parameters.
945        my ($objectID) = @_;
946        # Declare the return variables.
947        my ($entityName, $id);
948        # Only proceed if the object ID is defined. If it's not, we'll be returning a
949        # pair of undefs.
950        if ($objectID) {
951            if (ref $objectID eq 'ARRAY') {
952                # Here we have the new-style list reference. Pull out its pieces.
953                ($entityName, $id) = @{$objectID};
954            } else {
955                # Here the ID is the outgoing ID, and we need to look at its structure
956                # to determine the entity type.
957                $id = $objectID;
958                if ($objectID =~ /^\d+\.\d+/) {
959                    # Digits with a single period is a genome.
960                    $entityName = 'Genome';
961                } elsif ($objectID =~ /^fig\|/) {
962                    # The "fig|" prefix indicates a feature.
963                    $entityName = 'Feature';
964                } else {
965                    # Anything else is illegal!
966                    Confess("Invalid attribute ID specification \"$objectID\".");
967                }
968            }
969        }
970        # Return the result.
971        return ($entityName, $id);
972    }
973    
974  =head2 FIG Method Replacements  =head2 FIG Method Replacements
975    
976  The following methods are used by B<FIG.pm> to replace the previous attribute functionality.  The following methods are used by B<FIG.pm> to replace the previous attribute functionality.
# Line 937  Line 978 
978  supported and there is no longer any searching by URL. Fortunately, neither of these  supported and there is no longer any searching by URL. Fortunately, neither of these
979  capabilities were used in the old system.  capabilities were used in the old system.
980    
981    The methods here are the only ones supported by the B<RemoteCustomAttributes> object.
982    The idea is that these methods represent attribute manipulation allowed by all users, while
983    the others are only for privileged users with access to the attribute server.
984    
985  In the previous implementation, an attribute had a value and a URL. In the new implementation,  In the previous implementation, an attribute had a value and a URL. In the new implementation,
986  there is only a value. In this implementation, each attribute has only a value. These  there is only a value. In this implementation, each attribute has only a value. These
987  methods will treat the value as a list with the individual elements separated by the  methods will treat the value as a list with the individual elements separated by the
# Line 954  Line 999 
999    
1000  =head3 GetAttributes  =head3 GetAttributes
1001    
 C<< my @attributeList = GetAttributes($objectID, $key, @valuePatterns); >>  
   
 or  
   
1002  C<< my @attributeList = $attrDB->GetAttributes($objectID, $key, @valuePatterns); >>  C<< my @attributeList = $attrDB->GetAttributes($objectID, $key, @valuePatterns); >>
1003    
 The first form will connect to the database and release it. The second form  
 uses the database connection contained in the object.  
   
1004  In the database, attribute values are sectioned into pieces using a splitter  In the database, attribute values are sectioned into pieces using a splitter
1005  value specified in the constructor (L</new>). This is not a requirement of  value specified in the constructor (L</new>). This is not a requirement of
1006  the attribute system as a whole, merely a convenience for the purpose of  the attribute system as a whole, merely a convenience for the purpose of
# Line 1029  Line 1067 
1067  starts with C<fig|> is treated as a feature ID, and an ID that is all digits with a  starts with C<fig|> is treated as a feature ID, and an ID that is all digits with a
1068  single period is treated as a genome ID. For other entity types, use a list reference; in  single period is treated as a genome ID. For other entity types, use a list reference; in
1069  this case the first list element is the entity type and the second is the ID. A value of  this case the first list element is the entity type and the second is the ID. A value of
1070  C<undef> here will match all objects.  C<undef> or an empty string here will match all objects.
1071    
1072  =item key  =item key
1073    
# Line 1037  Line 1075 
1075  field name equal to the key name, it is very fast to find a list of all the  field name equal to the key name, it is very fast to find a list of all the
1076  matching keys. Each key's values require a separate query, however, which may  matching keys. Each key's values require a separate query, however, which may
1077  be a performance problem if the pattern matches a lot of keys. Wild cards are  be a performance problem if the pattern matches a lot of keys. Wild cards are
1078  acceptable here, and a value of C<undef> will match all attribute keys.  acceptable here, and a value of C<undef> or an empty string will match all
1079    attribute keys.
1080    
1081  =item valuePatterns  =item valuePatterns
1082    
1083  List of the desired attribute values, section by section. If C<undef>  List of the desired attribute values, section by section. If C<undef>
1084  is specified, all values in that section will match.  or an empty string is specified, all values in that section will match.
1085    
1086  =item RETURN  =item RETURN
1087    
# Line 1056  Line 1095 
1095  =cut  =cut
1096    
1097  sub GetAttributes {  sub GetAttributes {
1098      # Connect to the database. The tricky part is knowing whether or not we      # Get the parameters.
1099      # are an instance method (in which case the first parameter is a      my ($self, $objectID, $key, @valuePatterns) = @_;
     # CustomAttributes object) or a static method (in which case we must  
     # connect manually.  
     my $self = (UNIVERSAL::isa($_[0],__PACKAGE__) ? shift @_ : CustomAttributes->new());  
     # Get the remaining parameters.  
     my ($objectID, $key, @valuePatterns) = @_;  
1100      # Declare the return variable.      # Declare the return variable.
1101      my @retVal = ();      my @retVal = ();
1102      # Determine the entity types for our search.      # Determine the entity types for our search.
1103      my @objects = ();      my @objects = ();
1104      my ($actualObjectID, $computedType);      my ($actualObjectID, $computedType);
1105      if (! defined($objectID)) {      if (! $objectID) {
1106          push @objects, $self->GetEntityTypes();          push @objects, $self->GetEntityTypes();
1107      } else {      } else {
1108          ($computedType, $actualObjectID) = ComputeObjectTypeFromID($objectID);          ($computedType, $actualObjectID) = ComputeObjectTypeFromID($objectID);
# Line 1081  Line 1115 
1115          # MatchSqlPattern method          # MatchSqlPattern method
1116          my %secondaries = $self->GetSecondaryFields($entityType);          my %secondaries = $self->GetSecondaryFields($entityType);
1117          my @fieldList = grep { MatchSqlPattern($_, $key) } keys %secondaries;          my @fieldList = grep { MatchSqlPattern($_, $key) } keys %secondaries;
1118          # Now we figure out whether or not we need to filter by object.          # Now we figure out whether or not we need to filter by object. We will always
1119            # filter by key to a limited extent, so if we're filtering by object we need an
1120            # AND to join the object ID filter with the key filter.
1121          my $filter = "";          my $filter = "";
1122          my @params = ();          my @params = ();
1123          if (defined($actualObjectID)) {          if (defined($actualObjectID)) {
1124              # Here the caller wants to filter on object ID.              # Here the caller wants to filter on object ID.
1125              $filter = "$entityType(id) = ?";              $filter = "$entityType(id) = ? AND ";
1126              push @params, $actualObjectID;              push @params, $actualObjectID;
1127          }          }
1128          # It's time to begin making queries. We process one attribute key at a time, because          # It's time to begin making queries. We process one attribute key at a time, because
# Line 1095  Line 1131 
1131          # the DBD. That's a good thing, because an invalid key name will cause an SQL error.          # the DBD. That's a good thing, because an invalid key name will cause an SQL error.
1132          for my $key (@fieldList) {          for my $key (@fieldList) {
1133              # Get all of the attribute values for this key.              # Get all of the attribute values for this key.
1134              my @dataRows = $self->GetAll([$entityType], $filter, \@params,              my @dataRows = $self->GetAll([$entityType], "$filter$entityType($key) IS NOT NULL",
1135                                           ["$entityType(id)", "$entityType($key)"]);                                           \@params, ["$entityType(id)", "$entityType($key)"]);
1136              # Process each value separately. We need to verify the values and reformat the              # Process each value separately. We need to verify the values and reformat the
1137              # tuples. Note that GetAll will give us one row per matching object ID,              # tuples. Note that GetAll will give us one row per matching object ID,
1138              # with the ID first followed by a list of the data values. This is very              # with the ID first followed by a list of the data values. This is very
# Line 1139  Line 1175 
1175    
1176  C<< $attrDB->AddAttribute($objectID, $key, @values); >>  C<< $attrDB->AddAttribute($objectID, $key, @values); >>
1177    
 or  
   
 C<< AddAttribute($objectID, $key, @values); >>  
   
1178  Add an attribute key/value pair to an object. This method cannot add a new key, merely  Add an attribute key/value pair to an object. This method cannot add a new key, merely
1179  add a value to an existing key. Use L</StoreAttributeKey> to create a new key.  add a value to an existing key. Use L</StoreAttributeKey> to create a new key.
1180    
 The first form will connect to the database and release it. The second form  
 uses the database connection contained in the object.  
   
1181  =over 4  =over 4
1182    
1183  =item objectID  =item objectID
# Line 1173  Line 1202 
1202  =cut  =cut
1203    
1204  sub AddAttribute {  sub AddAttribute {
     # Connect to the database. The tricky part is knowing whether or not we  
     # are an instance method (in which case the first parameter is a  
     # CustomAttributes object) or a static method (in which case we must  
     # connect manually.  
     my $self = (UNIVERSAL::isa($_[0],__PACKAGE__) ? shift @_ : CustomAttributes->new());  
1205      # Get the parameters.      # Get the parameters.
1206      my ($objectID, $key, @values) = @_;      my ($self, $objectID, $key, @values) = @_;
1207      # Don't allow undefs.      # Don't allow undefs.
1208      if (! defined($objectID)) {      if (! defined($objectID)) {
1209          Confess("No object ID specified for AddAttribute call.");          Confess("No object ID specified for AddAttribute call.");
# Line 1204  Line 1228 
1228    
1229  C<< $attrDB->DeleteAttribute($objectID, $key, @values); >>  C<< $attrDB->DeleteAttribute($objectID, $key, @values); >>
1230    
 or  
   
 C<< DeleteAttribute($objectID, $key, @values); >>  
   
1231  Delete the specified attribute key/value combination from the database.  Delete the specified attribute key/value combination from the database.
1232    
1233  The first form will connect to the database and release it. The second form  The first form will connect to the database and release it. The second form
# Line 1235  Line 1255 
1255  =cut  =cut
1256    
1257  sub DeleteAttribute {  sub DeleteAttribute {
     # Connect to the database. The tricky part is knowing whether or not we  
     # are an instance method (in which case the first parameter is a  
     # CustomAttributes object) or a static method (in which case we must  
     # connect manually.  
     my $self = (UNIVERSAL::isa($_[0],__PACKAGE__) ? shift @_ : CustomAttributes->new());  
1258      # Get the parameters.      # Get the parameters.
1259      my ($objectID, $key, @values) = @_;      my ($self, $objectID, $key, @values) = @_;
1260      # Don't allow undefs.      # Don't allow undefs.
1261      if (! defined($objectID)) {      if (! defined($objectID)) {
1262          Confess("No object ID specified for DeleteAttribute call.");          Confess("No object ID specified for DeleteAttribute call.");
# Line 1261  Line 1276 
1276      return 1;      return 1;
1277  }  }
1278    
 =head3 ComputeObjectTypeFromID  
   
 C<< my ($entityName, $id) = CustomAttributes::ComputeObjectTypeFromID($objectID); >>  
   
 This method will compute the entity type corresponding to a specified object ID.  
 If the object ID begins with C<fig|>, it is presumed to be a feature ID. If it  
 is all digits with a single period, it is presumed to by a genome ID. Otherwise,  
 it must be a list reference. In this last case the first list element will be  
 taken as the entity type and the second will be taken as the actual ID.  
   
 =over 4  
   
 =item objectID  
   
 Object ID to examine.  
   
 =item RETURN  
   
 Returns a 2-element list consisting of the entity type followed by the specified ID.  
   
 =back  
   
 =cut  
   
 sub ComputeObjectTypeFromID {  
     # Get the parameters.  
     my ($objectID) = @_;  
     # Declare the return variables.  
     my ($entityName, $id);  
     # Only proceed if the object ID is defined. If it's not, we'll be returning a  
     # pair of undefs.  
     if (defined($objectID)) {  
         if (ref $objectID eq 'ARRAY') {  
             # Here we have the new-style list reference. Pull out its pieces.  
             ($entityName, $id) = @{$objectID};  
         } else {  
             # Here the ID is the outgoing ID, and we need to look at its structure  
             # to determine the entity type.  
             $id = $objectID;  
             if ($objectID =~ /^\d+\.\d+/) {  
                 # Digits with a single period is a genome.  
                 $entityName = 'Genome';  
             } elsif ($objectID =~ /^fig\|/) {  
                 # The "fig|" prefix indicates a feature.  
                 $entityName = 'Feature';  
             } else {  
                 # Anything else is illegal!  
                 Confess("Invalid attribute ID specification \"$objectID\".");  
             }  
         }  
     }  
     # Return the result.  
     return ($entityName, $id);  
 }  
   
1279  =head3 ChangeAttribute  =head3 ChangeAttribute
1280    
1281  C<< $attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >>  C<< $attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >>
1282    
 or  
   
 C<< ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >>  
   
1283  Change the value of an attribute key/value pair for an object.  Change the value of an attribute key/value pair for an object.
1284    
 The first form will connect to the database and release it. The second form  
 uses the database connection contained in the object.  
   
1285  =over 4  =over 4
1286    
1287  =item objectID  =item objectID
# Line 1355  Line 1308 
1308  =cut  =cut
1309    
1310  sub ChangeAttribute {  sub ChangeAttribute {
     # Connect to the database. The tricky part is knowing whether or not we  
     # are an instance method (in which case the first parameter is a  
     # CustomAttributes object) or a static method (in which case we must  
     # connect manually.  
     my $self = (UNIVERSAL::isa($_[0],__PACKAGE__) ? shift @_ : CustomAttributes->new());  
1311      # Get the parameters.      # Get the parameters.
1312      my ($objectID, $key, $oldValues, $newValues) = @_;      my ($self, $objectID, $key, $oldValues, $newValues) = @_;
1313      # Don't allow undefs.      # Don't allow undefs.
1314      if (! defined($objectID)) {      if (! defined($objectID)) {
1315          Confess("No object ID specified for ChangeAttribute call.");          Confess("No object ID specified for ChangeAttribute call.");
# Line 1380  Line 1328 
1328      return 1;      return 1;
1329  }  }
1330    
1331    =head3 EraseAttribute
1332    
1333    C<< $attrDB->EraseAttribute($entityName, $key); >>
1334    
1335    Erase all values for the specified attribute key. This does not remove the
1336    key from the database; it merely removes all the values.
1337    
1338    =over 4
1339    
1340    =item entityName
1341    
1342    Name of the entity to which the key belongs. If undefined, all entities will be
1343    examined for the desired key.
1344    
1345    =item key
1346    
1347    Key to erase.
1348    
1349    =back
1350    
1351    =cut
1352    
1353    sub EraseAttribute {
1354        # Get the parameters.
1355        my ($self, $entityName, $key) = @_;
1356        # Determine the relevant entity types.
1357        my @objects = ();
1358        if (! $entityName) {
1359            push @objects, $self->GetEntityTypes();
1360        } else {
1361            push @objects, $entityName;
1362        }
1363        # Loop through the entity types.
1364        for my $entityType (@objects) {
1365            # Now check for this key in this entity.
1366            my %secondaries = $self->GetSecondaryFields($entityType);
1367            if (exists $secondaries{$key}) {
1368                # We found it, so delete all the values of the key.
1369                $self->DeleteValue($entityName, undef, $key);
1370            }
1371        }
1372        # Return a 1, for backward compatability.
1373        return 1;
1374    }
1375    
1376  1;  1;

Legend:
Removed from v.1.3  
changed lines
  Added in v.1.7

MCS Webmaster
ViewVC Help
Powered by ViewVC 1.0.3