8 |
use strict; |
use strict; |
9 |
use Tracer; |
use Tracer; |
10 |
use ERDBLoad; |
use ERDBLoad; |
11 |
|
use Stats; |
12 |
|
use Time::HiRes qw(time); |
13 |
|
|
14 |
=head1 Custom SEED Attribute Manager |
=head1 Custom SEED Attribute Manager |
15 |
|
|
29 |
The actual attribute values are stored as a relationship between the attribute |
The actual attribute values are stored as a relationship between the attribute |
30 |
keys and the objects. There can be multiple values for a single key/object pair. |
keys and the objects. There can be multiple values for a single key/object pair. |
31 |
|
|
32 |
|
=head3 Object IDs |
33 |
|
|
34 |
|
The object ID is normally represented as |
35 |
|
|
36 |
|
I<type>:I<id> |
37 |
|
|
38 |
|
where I<type> is the object type (C<Role>, C<Coupling>, etc.) and I<id> is |
39 |
|
the actual object ID. Note that the object type must consist of only upper- and |
40 |
|
lower-case letters! Thus, C<GenomeGroup> is a valid object type, but |
41 |
|
C<genome_group> is not. Given that restriction, the object ID |
42 |
|
|
43 |
|
Family:aclame|cluster10 |
44 |
|
|
45 |
|
would represent the FIG family C<aclame|cluster10>. For historical reasons, |
46 |
|
there are three exceptions: subsystems, genomes, and features do not need |
47 |
|
a type. So, for PEG 3361 of Streptomyces coelicolor A3(2), you simply code |
48 |
|
|
49 |
|
fig|100226.1.peg.3361 |
50 |
|
|
51 |
|
The methods L</ParseID> and L</FormID> can be used to make this all seem |
52 |
|
more consistent. Given any object ID string, L</ParseID> will convert it to an |
53 |
|
object type and ID, and given any object type and ID, L</FormID> will |
54 |
|
convert it to an object ID string. The attribute database is pretty |
55 |
|
freewheeling about what it will allow for an ID; however, for best |
56 |
|
results, the type should match an entity type from a Sprout genetics |
57 |
|
database. If this rule is followed, then the database object |
58 |
|
corresponding to an ID in the attribute database could be retrieved using |
59 |
|
L</GetTargetObject> method. |
60 |
|
|
61 |
|
my $object = CustomAttributes::GetTargetObject($sprout, $idValue); |
62 |
|
|
63 |
|
=head3 Retrieval and Logging |
64 |
|
|
65 |
The full suite of ERDB retrieval capabilities is provided. In addition, |
The full suite of ERDB retrieval capabilities is provided. In addition, |
66 |
custom methods are provided specific to this application. To get all |
custom methods are provided specific to this application. To get all |
67 |
the values of the attribute C<essential> in a specified B<Feature>, you |
the values of the attribute C<essential> in a specified B<Feature>, you |
71 |
|
|
72 |
where I<$fid> contains the ID of the desired feature. |
where I<$fid> contains the ID of the desired feature. |
73 |
|
|
74 |
New attribute keys must be defined before they can be used. A web interface |
Keys can be split into two pieces using the splitter value defined in the |
75 |
is provided for this purpose. |
constructor (the default is C<::>). The first piece of the key is called |
76 |
|
the I<real key>. This portion of the key must be defined using the |
77 |
|
web interface (C<Attributes.cgi>). The second portion of the key is called |
78 |
|
the I<sub key>, and can take any value. |
79 |
|
|
80 |
|
Major attribute activity is recorded in a log (C<attributes.log>) in the |
81 |
|
C<$FIG_Config::var> directory. The log reports the user name, time, and |
82 |
|
the details of the operation. The user name will almost always be unknown, |
83 |
|
the exception being when it is specified in this object's constructor |
84 |
|
(see L</new>). |
85 |
|
|
86 |
=head2 FIG_Config Parameters |
=head2 FIG_Config Parameters |
87 |
|
|
131 |
|
|
132 |
=head3 new |
=head3 new |
133 |
|
|
134 |
C<< my $attrDB = CustomAttributes->new($splitter); >> |
C<< my $attrDB = CustomAttributes->new(%options); >> |
135 |
|
|
136 |
Construct a new CustomAttributes object. |
Construct a new CustomAttributes object. The following options are |
137 |
|
supported. |
138 |
|
|
139 |
=over 4 |
=over 4 |
140 |
|
|
141 |
=item splitter |
=item splitter |
142 |
|
|
143 |
Value to be used to split attribute values into sections in the |
Value to be used to split attribute values into sections in the |
144 |
L</Fig Replacement Methods>. The default is a double colon C<::>. |
L</Fig Replacement Methods>. The default is a double colon C<::>, |
145 |
If you do not use the replacement methods, you do not need to |
and should only be overridden in extreme circumstances. |
146 |
worry about this parameter. |
|
147 |
|
=item user |
148 |
|
|
149 |
|
Name of the current user. This will appear in the attribute log. |
150 |
|
|
151 |
=back |
=back |
152 |
|
|
154 |
|
|
155 |
sub new { |
sub new { |
156 |
# Get the parameters. |
# Get the parameters. |
157 |
my ($class, $splitter) = @_; |
my ($class, %options) = @_; |
158 |
# Connect to the database. |
# Connect to the database. |
159 |
my $dbh = DBKernel->new($FIG_Config::attrDbms, $FIG_Config::attrDbName, |
my $dbh = DBKernel->new($FIG_Config::attrDbms, $FIG_Config::attrDbName, |
160 |
$FIG_Config::attrUser, $FIG_Config::attrPass, |
$FIG_Config::attrUser, $FIG_Config::attrPass, |
164 |
my $xmlFileName = $FIG_Config::attrDBD; |
my $xmlFileName = $FIG_Config::attrDBD; |
165 |
my $retVal = ERDB::new($class, $dbh, $xmlFileName); |
my $retVal = ERDB::new($class, $dbh, $xmlFileName); |
166 |
# Store the splitter value. |
# Store the splitter value. |
167 |
$retVal->{splitter} = (defined($splitter) ? $splitter : '::'); |
$retVal->{splitter} = $options{splitter} || '::'; |
168 |
|
# Store the user name. |
169 |
|
$retVal->{user} = $options{user} || '<unknown>'; |
170 |
|
Trace("User $retVal->{user} selected for attribute object.") if T(3); |
171 |
# Return the result. |
# Return the result. |
172 |
return $retVal; |
return $retVal; |
173 |
} |
} |
182 |
|
|
183 |
=item attributeName |
=item attributeName |
184 |
|
|
185 |
Name of the attribute. It must be a valid ERDB field name, consisting entirely of |
Name of the attribute (the real key). If it does not exist already, it will be created. |
|
letters, digits, and hyphens, with a letter at the beginning. If it does not |
|
|
exist already, it will be created. |
|
186 |
|
|
187 |
=item type |
=item type |
188 |
|
|
209 |
# Get the data type hash. |
# Get the data type hash. |
210 |
my %types = ERDB::GetDataTypes(); |
my %types = ERDB::GetDataTypes(); |
211 |
# Validate the initial input values. |
# Validate the initial input values. |
212 |
if (! ERDB::ValidateFieldName($attributeName)) { |
if ($attributeName =~ /$self->{splitter}/) { |
213 |
Confess("Invalid attribute name \"$attributeName\" specified."); |
Confess("Invalid attribute name \"$attributeName\" specified."); |
214 |
} elsif (! $notes || length($notes) < 25) { |
} elsif (! $notes || length($notes) < 25) { |
215 |
Confess("Missing or incomplete description for $attributeName."); |
Confess("Missing or incomplete description for $attributeName."); |
216 |
} elsif (! exists $types{$type}) { |
} elsif (! exists $types{$type}) { |
217 |
Confess("Invalid data type \"$type\" for $attributeName."); |
Confess("Invalid data type \"$type\" for $attributeName."); |
218 |
} else { |
} else { |
219 |
|
# Create a variable to hold the action to be displayed for the log (Add or Update). |
220 |
|
my $action; |
221 |
# Okay, we're ready to begin. See if this key exists. |
# Okay, we're ready to begin. See if this key exists. |
222 |
my $attribute = $self->GetEntity('AttributeKey', $attributeName); |
my $attribute = $self->GetEntity('AttributeKey', $attributeName); |
223 |
if (defined($attribute)) { |
if (defined($attribute)) { |
224 |
# It does, so we do an update. |
# It does, so we do an update. |
225 |
|
$action = "Update Key"; |
226 |
$self->UpdateEntity('AttributeKey', $attributeName, |
$self->UpdateEntity('AttributeKey', $attributeName, |
227 |
{ description => $notes, 'data-type' => $type }); |
{ description => $notes, 'data-type' => $type }); |
228 |
# Detach the key from its current groups. |
# Detach the key from its current groups. |
229 |
$self->Disconnect('IsInGroup', 'AttributeKey', $attributeName); |
$self->Disconnect('IsInGroup', 'AttributeKey', $attributeName); |
230 |
} else { |
} else { |
231 |
# It doesn't, so we do an insert. |
# It doesn't, so we do an insert. |
232 |
|
$action = "Insert Key"; |
233 |
$self->InsertObject('AttributeKey', { id => $attributeName, |
$self->InsertObject('AttributeKey', { id => $attributeName, |
234 |
description => $notes, 'data-type' => $type }); |
description => $notes, 'data-type' => $type }); |
235 |
} |
} |
239 |
$self->InsertObject('IsInGroup', { 'from-link' => $attributeName, |
$self->InsertObject('IsInGroup', { 'from-link' => $attributeName, |
240 |
'to-link' => $group }); |
'to-link' => $group }); |
241 |
} |
} |
242 |
|
# Log the operation. |
243 |
|
$self->LogOperation($action, $attributeName, "Group list is " . join(" ", @{$groups})); |
244 |
} |
} |
245 |
} |
} |
246 |
|
|
|
=head3 LoadAttributeKey |
|
|
|
|
|
C<< my $stats = $attrDB->LoadAttributeKey($keyName, $fh, $keyCol, $dataCol, %options); >> |
|
|
|
|
|
Load the specified attribute from the specified file. The file should be a |
|
|
tab-delimited file with internal tab and new-line characters escaped. This is |
|
|
the typical TBL-style file used by most FIG applications. One of the columns |
|
|
in the input file must contain the appropriate object id value and the other the |
|
|
corresponding attribute value. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item keyName |
|
|
|
|
|
Key of the attribute to load. |
|
|
|
|
|
=item fh |
|
|
|
|
|
Open file handle for the input file. |
|
|
|
|
|
=item idCol |
|
|
|
|
|
Index (0-based) of the column containing the ID field. The ID field should |
|
|
contain the ID of an instance of the named entity. |
|
|
|
|
|
=item dataCol |
|
|
|
|
|
Index (0-based) of the column containing the data value field. |
|
|
|
|
|
=item options |
|
|
|
|
|
Hash specifying the options for this load. |
|
|
|
|
|
=item RETURN |
|
|
|
|
|
Returns a statistics object for the load process. |
|
|
|
|
|
=back |
|
|
|
|
|
The available options are as follows. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item erase |
|
|
|
|
|
If TRUE, the key's values will all be erased before loading. (Doing so |
|
|
makes for a faster load.) |
|
|
|
|
|
=back |
|
|
|
|
|
=cut |
|
|
|
|
|
sub LoadAttributeKey { |
|
|
# Get the parameters. |
|
|
my ($self, $keyName, $fh, $idCol, $dataCol, %options) = @_; |
|
|
# Create the return variable. |
|
|
my $retVal = Stats->new("lineIn", "shortLine", "newObject"); |
|
|
# Compute the minimum number of fields required in each input line. |
|
|
my $minCols = ($idCol < $dataCol ? $idCol : $idCol) + 1; |
|
|
# Insure the attribute key exists. |
|
|
my $found = $self->GetEntity('AttributeKey', $keyName); |
|
|
if (! defined $found) { |
|
|
Confess("Attribute key \"$keyName\" not found in database."); |
|
|
} else { |
|
|
# Erase the key's current values. |
|
|
$self->EraseAttribute($keyName); |
|
|
# Save a list of the object IDs we need to add. |
|
|
my %objectIDs = (); |
|
|
# Loop through the input file. |
|
|
while (! eof $fh) { |
|
|
# Get the next line of the file. |
|
|
my @fields = Tracer::GetLine($fh); |
|
|
$retVal->Add(lineIn => 1); |
|
|
# Now we need to validate the line. |
|
|
if (scalar(@fields) < $minCols) { |
|
|
$retVal->Add(shortLine => 1); |
|
|
} else { |
|
|
# It's valid, so get the ID and value. |
|
|
my ($id, $value) = ($fields[$idCol], $fields[$dataCol]); |
|
|
# Denote we're using this input line. |
|
|
$retVal->Add(lineUsed => 1); |
|
|
# Now the fun begins. Find out if we need to create a target object record for this object ID. |
|
|
if (! exists $objectIDs{$id}) { |
|
|
my $found = $self->Exists('TargetObject', $id); |
|
|
if (! $found) { |
|
|
$self->InsertObject('TargetObject', { id => $id }); |
|
|
} |
|
|
$objectIDs{$id} = 1; |
|
|
$retVal->Add(newObject => 1); |
|
|
} |
|
|
# Now we insert the attribute. |
|
|
$self->InsertObject('HasValueFor', { from => $keyName, to => $id, value => $value }); |
|
|
$retVal->Add(newValue => 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
# Return the statistics. |
|
|
return $retVal; |
|
|
} |
|
|
|
|
247 |
|
|
248 |
=head3 DeleteAttributeKey |
=head3 DeleteAttributeKey |
249 |
|
|
270 |
my ($self, $attributeName) = @_; |
my ($self, $attributeName) = @_; |
271 |
# Delete the attribute key. |
# Delete the attribute key. |
272 |
my $retVal = $self->Delete('AttributeKey', $attributeName); |
my $retVal = $self->Delete('AttributeKey', $attributeName); |
273 |
|
# Log this operation. |
274 |
|
$self->LogOperation("Delete Key", $attributeName, "Key will no longer be available for use by anyone."); |
275 |
# Return the result. |
# Return the result. |
276 |
return $retVal; |
return $retVal; |
277 |
|
|
345 |
-labels => \%labelMap, |
-labels => \%labelMap, |
346 |
-default => 'string'); |
-default => 'string'); |
347 |
# Allow the user to specify a new field name. This is required if the |
# Allow the user to specify a new field name. This is required if the |
348 |
# user has selected the "(new)" marker. We put a little scriptlet in here that |
# user has selected the "(new)" marker. |
|
# selects the (new) marker when the user enters the field. |
|
|
push @retVal, "<script language=\"javaScript\">"; |
|
349 |
my $fieldField = "document.$name.fieldName"; |
my $fieldField = "document.$name.fieldName"; |
350 |
my $newName = "\"" . NewName() . "\""; |
my $newName = "\"" . NewName() . "\""; |
351 |
push @retVal, $cgi->Tr($cgi->th("New Field Name"), |
push @retVal, $cgi->Tr($cgi->th("New Field Name"), |
368 |
$cgi->td($cgi->checkbox_group(-name=>'groups', |
$cgi->td($cgi->checkbox_group(-name=>'groups', |
369 |
-values=> \@groups)) |
-values=> \@groups)) |
370 |
); |
); |
371 |
# If the user wants to upload new values for the field, then we have |
# Now the four buttons: STORE, SHOW, ERASE, and DELETE. |
|
# an upload file name and column indicators. |
|
|
push @retVal, $cgi->Tr($cgi->th("Upload Values"), |
|
|
$cgi->td($cgi->filefield(-name => 'newValueFile', |
|
|
-size => 20) . |
|
|
" Key " . |
|
|
$cgi->textfield(-name => 'keyCol', |
|
|
-size => 3, |
|
|
-default => 0) . |
|
|
" Value " . |
|
|
$cgi->textfield(-name => 'valueCol', |
|
|
-size => 3, |
|
|
-default => 1) |
|
|
), |
|
|
); |
|
|
# Now the three buttons: STORE, SHOW, and DELETE. |
|
372 |
push @retVal, $cgi->Tr($cgi->th(" "), |
push @retVal, $cgi->Tr($cgi->th(" "), |
373 |
$cgi->td({align => 'center'}, |
$cgi->td({align => 'center'}, join(" ", |
374 |
$cgi->submit(-name => 'Delete', -value => 'DELETE') . " " . |
$cgi->submit(-name => 'Delete', -value => 'DELETE'), |
375 |
$cgi->submit(-name => 'Store', -value => 'STORE') . " " . |
$cgi->submit(-name => 'Store', -value => 'STORE'), |
376 |
|
$cgi->submit(-name => 'Erase', -value => 'ERASE'), |
377 |
$cgi->submit(-name => 'Show', -value => 'SHOW') |
$cgi->submit(-name => 'Show', -value => 'SHOW') |
378 |
) |
)) |
379 |
); |
); |
380 |
# Close the table and the form. |
# Close the table and the form. |
381 |
push @retVal, $cgi->end_table(); |
push @retVal, $cgi->end_table(); |
386 |
=head3 LoadAttributesFrom |
=head3 LoadAttributesFrom |
387 |
|
|
388 |
C<< my $stats = $attrDB->LoadAttributesFrom($fileName, %options); >> |
C<< my $stats = $attrDB->LoadAttributesFrom($fileName, %options); >> |
389 |
|
s |
390 |
Load attributes from the specified tab-delimited file. Each line of the file must |
Load attributes from the specified tab-delimited file. Each line of the file must |
391 |
contain an object ID in the first column, an attribute key name in the second |
contain an object ID in the first column, an attribute key name in the second |
392 |
column, and attribute values in the remaining columns. The attribute values will |
column, and attribute values in the remaining columns. The attribute values will |
393 |
be assembled into a single value using the splitter code. |
be assembled into a single value using the splitter code. In addition, the key names may |
394 |
|
contain a splitter. If this is the case, the portion of the key after the splitter is |
395 |
|
treated as a subkey. |
396 |
|
|
397 |
=over 4 |
=over 4 |
398 |
|
|
399 |
=item fileName |
=item fileName |
400 |
|
|
401 |
Name of the file from which to load the attributes. |
Name of the file from which to load the attributes, or an open handle for the file. |
402 |
|
(This last enables the method to be used in conjunction with the CGI form upload |
403 |
|
control.) |
404 |
|
|
405 |
=item options |
=item options |
406 |
|
|
421 |
If TRUE, then the attributes will be appended to existing data; otherwise, the |
If TRUE, then the attributes will be appended to existing data; otherwise, the |
422 |
first time a key name is encountered, it will be erased. |
first time a key name is encountered, it will be erased. |
423 |
|
|
424 |
|
=item archive |
425 |
|
|
426 |
|
If specified, the name of a file into which the incoming data file should be saved. |
427 |
|
|
428 |
|
=item objectType |
429 |
|
|
430 |
|
If specified, the specified object type will be prefixed to each object ID. |
431 |
|
|
432 |
|
=item resume |
433 |
|
|
434 |
|
If specified, key-value pairs already in the database will not be reinserted. |
435 |
|
|
436 |
=back |
=back |
437 |
|
|
438 |
=cut |
=cut |
442 |
my ($self, $fileName, %options) = @_; |
my ($self, $fileName, %options) = @_; |
443 |
# Declare the return variable. |
# Declare the return variable. |
444 |
my $retVal = Stats->new('keys', 'values'); |
my $retVal = Stats->new('keys', 'values'); |
445 |
|
# Initialize the timers. |
446 |
|
my ($insertTime, $eraseTime, $archiveTime, $checkTime) = (0, 0, 0, 0); |
447 |
# Check for append mode. |
# Check for append mode. |
448 |
my $append = ($options{append} ? 1 : 0); |
my $append = ($options{append} ? 1 : 0); |
449 |
|
# Check for resume mode. |
450 |
|
my $resume = ($options{resume} ? 1 : 0); |
451 |
# Create a hash of key names found. |
# Create a hash of key names found. |
452 |
my %keyHash = (); |
my %keyHash = (); |
453 |
# Open the file for input. |
# Open the file for input. Note we must anticipate the possibility of an |
454 |
my $fh = Open(undef, "<$fileName"); |
# open filehandle being passed in. |
455 |
|
my $fh; |
456 |
|
if (ref $fileName) { |
457 |
|
Trace("Using file opened by caller.") if T(3); |
458 |
|
$fh = $fileName; |
459 |
|
} else { |
460 |
|
Trace("Attributes will be loaded from $fileName.") if T(3); |
461 |
|
$fh = Open(undef, "<$fileName"); |
462 |
|
} |
463 |
|
# Now check to see if we need to archive. |
464 |
|
my $ah; |
465 |
|
if ($options{archive}) { |
466 |
|
$ah = Open(undef, ">$options{archive}"); |
467 |
|
Trace("Load file will be archived to $options{archive}.") if T(3); |
468 |
|
} |
469 |
|
# Insure we recover from errors. |
470 |
|
eval { |
471 |
# Loop through the file. |
# Loop through the file. |
472 |
while (! eof $fh) { |
while (! eof $fh) { |
473 |
|
# Read the current line. |
474 |
my ($id, $key, @values) = Tracer::GetLine($fh); |
my ($id, $key, @values) = Tracer::GetLine($fh); |
475 |
$retVal->Add(linesIn => 1); |
$retVal->Add(linesIn => 1); |
476 |
|
# Check to see if we need to fix up the object ID. |
477 |
|
if ($options{objectType}) { |
478 |
|
$id = "$options{objectType}:$id"; |
479 |
|
} |
480 |
|
# Archive the line (if necessary). |
481 |
|
if (defined $ah) { |
482 |
|
my $startTime = time(); |
483 |
|
Tracer::PutLine($ah, [$id, $key, @values]); |
484 |
|
$archiveTime += time() - $startTime; |
485 |
|
} |
486 |
# Do some validation. |
# Do some validation. |
487 |
if (! defined($id)) { |
if (! $id) { |
488 |
# We ignore blank lines. |
# We ignore blank lines. |
489 |
$retVal->Add(blankLines => 1); |
$retVal->Add(blankLines => 1); |
490 |
|
} elsif (substr($id, 0, 1) eq '#') { |
491 |
|
# A line beginning with a pound sign is a comment. |
492 |
|
$retVal->Add(comments => 1); |
493 |
} elsif (! defined($key)) { |
} elsif (! defined($key)) { |
494 |
# An ID without a key is a serious error. |
# An ID without a key is a serious error. |
495 |
my $lines = $retVal->Ask('linesIn'); |
my $lines = $retVal->Ask('linesIn'); |
496 |
Confess("Line $lines in $fileName has no attribute key."); |
Confess("Line $lines in $fileName has no attribute key."); |
497 |
|
} elsif (! @values) { |
498 |
|
# A line with no values is not allowed. |
499 |
|
my $lines = $retVal->Ask('linesIn'); |
500 |
|
Trace("Line $lines for key $key has no attribute values.") if T(1); |
501 |
|
$retVal->Add(skipped => 1); |
502 |
} else { |
} else { |
503 |
|
# The key contains a real part and an optional sub-part. We need the real part. |
504 |
|
my ($realKey, $subKey) = $self->SplitKey($key); |
505 |
# Now we need to check for a new key. |
# Now we need to check for a new key. |
506 |
if (! exists $keyHash{$key}) { |
if (! exists $keyHash{$realKey}) { |
507 |
# This is a new key. Verify that it exists. |
if (! $self->Exists('AttributeKey', $realKey)) { |
|
if (! $self->Exists('AttributeKey', $key)) { |
|
508 |
my $line = $retVal->Ask('linesIn'); |
my $line = $retVal->Ask('linesIn'); |
509 |
Confess("Attribute \"$key\" on line $line of $fileName not found in database."); |
Confess("Attribute \"$realKey\" on line $line of $fileName not found in database."); |
510 |
} else { |
} else { |
511 |
# Make sure we know this is no longer a new key. |
# Make sure we know this is no longer a new key. |
512 |
$keyHash{$key} = 1; |
$keyHash{$realKey} = 1; |
513 |
$retVal->Add(keys => 1); |
$retVal->Add(keys => 1); |
514 |
# If this is NOT append mode, erase the key. |
# If this is NOT append mode, erase the key. |
515 |
if (! $append) { |
if (! $append) { |
516 |
$self->EraseAttribute($key); |
my $startTime = time(); |
517 |
} |
$self->EraseAttribute($realKey); |
518 |
} |
$eraseTime += time() - $startTime; |
519 |
Trace("Key $key found.") if T(3); |
Trace("Attribute $realKey erased.") if T(3); |
520 |
} |
} |
521 |
# Now we know the key is valid. Add this value. |
} |
522 |
|
Trace("Key $realKey found.") if T(3); |
523 |
|
} |
524 |
|
# If we're in resume mode, check to see if this insert is redundant. |
525 |
|
my $ok = 1; |
526 |
|
if ($resume) { |
527 |
|
my $startTime = time(); |
528 |
|
my $count = $self->GetAttributes($id, $key, @values); |
529 |
|
$ok = ! $count; |
530 |
|
$checkTime += time() - $startTime; |
531 |
|
} |
532 |
|
if ($ok) { |
533 |
|
# Everything is all set up, so add the value. |
534 |
|
my $startTime = time(); |
535 |
$self->AddAttribute($id, $key, @values); |
$self->AddAttribute($id, $key, @values); |
536 |
|
$insertTime += time() - $startTime; |
537 |
|
} else { |
538 |
|
# Here we skipped because of resume mode. |
539 |
|
$retVal->Add(resumeSkip => 1); |
540 |
|
} |
541 |
|
|
542 |
my $progress = $retVal->Add(values => 1); |
my $progress = $retVal->Add(values => 1); |
543 |
Trace("$progress values loaded.") if T(3) && ($progress % 1000 == 0); |
Trace("$progress values loaded.") if T(3) && ($progress % 1000 == 0); |
544 |
|
} |
545 |
|
} |
546 |
|
$retVal->Add(eraseTime => $eraseTime); |
547 |
|
$retVal->Add(insertTime => $insertTime); |
548 |
|
$retVal->Add(archiveTime => $archiveTime); |
549 |
|
$retVal->Add(checkTime => $checkTime); |
550 |
|
}; |
551 |
|
# Check for an error. |
552 |
|
if ($@) { |
553 |
|
# Here we have an error. Display the error message. |
554 |
|
my $message = $@; |
555 |
|
Trace("Error during attribute load: $message") if T(0); |
556 |
|
$retVal->AddMessage($message); |
557 |
|
} |
558 |
|
# Close the archive file, if any. |
559 |
|
if (defined $ah) { |
560 |
|
Trace("Closing archive file $options{archive}.") if T(2); |
561 |
|
close $ah; |
562 |
|
} |
563 |
|
# Return the result. |
564 |
|
return $retVal; |
565 |
|
} |
566 |
|
|
567 |
|
=head3 BackupKeys |
568 |
|
|
569 |
|
C<< my $stats = $attrDB->BackupKeys($fileName, %options); >> |
570 |
|
|
571 |
|
Backup the attribute key information from the attribute database. |
572 |
|
|
573 |
|
=over 4 |
574 |
|
|
575 |
|
=item fileName |
576 |
|
|
577 |
|
Name of the output file. |
578 |
|
|
579 |
|
=item options |
580 |
|
|
581 |
|
Options for modifying the backup process. |
582 |
|
|
583 |
|
=item RETURN |
584 |
|
|
585 |
|
Returns a statistics object for the backup. |
586 |
|
|
587 |
|
=back |
588 |
|
|
589 |
|
Currently there are no options. The backup is straight to a text file in |
590 |
|
tab-delimited format. Each key is backup up to two lines. The first line |
591 |
|
is all of the data from the B<AttributeKey> table. The second is a |
592 |
|
tab-delimited list of all the groups. |
593 |
|
|
594 |
|
=cut |
595 |
|
|
596 |
|
sub BackupKeys { |
597 |
|
# Get the parameters. |
598 |
|
my ($self, $fileName, %options) = @_; |
599 |
|
# Declare the return variable. |
600 |
|
my $retVal = Stats->new(); |
601 |
|
# Open the output file. |
602 |
|
my $fh = Open(undef, ">$fileName"); |
603 |
|
# Set up to read the keys. |
604 |
|
my $keyQuery = $self->Get(['AttributeKey'], "", []); |
605 |
|
# Loop through the keys. |
606 |
|
while (my $keyData = $keyQuery->Fetch()) { |
607 |
|
$retVal->Add(key => 1); |
608 |
|
# Get the fields. |
609 |
|
my ($id, $type, $description) = $keyData->Values(['AttributeKey(id)', 'AttributeKey(data-type)', |
610 |
|
'AttributeKey(description)']); |
611 |
|
# Escape any tabs or new-lines in the description. |
612 |
|
my $escapedDescription = Tracer::Escape($description); |
613 |
|
# Write the key data to the output. |
614 |
|
Tracer::PutLine($fh, [$id, $type, $escapedDescription]); |
615 |
|
# Get the key's groups. |
616 |
|
my @groups = $self->GetFlat(['IsInGroup'], "IsInGroup(from-link) = ?", [$id], |
617 |
|
'IsInGroup(to-link)'); |
618 |
|
$retVal->Add(memberships => scalar(@groups)); |
619 |
|
# Write them to the output. Note we put a marker at the beginning to insure the line |
620 |
|
# is nonempty. |
621 |
|
Tracer::PutLine($fh, ['#GROUPS', @groups]); |
622 |
} |
} |
623 |
|
# Log the operation. |
624 |
|
$self->LogOperation("Backup Keys", $fileName, $retVal->Display()); |
625 |
|
# Return the result. |
626 |
|
return $retVal; |
627 |
} |
} |
628 |
|
|
629 |
|
=head3 RestoreKeys |
630 |
|
|
631 |
|
C<< my $stats = $attrDB->RestoreKeys($fileName, %options); >> |
632 |
|
|
633 |
|
Restore the attribute keys and groups from a backup file. |
634 |
|
|
635 |
|
=over 4 |
636 |
|
|
637 |
|
=item fileName |
638 |
|
|
639 |
|
Name of the file containing the backed-up keys. Each key has a pair of lines, |
640 |
|
one containing the key data and one listing its groups. |
641 |
|
|
642 |
|
=back |
643 |
|
|
644 |
|
=cut |
645 |
|
|
646 |
|
sub RestoreKeys { |
647 |
|
# Get the parameters. |
648 |
|
my ($self, $fileName, %options) = @_; |
649 |
|
# Declare the return variable. |
650 |
|
my $retVal = Stats->new(); |
651 |
|
# Set up a hash to hold the group IDs. |
652 |
|
my %groups = (); |
653 |
|
# Open the file. |
654 |
|
my $fh = Open(undef, "<$fileName"); |
655 |
|
# Loop until we're done. |
656 |
|
while (! eof $fh) { |
657 |
|
# Get a key record. |
658 |
|
my ($id, $dataType, $description) = Tracer::GetLine($fh); |
659 |
|
if ($id eq '#GROUPS') { |
660 |
|
Confess("Group record found when key record expected."); |
661 |
|
} elsif (! defined($description)) { |
662 |
|
Confess("Invalid format found for key record."); |
663 |
|
} else { |
664 |
|
$retVal->Add("keyIn" => 1); |
665 |
|
# Add this key to the database. |
666 |
|
$self->InsertObject('AttributeKey', { id => $id, 'data-type' => $dataType, |
667 |
|
description => Tracer::UnEscape($description) }); |
668 |
|
Trace("Attribute $id stored.") if T(3); |
669 |
|
# Get the group line. |
670 |
|
my ($marker, @groups) = Tracer::GetLine($fh); |
671 |
|
if (! defined($marker)) { |
672 |
|
Confess("End of file found where group record expected."); |
673 |
|
} elsif ($marker ne '#GROUPS') { |
674 |
|
Confess("Group record not found after key record."); |
675 |
|
} else { |
676 |
|
$retVal->Add(memberships => scalar(@groups)); |
677 |
|
# Connect the groups. |
678 |
|
for my $group (@groups) { |
679 |
|
# Find out if this is a new group. |
680 |
|
if (! $groups{$group}) { |
681 |
|
$retVal->Add(newGroup => 1); |
682 |
|
# Add the group. |
683 |
|
$self->InsertObject('AttributeGroup', { id => $group }); |
684 |
|
Trace("Group $group created.") if T(3); |
685 |
|
# Make sure we know it's not new. |
686 |
|
$groups{$group} = 1; |
687 |
|
} |
688 |
|
# Connect the group to our key. |
689 |
|
$self->InsertObject('IsInGroup', { 'from-link' => $id, 'to-link' => $group }); |
690 |
|
} |
691 |
|
Trace("$id added to " . scalar(@groups) . " groups.") if T(3); |
692 |
|
} |
693 |
|
} |
694 |
|
} |
695 |
|
# Log the operation. |
696 |
|
$self->LogOperation("Backup Keys", $fileName, $retVal->Display()); |
697 |
|
# Return the result. |
698 |
|
return $retVal; |
699 |
|
} |
700 |
|
|
701 |
|
=head3 ArchiveFileName |
702 |
|
|
703 |
|
C<< my $fileName = $ca->ArchiveFileName(); >> |
704 |
|
|
705 |
|
Compute a file name for archiving attribute input data. The file will be in the attribute log directory |
706 |
|
|
707 |
|
=cut |
708 |
|
|
709 |
|
sub ArchiveFileName { |
710 |
|
# Get the parameters. |
711 |
|
my ($self) = @_; |
712 |
|
# Declare the return variable. |
713 |
|
my $retVal; |
714 |
|
# We start by turning the timestamp into something usable as a file name. |
715 |
|
my $now = Tracer::Now(); |
716 |
|
$now =~ tr/ :\//___/; |
717 |
|
# Next we get the directory name. |
718 |
|
my $dir = "$FIG_Config::var/attributes"; |
719 |
|
if (! -e $dir) { |
720 |
|
Trace("Creating attribute file directory $dir.") if T(1); |
721 |
|
mkdir $dir; |
722 |
|
} |
723 |
|
# Put it together with the field name and the time stamp. |
724 |
|
$retVal = "$dir/upload.$now"; |
725 |
|
# Modify the file name to insure it's unique. |
726 |
|
my $seq = 0; |
727 |
|
while (-e "$retVal.$seq.tbl") { $seq++ } |
728 |
|
# Use the computed sequence number to get the correct file name. |
729 |
|
$retVal .= ".$seq.tbl"; |
730 |
# Return the result. |
# Return the result. |
731 |
return $retVal; |
return $retVal; |
732 |
} |
} |
767 |
my @keys = $self->GetFlat(['AttributeKey'], "", [], 'AttributeKey(id)'); |
my @keys = $self->GetFlat(['AttributeKey'], "", [], 'AttributeKey(id)'); |
768 |
Trace(scalar(@keys) . " keys found during backup.") if T(2); |
Trace(scalar(@keys) . " keys found during backup.") if T(2); |
769 |
# Open the file for output. |
# Open the file for output. |
770 |
my $fh = Open(undef, $fileName); |
my $fh = Open(undef, ">$fileName"); |
771 |
# Loop through the keys. |
# Loop through the keys. |
772 |
for my $key (@keys) { |
for my $key (@keys) { |
773 |
Trace("Backing up attribute $key.") if T(3); |
Trace("Backing up attribute $key.") if T(3); |
774 |
$retVal->Add(keys => 1); |
$retVal->Add(keys => 1); |
775 |
# Loop through this key's values. |
# Loop through this key's values. |
776 |
my $query = $self->Get(['HasValueFor'], "HasValueFor(to-link) = ?", [$key]); |
my $query = $self->Get(['HasValueFor'], "HasValueFor(from-link) = ?", [$key]); |
777 |
my $valuesFound = 0; |
my $valuesFound = 0; |
778 |
while (my $line = $query->Fetch()) { |
while (my $line = $query->Fetch()) { |
779 |
$valuesFound++; |
$valuesFound++; |
780 |
# Get this row's data. |
# Get this row's data. |
781 |
my @row = $line->Values(['HasValueFor(from-link)', 'HasValueFor(to-link)', |
my ($id, $key, $subKey, $value) = $line->Values(['HasValueFor(to-link)', |
782 |
|
'HasValueFor(from-link)', |
783 |
|
'HasValueFor(subkey)', |
784 |
'HasValueFor(value)']); |
'HasValueFor(value)']); |
785 |
|
# Check for a subkey. |
786 |
|
if ($subKey ne '') { |
787 |
|
$key = "$key$self->{splitter}$subKey"; |
788 |
|
} |
789 |
# Write it to the file. |
# Write it to the file. |
790 |
Tracer::PutLine($fh, \@row); |
Tracer::PutLine($fh, [$id, $key, $value]); |
791 |
} |
} |
792 |
Trace("$valuesFound values backed up for key $key.") if T(3); |
Trace("$valuesFound values backed up for key $key.") if T(3); |
793 |
$retVal->Add(values => $valuesFound); |
$retVal->Add(values => $valuesFound); |
794 |
} |
} |
795 |
|
# Log the operation. |
796 |
|
$self->LogOperation("Backup Data", $fileName, $retVal->Display()); |
797 |
# Return the result. |
# Return the result. |
798 |
return $retVal; |
return $retVal; |
799 |
} |
} |
1058 |
return %retVal; |
return %retVal; |
1059 |
} |
} |
1060 |
|
|
1061 |
|
=head3 LogOperation |
1062 |
|
|
1063 |
|
C<< $ca->LogOperation($action, $target, $description); >> |
1064 |
|
|
1065 |
|
Write an operation description to the attribute activity log (C<$FIG_Config::var/attributes.log>). |
1066 |
|
|
1067 |
|
=over 4 |
1068 |
|
|
1069 |
|
=item action |
1070 |
|
|
1071 |
|
Action being logged (e.g. C<Delete Group> or C<Load Key>). |
1072 |
|
|
1073 |
|
=item target |
1074 |
|
|
1075 |
|
ID of the key or group affected. |
1076 |
|
|
1077 |
|
=item description |
1078 |
|
|
1079 |
|
Short description of the action. |
1080 |
|
|
1081 |
|
=back |
1082 |
|
|
1083 |
|
=cut |
1084 |
|
|
1085 |
|
sub LogOperation { |
1086 |
|
# Get the parameters. |
1087 |
|
my ($self, $action, $target, $description) = @_; |
1088 |
|
# Get the user ID. |
1089 |
|
my $user = $self->{user}; |
1090 |
|
# Get a timestamp. |
1091 |
|
my $timeString = Tracer::Now(); |
1092 |
|
# Open the log file for appending. |
1093 |
|
my $oh = Open(undef, ">>$FIG_Config::var/attributes.log"); |
1094 |
|
# Write the data to it. |
1095 |
|
Tracer::PutLine($oh, [$timeString, $user, $action, $target, $description]); |
1096 |
|
# Close the log file. |
1097 |
|
close $oh; |
1098 |
|
} |
1099 |
|
|
1100 |
|
=head2 Internal Utility Methods |
1101 |
|
|
1102 |
|
=head3 _KeywordString |
1103 |
|
|
1104 |
|
C<< my $keywordString = $ca->_KeywordString($key, $value); >> |
1105 |
|
|
1106 |
|
Compute the keyword string for a specified key/value pair. This consists of the |
1107 |
|
key name and value converted to lower case with underscores translated to spaces. |
1108 |
|
|
1109 |
|
This method is for internal use only. It is called whenever we need to update or |
1110 |
|
insert a B<HasValueFor> record. |
1111 |
|
|
1112 |
|
=over 4 |
1113 |
|
|
1114 |
|
=item key |
1115 |
|
|
1116 |
|
Name of the relevant attribute key. |
1117 |
|
|
1118 |
|
=item target |
1119 |
|
|
1120 |
|
ID of the target object to which this key/value pair will be associated. |
1121 |
|
|
1122 |
|
=item value |
1123 |
|
|
1124 |
|
The value to store for this key/object combination. |
1125 |
|
|
1126 |
|
=item RETURN |
1127 |
|
|
1128 |
|
Returns the value that should be stored as the keyword string for the specified |
1129 |
|
key/value pair. |
1130 |
|
|
1131 |
|
=back |
1132 |
|
|
1133 |
|
=cut |
1134 |
|
|
1135 |
|
sub _KeywordString { |
1136 |
|
# Get the parameters. |
1137 |
|
my ($self, $key, $value) = @_; |
1138 |
|
# Get a copy of the key name and convert underscores to spaces. |
1139 |
|
my $keywordString = $key; |
1140 |
|
$keywordString =~ s/_/ /g; |
1141 |
|
# Add the value convert it all to lower case. |
1142 |
|
my $retVal = lc "$keywordString $value"; |
1143 |
|
# Return the result. |
1144 |
|
return $retVal; |
1145 |
|
} |
1146 |
|
|
1147 |
|
=head3 _QueryResults |
1148 |
|
|
1149 |
|
C<< my @attributeList = $attrDB->_QueryResults($query, @values); >> |
1150 |
|
|
1151 |
|
Match the results of a B<HasValueFor> query against value criteria and return |
1152 |
|
the results. This is an internal method that splits the values coming back |
1153 |
|
and matches the sections against the specified section patterns. It serves |
1154 |
|
as the back end to L</GetAttributes> and L</FindAttributes>. |
1155 |
|
|
1156 |
|
=over 4 |
1157 |
|
|
1158 |
|
=item query |
1159 |
|
|
1160 |
|
A query object that will return the desired B<HasValueFor> records. |
1161 |
|
|
1162 |
|
=item values |
1163 |
|
|
1164 |
|
List of the desired attribute values, section by section. If C<undef> |
1165 |
|
or an empty string is specified, all values in that section will match. A |
1166 |
|
generic match can be requested by placing a percent sign (C<%>) at the end. |
1167 |
|
In that case, all values that match up to and not including the percent sign |
1168 |
|
will match. You may also specify a regular expression enclosed |
1169 |
|
in slashes. All values that match the regular expression will be returned. For |
1170 |
|
performance reasons, only values have this extra capability. |
1171 |
|
|
1172 |
|
=item RETURN |
1173 |
|
|
1174 |
|
Returns a list of tuples. The first element in the tuple is an object ID, the |
1175 |
|
second is an attribute key, and the remaining elements are the sections of |
1176 |
|
the attribute value. All of the tuples will match the criteria set forth in |
1177 |
|
the parameter list. |
1178 |
|
|
1179 |
|
=back |
1180 |
|
|
1181 |
|
=cut |
1182 |
|
|
1183 |
|
sub _QueryResults { |
1184 |
|
# Get the parameters. |
1185 |
|
my ($self, $query, @values) = @_; |
1186 |
|
# Declare the return value. |
1187 |
|
my @retVal = (); |
1188 |
|
# Get the number of value sections we have to match. |
1189 |
|
my $sectionCount = scalar(@values); |
1190 |
|
# Loop through the assignments found. |
1191 |
|
while (my $row = $query->Fetch()) { |
1192 |
|
# Get the current row's data. |
1193 |
|
my ($id, $realKey, $subKey, $valueString) = $row->Values(['HasValueFor(to-link)', |
1194 |
|
'HasValueFor(from-link)', |
1195 |
|
'HasValueFor(subkey)', |
1196 |
|
'HasValueFor(value)' |
1197 |
|
]); |
1198 |
|
# Form the key from the real key and the sub key. |
1199 |
|
my $key = $self->JoinKey($realKey, $subKey); |
1200 |
|
# Break the value into sections. |
1201 |
|
my @sections = split($self->{splitter}, $valueString); |
1202 |
|
# Match each section against the incoming values. We'll assume we're |
1203 |
|
# okay unless we learn otherwise. |
1204 |
|
my $matching = 1; |
1205 |
|
for (my $i = 0; $i < $sectionCount && $matching; $i++) { |
1206 |
|
# We need to check to see if this section is generic. |
1207 |
|
my $value = $values[$i]; |
1208 |
|
Trace("Current value pattern is \"$value\".") if T(4); |
1209 |
|
if (substr($value, -1, 1) eq '%') { |
1210 |
|
Trace("Generic match used.") if T(4); |
1211 |
|
# Here we have a generic match. |
1212 |
|
my $matchLen = length($values[$i]) - 1; |
1213 |
|
$matching = substr($sections[$i], 0, $matchLen) eq |
1214 |
|
substr($values[$i], 0, $matchLen); |
1215 |
|
} elsif ($value =~ m#^/(.+)/[a-z]*$#) { |
1216 |
|
Trace("Regular expression detected.") if T(4); |
1217 |
|
# Here we have a regular expression match. |
1218 |
|
my $section = $sections[$i]; |
1219 |
|
$matching = eval("\$section =~ $value"); |
1220 |
|
} else { |
1221 |
|
# Here we have a strict match. |
1222 |
|
Trace("Strict match used.") if T(4); |
1223 |
|
$matching = ($sections[$i] eq $values[$i]); |
1224 |
|
} |
1225 |
|
} |
1226 |
|
# If we match, output this row to the return list. |
1227 |
|
if ($matching) { |
1228 |
|
push @retVal, [$id, $key, @sections]; |
1229 |
|
} |
1230 |
|
} |
1231 |
|
# Return the rows found. |
1232 |
|
return @retVal; |
1233 |
|
} |
1234 |
|
|
1235 |
=head2 FIG Method Replacements |
=head2 FIG Method Replacements |
1236 |
|
|
1237 |
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. |
1243 |
The idea is that these methods represent attribute manipulation allowed by all users, while |
The idea is that these methods represent attribute manipulation allowed by all users, while |
1244 |
the others are only for privileged users with access to the attribute server. |
the others are only for privileged users with access to the attribute server. |
1245 |
|
|
1246 |
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 this implementation, |
1247 |
there is only a value. In this implementation, each attribute has only a value. These |
each attribute has only a value. These methods will treat the value as a list with the individual |
1248 |
methods will treat the value as a list with the individual elements separated by the |
elements separated by the value of the splitter parameter on the constructor (L</new>). The default |
1249 |
value of the splitter parameter on the constructor (L</new>). The default is double |
is double colons C<::>. |
|
colons C<::>. |
|
1250 |
|
|
1251 |
So, for example, an old-style keyword with a value of C<essential> and a URL of |
So, for example, an old-style keyword with a value of C<essential> and a URL of |
1252 |
C<http://www.sciencemag.org/cgi/content/abstract/293/5538/2266> using the default |
C<http://www.sciencemag.org/cgi/content/abstract/293/5538/2266> using the default |
1298 |
which has no wildcard in the key or the object ID, may return multiple tuples. |
which has no wildcard in the key or the object ID, may return multiple tuples. |
1299 |
|
|
1300 |
Value matching in this system works very poorly, because of the way multiple values are |
Value matching in this system works very poorly, because of the way multiple values are |
1301 |
stored. For the object ID and key name, we create queries that filter for the desired |
stored. For the object ID, key name, and first value, we create queries that filter for the |
1302 |
results. For the values, we do a comparison after the attributes are retrieved from the |
desired results. On any filtering by value, we must do a comparison after the attributes are |
1303 |
database. As a result, queries in which filter only on value end up reading the entire |
retrieved from the database, since the database has no notion of the multiple values, which |
1304 |
attribute table to find the desired results. |
are stored in a single string. As a result, queries in which filter only on value end up |
1305 |
|
reading a lot more than they need to. |
1306 |
|
|
1307 |
=over 4 |
=over 4 |
1308 |
|
|
1326 |
or an empty string is specified, all values in that section will match. A |
or an empty string is specified, all values in that section will match. A |
1327 |
generic match can be requested by placing a percent sign (C<%>) at the end. |
generic match can be requested by placing a percent sign (C<%>) at the end. |
1328 |
In that case, all values that match up to and not including the percent sign |
In that case, all values that match up to and not including the percent sign |
1329 |
will match. |
will match. You may also specify a regular expression enclosed |
1330 |
|
in slashes. All values that match the regular expression will be returned. For |
1331 |
|
performance reasons, only values have this extra capability. |
1332 |
|
|
1333 |
=item RETURN |
=item RETURN |
1334 |
|
|
1344 |
sub GetAttributes { |
sub GetAttributes { |
1345 |
# Get the parameters. |
# Get the parameters. |
1346 |
my ($self, $objectID, $key, @values) = @_; |
my ($self, $objectID, $key, @values) = @_; |
1347 |
# We will create one big honking query. The following hash will build the filter |
# This hash will map "HasValueFor" fields to patterns. We use it to build the |
1348 |
# clause and a parameter list. |
# SQL statement. |
1349 |
my %data = ('HasValueFor(from-link)' => $key, 'HasValueFor(to-link)' => $objectID); |
my %data; |
1350 |
|
# Before we do anything else, we must parse the key. The key is treated by the |
1351 |
|
# user as a single field, but to us it's actually a real key and a subkey. |
1352 |
|
# If the key has no splitter and is exact, the real key is the original key |
1353 |
|
# and the subkey is an empty string. If the key has a splitter, it is |
1354 |
|
# split into two pieces and each piece is processed separately. If the key has |
1355 |
|
# no splitter and is generic, the real key is the incoming key and the subkey |
1356 |
|
# is allowed to be wild. Of course, this only matters if an actual key has |
1357 |
|
# been specified. |
1358 |
|
if (defined $key) { |
1359 |
|
if ($key =~ /$self->{splitter}/) { |
1360 |
|
# Here we have a two-part key, so we split it normally. |
1361 |
|
my ($realKey, $subKey) = $self->SplitKey($key); |
1362 |
|
$data{'HasValueFor(from-link)'} = $realKey; |
1363 |
|
$data{'HasValueFor(subkey)'} = $subKey; |
1364 |
|
} elsif (substr($key, -1, 1) eq '%') { |
1365 |
|
$data{'HasValueFor(from-link)'} = $key; |
1366 |
|
} else { |
1367 |
|
$data{'HasValueFor(from-link)'} = $key; |
1368 |
|
$data{'HasValueFor(subkey)'} = ''; |
1369 |
|
} |
1370 |
|
} |
1371 |
|
# Add the object ID to the key information. |
1372 |
|
$data{'HasValueFor(to-link)'} = $objectID; |
1373 |
|
# The first value represents a problem, because we can search it using SQL, but not |
1374 |
|
# in the normal way. If the user specifies a generic search or exact match for |
1375 |
|
# every alternative value (remember, the values may be specified as a list), |
1376 |
|
# then we can create SQL filtering for it. If any of the values are specified |
1377 |
|
# as a regular expression, however, that's a problem, because we need to read |
1378 |
|
# every value to verify a match. |
1379 |
|
if (@values > 0) { |
1380 |
|
# Get the first value and put its alternatives in an array. |
1381 |
|
my $valueParm = $values[0]; |
1382 |
|
my @valueList; |
1383 |
|
if (ref $valueParm eq 'ARRAY') { |
1384 |
|
@valueList = @{$valueParm}; |
1385 |
|
} else { |
1386 |
|
@valueList = ($valueParm); |
1387 |
|
} |
1388 |
|
# Okay, now we have all the possible criteria for the first value in the list |
1389 |
|
# @valueList. We'll copy the values to a new array in which they have been |
1390 |
|
# converted to generic requests. If we find a regular-expression match |
1391 |
|
# anywhere in the list, we toss the whole thing. |
1392 |
|
my @valuePatterns = (); |
1393 |
|
my $okValues = 1; |
1394 |
|
for my $valuePattern (@valueList) { |
1395 |
|
# Check the pattern type. |
1396 |
|
if (substr($valuePattern, 0, 1) eq '/') { |
1397 |
|
# Regular expressions invalidate the entire process. |
1398 |
|
$okValues = 0; |
1399 |
|
} elsif (substr($valuePattern, -1, 1) eq '%') { |
1400 |
|
# A Generic pattern is passed in unmodified. |
1401 |
|
push @valuePatterns, $valuePattern; |
1402 |
|
} else { |
1403 |
|
# An exact match is converted to generic. |
1404 |
|
push @valuePatterns, "$valuePattern%"; |
1405 |
|
} |
1406 |
|
} |
1407 |
|
# If everything works, add the value data to the filtering hash. |
1408 |
|
if ($okValues) { |
1409 |
|
$data{'HasValueFor(value)'} = \@valuePatterns; |
1410 |
|
} |
1411 |
|
} |
1412 |
|
# Create some lists to contain the filter fragments and parameter values. |
1413 |
my @filter = (); |
my @filter = (); |
1414 |
my @parms = (); |
my @parms = (); |
1415 |
# This next loop goes through the different fields that can be specified in the |
# This next loop goes through the different fields that can be specified in the |
1416 |
# parameter list and generates filters for each. |
# parameter list and generates filters for each. The %data hash that we built above |
1417 |
|
# contains all the necessary information to do this. |
1418 |
for my $field (keys %data) { |
for my $field (keys %data) { |
1419 |
# Accumulate filter information for this field. We will OR together all the |
# Accumulate filter information for this field. We will OR together all the |
1420 |
# elements accumulated to create the final result. |
# elements accumulated to create the final result. |
1442 |
push @fieldFilter, "$field = ?"; |
push @fieldFilter, "$field = ?"; |
1443 |
push @parms, $pattern; |
push @parms, $pattern; |
1444 |
} else { |
} else { |
1445 |
# Here we have a generate request, so we will use the LIKE operator to |
# Here we have a generic request, so we will use the LIKE operator to |
1446 |
# filter the field to this value pattern. |
# filter the field to this value pattern. |
1447 |
push @fieldFilter, "$field LIKE ?"; |
push @fieldFilter, "$field LIKE ?"; |
1448 |
# We must convert the pattern value to an SQL match pattern. First |
# We must convert the pattern value to an SQL match pattern. First |
1464 |
# Now @filter contains one or more filter strings and @parms contains the parameter |
# Now @filter contains one or more filter strings and @parms contains the parameter |
1465 |
# values to bind to them. |
# values to bind to them. |
1466 |
my $actualFilter = join(" AND ", @filter); |
my $actualFilter = join(" AND ", @filter); |
|
# Declare the return variable. |
|
|
my @retVal = (); |
|
|
# Get the number of value sections we have to match. |
|
|
my $sectionCount = scalar(@values); |
|
1467 |
# Now we're ready to make our query. |
# Now we're ready to make our query. |
1468 |
my $query = $self->Get(['HasValueFor'], $actualFilter, \@parms); |
my $query = $self->Get(['HasValueFor'], $actualFilter, \@parms); |
1469 |
# Loop through the assignments found. |
# Format the results. |
1470 |
while (my $row = $query->Fetch()) { |
my @retVal = $self->_QueryResults($query, @values); |
|
# Get the current row's data. |
|
|
my ($id, $key, $valueString) = $row->Values(['HasValueFor(to-link)', 'HasValueFor(from-link)', |
|
|
'HasValueFor(value)']); |
|
|
# Break the value into sections. |
|
|
my @sections = split($self->{splitter}, $valueString); |
|
|
# Match each section against the incoming values. We'll assume we're |
|
|
# okay unless we learn otherwise. |
|
|
my $matching = 1; |
|
|
for (my $i = 0; $i < $sectionCount && $matching; $i++) { |
|
|
# We need to check to see if this section is generic. |
|
|
if (substr($values[$i], -1, 1) eq '%') { |
|
|
my $matchLen = length($values[$i] - 1); |
|
|
$matching = substr($sections[$i], 0, $matchLen) eq |
|
|
substr($values[$i], 0, $matchLen); |
|
|
} else { |
|
|
$matching = ($sections[$i] eq $values[$i]); |
|
|
} |
|
|
} |
|
|
# If we match, output this row to the return list. |
|
|
if ($matching) { |
|
|
push @retVal, [$id, $key, @sections]; |
|
|
} |
|
|
} |
|
1471 |
# Return the rows found. |
# Return the rows found. |
1472 |
return @retVal; |
return @retVal; |
1473 |
} |
} |
1513 |
# Okay, now we have some reason to believe we can do this. Form the values |
# Okay, now we have some reason to believe we can do this. Form the values |
1514 |
# into a scalar. |
# into a scalar. |
1515 |
my $valueString = join($self->{splitter}, @values); |
my $valueString = join($self->{splitter}, @values); |
1516 |
|
# Split up the key. |
1517 |
|
my ($realKey, $subKey) = $self->SplitKey($key); |
1518 |
# Connect the object to the key. |
# Connect the object to the key. |
1519 |
$self->InsertObject('HasValueFor', { 'from-link' => $key, |
$self->InsertObject('HasValueFor', { 'from-link' => $realKey, |
1520 |
'to-link' => $objectID, |
'to-link' => $objectID, |
1521 |
|
'subkey' => $subKey, |
1522 |
'value' => $valueString, |
'value' => $valueString, |
1523 |
}); |
}); |
1524 |
} |
} |
1559 |
Confess("No object ID specified for DeleteAttribute call."); |
Confess("No object ID specified for DeleteAttribute call."); |
1560 |
} elsif (! defined($key)) { |
} elsif (! defined($key)) { |
1561 |
Confess("No attribute key specified for DeleteAttribute call."); |
Confess("No attribute key specified for DeleteAttribute call."); |
1562 |
} elsif (scalar(@values) == 0) { |
} else { |
1563 |
# Here we erase the entire key. |
# Split the key into the real key and the subkey. |
1564 |
$self->EraseAttribute($key); |
my ($realKey, $subKey) = $self->SplitKey($key); |
1565 |
|
if ($subKey eq '' && scalar(@values) == 0) { |
1566 |
|
# Here we erase the entire key for this object. |
1567 |
|
$self->DeleteRow('HasValueFor', $key, $objectID); |
1568 |
} else { |
} else { |
1569 |
# Here we erase the matching values. |
# Here we erase the matching values. |
1570 |
my $valueString = join($self->{splitter}, @values); |
my $valueString = join($self->{splitter}, @values); |
1571 |
$self->DeleteRow('HasValueFor', $key, $objectID, { value => $valueString }); |
$self->DeleteRow('HasValueFor', $realKey, $objectID, |
1572 |
|
{ subkey => $subKey, value => $valueString }); |
1573 |
|
} |
1574 |
} |
} |
1575 |
# Return a one. This is for backward compatability. |
# Return a one. This is for backward compatability. |
1576 |
return 1; |
return 1; |
1577 |
} |
} |
1578 |
|
|
1579 |
|
=head3 DeleteMatchingAttributes |
1580 |
|
|
1581 |
|
C<< my @deleted = $attrDB->DeleteMatchingAttributes($objectID, $key, @values); >> |
1582 |
|
|
1583 |
|
Delete all attributes that match the specified criteria. This is equivalent to |
1584 |
|
calling L</GetAttributes> and then invoking L</DeleteAttribute> for each |
1585 |
|
row found. |
1586 |
|
|
1587 |
|
=over 4 |
1588 |
|
|
1589 |
|
=item objectID |
1590 |
|
|
1591 |
|
ID of object whose attributes are to be deleted. If the attributes for multiple |
1592 |
|
objects are to be deleted, this parameter can be specified as a list reference. If |
1593 |
|
attributes are to be deleted for all objects, specify C<undef> or an empty string. |
1594 |
|
Finally, you can delete attributes for a range of object IDs by putting a percent |
1595 |
|
sign (C<%>) at the end. |
1596 |
|
|
1597 |
|
=item key |
1598 |
|
|
1599 |
|
Attribute key name. A value of C<undef> or an empty string will match all |
1600 |
|
attribute keys. If the values are to be deletedfor multiple keys, this parameter can be |
1601 |
|
specified as a list reference. Finally, you can delete attributes for a range of |
1602 |
|
keys by putting a percent sign (C<%>) at the end. |
1603 |
|
|
1604 |
|
=item values |
1605 |
|
|
1606 |
|
List of the desired attribute values, section by section. If C<undef> |
1607 |
|
or an empty string is specified, all values in that section will match. A |
1608 |
|
generic match can be requested by placing a percent sign (C<%>) at the end. |
1609 |
|
In that case, all values that match up to and not including the percent sign |
1610 |
|
will match. You may also specify a regular expression enclosed |
1611 |
|
in slashes. All values that match the regular expression will be deleted. For |
1612 |
|
performance reasons, only values have this extra capability. |
1613 |
|
|
1614 |
|
=item RETURN |
1615 |
|
|
1616 |
|
Returns a list of tuples for the attributes that were deleted, in the |
1617 |
|
same form as L</GetAttributes>. |
1618 |
|
|
1619 |
|
=back |
1620 |
|
|
1621 |
|
=cut |
1622 |
|
|
1623 |
|
sub DeleteMatchingAttributes { |
1624 |
|
# Get the parameters. |
1625 |
|
my ($self, $objectID, $key, @values) = @_; |
1626 |
|
# Get the matching attributes. |
1627 |
|
my @retVal = $self->GetAttributes($objectID, $key, @values); |
1628 |
|
# Loop through the attributes, deleting them. |
1629 |
|
for my $tuple (@retVal) { |
1630 |
|
$self->DeleteAttribute(@{$tuple}); |
1631 |
|
} |
1632 |
|
# Log this operation. |
1633 |
|
my $count = @retVal; |
1634 |
|
$self->LogOperation("Mass Delete", $key, "$count matching attributes deleted."); |
1635 |
|
# Return the deleted attributes. |
1636 |
|
return @retVal; |
1637 |
|
} |
1638 |
|
|
1639 |
=head3 ChangeAttribute |
=head3 ChangeAttribute |
1640 |
|
|
1641 |
C<< $attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >> |
C<< $attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >> |
1699 |
|
|
1700 |
=item key |
=item key |
1701 |
|
|
1702 |
Key to erase. |
Key to erase. This must be a real key; that is, it cannot have a subkey |
1703 |
|
component. |
1704 |
|
|
1705 |
=back |
=back |
1706 |
|
|
1709 |
sub EraseAttribute { |
sub EraseAttribute { |
1710 |
# Get the parameters. |
# Get the parameters. |
1711 |
my ($self, $key) = @_; |
my ($self, $key) = @_; |
1712 |
# Delete everything connected to the key. The "keepRoot" option keeps the key in the |
# Delete everything connected to the key. |
1713 |
# datanase while deleting everything attached to it. |
$self->Disconnect('HasValueFor', 'AttributeKey', $key); |
1714 |
$self->Delete('AttributeKey', $key, keepRoot => 1); |
# Log the operation. |
1715 |
|
$self->LogOperation("Erase Data", $key); |
1716 |
# Return a 1, for backward compatability. |
# Return a 1, for backward compatability. |
1717 |
return 1; |
return 1; |
1718 |
} |
} |
1747 |
return sort @groups; |
return sort @groups; |
1748 |
} |
} |
1749 |
|
|
1750 |
|
=head3 QueryAttributes |
1751 |
|
|
1752 |
|
C<< my @attributeData = $ca->QueryAttributes($filter, $filterParms); >> |
1753 |
|
|
1754 |
|
Return the attribute data based on an SQL filter clause. In the filter clause, |
1755 |
|
the name C<$object> should be used for the object ID, C<$key> should be used for |
1756 |
|
the key name, C<$subkey> for the subkey value, and C<$value> for the value field. |
1757 |
|
|
1758 |
|
=over 4 |
1759 |
|
|
1760 |
|
=item filter |
1761 |
|
|
1762 |
|
Filter clause in the standard ERDB format, except that the field names are C<$object> for |
1763 |
|
the object ID field, C<$key> for the key name field, C<$subkey> for the subkey field, |
1764 |
|
and C<$value> for the value field. This abstraction enables us to hide the details of |
1765 |
|
the database construction from the user. |
1766 |
|
|
1767 |
|
=item filterParms |
1768 |
|
|
1769 |
|
Parameters for the filter clause. |
1770 |
|
|
1771 |
|
=item RETURN |
1772 |
|
|
1773 |
|
Returns a list of tuples. Each tuple consists of an object ID, a key (with optional subkey), and |
1774 |
|
one or more attribute values. |
1775 |
|
|
1776 |
|
=back |
1777 |
|
|
1778 |
|
=cut |
1779 |
|
|
1780 |
|
# This hash is used to drive the substitution process. |
1781 |
|
my %AttributeParms = (object => 'HasValueFor(to-link)', |
1782 |
|
key => 'HasValueFor(from-link)', |
1783 |
|
subkey => 'HasValueFor(subkey)', |
1784 |
|
value => 'HasValueFor(value)'); |
1785 |
|
|
1786 |
|
sub QueryAttributes { |
1787 |
|
# Get the parameters. |
1788 |
|
my ($self, $filter, $filterParms) = @_; |
1789 |
|
# Declare the return variable. |
1790 |
|
my @retVal = (); |
1791 |
|
# Make sue we have filter parameters. |
1792 |
|
my $realParms = (defined($filterParms) ? $filterParms : []); |
1793 |
|
# Create the query by converting the filter. |
1794 |
|
my $realFilter = $filter; |
1795 |
|
for my $name (keys %AttributeParms) { |
1796 |
|
$realFilter =~ s/\$$name/$AttributeParms{$name}/g; |
1797 |
|
} |
1798 |
|
my $query = $self->Get(['HasValueFor'], $realFilter, $realParms); |
1799 |
|
# Loop through the results, forming the output attribute tuples. |
1800 |
|
while (my $result = $query->Fetch()) { |
1801 |
|
# Get the four values from this query result row. |
1802 |
|
my ($objectID, $key, $subkey, $value) = $result->Values([$AttributeParms{object}, |
1803 |
|
$AttributeParms{key}, |
1804 |
|
$AttributeParms{subkey}, |
1805 |
|
$AttributeParms{value}]); |
1806 |
|
# Combine the key and the subkey. |
1807 |
|
my $realKey = ($subkey ? $key . $self->{splitter} . $subkey : $key); |
1808 |
|
# Split the value. |
1809 |
|
my @values = split $self->{splitter}, $value; |
1810 |
|
# Output the result. |
1811 |
|
push @retVal, [$objectID, $realKey, @values]; |
1812 |
|
} |
1813 |
|
# Return the result. |
1814 |
|
return @retVal; |
1815 |
|
} |
1816 |
|
|
1817 |
|
=head2 Key and ID Manipulation Methods |
1818 |
|
|
1819 |
|
=head3 ParseID |
1820 |
|
|
1821 |
|
C<< my ($type, $id) = CustomAttributes::ParseID($idValue); >> |
1822 |
|
|
1823 |
|
Determine the type and object ID corresponding to an ID value from the attribute database. |
1824 |
|
Most ID values consist of a type name and an ID, separated by a colon (e.g. C<Family:aclame|cluster10>); |
1825 |
|
however, Genomes, Features, and Subsystems are not stored with a type name, so we need to |
1826 |
|
deduce the type from the ID value structure. |
1827 |
|
|
1828 |
|
The theory here is that you can plug the ID and type directly into a Sprout database method, as |
1829 |
|
follows |
1830 |
|
|
1831 |
|
my ($type, $id) = CustomAttributes::ParseID($attrList[$num]->[0]); |
1832 |
|
my $target = $sprout->GetEntity($type, $id); |
1833 |
|
|
1834 |
|
=over 4 |
1835 |
|
|
1836 |
|
=item idValue |
1837 |
|
|
1838 |
|
ID value taken from the attribute database. |
1839 |
|
|
1840 |
|
=item RETURN |
1841 |
|
|
1842 |
|
Returns a two-element list. The first element is the type of object indicated by the ID value, |
1843 |
|
and the second element is the actual object ID. |
1844 |
|
|
1845 |
|
=back |
1846 |
|
|
1847 |
|
=cut |
1848 |
|
|
1849 |
|
sub ParseID { |
1850 |
|
# Get the parameters. |
1851 |
|
my ($idValue) = @_; |
1852 |
|
# Declare the return variables. |
1853 |
|
my ($type, $id); |
1854 |
|
# Parse the incoming ID. We first check for the presence of an entity name. Entity names |
1855 |
|
# can only contain letters, which helps to insure typed object IDs don't collide with |
1856 |
|
# subsystem names (which are untyped). |
1857 |
|
if ($idValue =~ /^([A-Za-z]+):(.+)/) { |
1858 |
|
# Here we have a typed ID. |
1859 |
|
($type, $id) = ($1, $2); |
1860 |
|
# Fix the case sensitivity on PDB IDs. |
1861 |
|
if ($type eq 'PDB') { $id = lc $id; } |
1862 |
|
} elsif ($idValue =~ /fig\|/) { |
1863 |
|
# Here we have a feature ID. |
1864 |
|
($type, $id) = (Feature => $idValue); |
1865 |
|
} elsif ($idValue =~ /\d+\.\d+/) { |
1866 |
|
# Here we have a genome ID. |
1867 |
|
($type, $id) = (Genome => $idValue); |
1868 |
|
} else { |
1869 |
|
# The default is a subsystem ID. |
1870 |
|
($type, $id) = (Subsystem => $idValue); |
1871 |
|
} |
1872 |
|
# Return the results. |
1873 |
|
return ($type, $id); |
1874 |
|
} |
1875 |
|
|
1876 |
|
=head3 FormID |
1877 |
|
|
1878 |
|
C<< my $idValue = CustomAttributes::FormID($type, $id); >> |
1879 |
|
|
1880 |
|
Convert an object type and ID pair into an object ID string for the attribute system. Subsystems, |
1881 |
|
genomes, and features are stored in the database without type information, but all other object IDs |
1882 |
|
must be prefixed with the object type. |
1883 |
|
|
1884 |
|
=over 4 |
1885 |
|
|
1886 |
|
=item type |
1887 |
|
|
1888 |
|
Relevant object type. |
1889 |
|
|
1890 |
|
=item id |
1891 |
|
|
1892 |
|
ID of the object in question. |
1893 |
|
|
1894 |
|
=item RETURN |
1895 |
|
|
1896 |
|
Returns a string that will be recognized as an object ID in the attribute database. |
1897 |
|
|
1898 |
|
=back |
1899 |
|
|
1900 |
|
=cut |
1901 |
|
|
1902 |
|
sub FormID { |
1903 |
|
# Get the parameters. |
1904 |
|
my ($type, $id) = @_; |
1905 |
|
# Declare the return variable. |
1906 |
|
my $retVal; |
1907 |
|
# Compute the ID string from the type. |
1908 |
|
if (grep { $type eq $_ } qw(Feature Genome Subsystem)) { |
1909 |
|
$retVal = $id; |
1910 |
|
} else { |
1911 |
|
$retVal = "$type:$id"; |
1912 |
|
} |
1913 |
|
# Return the result. |
1914 |
|
return $retVal; |
1915 |
|
} |
1916 |
|
|
1917 |
|
=head3 GetTargetObject |
1918 |
|
|
1919 |
|
C<< my $object = CustomAttributes::GetTargetObject($erdb, $idValue); >> |
1920 |
|
|
1921 |
|
Return the database object corresponding to the specified attribute object ID. The |
1922 |
|
object type associated with the ID value must correspond to an entity name in the |
1923 |
|
specified database. |
1924 |
|
|
1925 |
|
=over 4 |
1926 |
|
|
1927 |
|
=item erdb |
1928 |
|
|
1929 |
|
B<ERDB> object for accessing the target database. |
1930 |
|
|
1931 |
|
=item idValue |
1932 |
|
|
1933 |
|
ID value retrieved from the attribute database. |
1934 |
|
|
1935 |
|
=item RETURN |
1936 |
|
|
1937 |
|
Returns a B<ERDBObject> for the attribute value's target object. |
1938 |
|
|
1939 |
|
=back |
1940 |
|
|
1941 |
|
=cut |
1942 |
|
|
1943 |
|
sub GetTargetObject { |
1944 |
|
# Get the parameters. |
1945 |
|
my ($erdb, $idValue) = @_; |
1946 |
|
# Declare the return variable. |
1947 |
|
my $retVal; |
1948 |
|
# Get the type and ID for the target object. |
1949 |
|
my ($type, $id) = ParseID($idValue); |
1950 |
|
# Plug them into the GetEntity method. |
1951 |
|
$retVal = $erdb->GetEntity($type, $id); |
1952 |
|
# Return the resulting object. |
1953 |
|
return $retVal; |
1954 |
|
} |
1955 |
|
|
1956 |
|
=head3 SplitKey |
1957 |
|
|
1958 |
|
C<< my ($realKey, $subKey) = $ca->SplitKey($key); >> |
1959 |
|
|
1960 |
|
Split an external key (that is, one passed in by a caller) into the real key and the sub key. |
1961 |
|
The real and sub keys are separated by a splitter value (usually C<::>). If there is no splitter, |
1962 |
|
then the sub key is presumed to be an empty string. |
1963 |
|
|
1964 |
|
=over 4 |
1965 |
|
|
1966 |
|
=item key |
1967 |
|
|
1968 |
|
Incoming key to be split. |
1969 |
|
|
1970 |
|
=item RETURN |
1971 |
|
|
1972 |
|
Returns a two-element list, the first element of which is the real key and the second element of |
1973 |
|
which is the sub key. |
1974 |
|
|
1975 |
|
=back |
1976 |
|
|
1977 |
|
=cut |
1978 |
|
|
1979 |
|
sub SplitKey { |
1980 |
|
# Get the parameters. |
1981 |
|
my ($self, $key) = @_; |
1982 |
|
# Do the split. |
1983 |
|
my ($realKey, $subKey) = split($self->{splitter}, $key, 2); |
1984 |
|
# Insure the subkey has a value. |
1985 |
|
if (! defined $subKey) { |
1986 |
|
$subKey = ''; |
1987 |
|
} |
1988 |
|
# Return the results. |
1989 |
|
return ($realKey, $subKey); |
1990 |
|
} |
1991 |
|
|
1992 |
|
=head3 JoinKey |
1993 |
|
|
1994 |
|
C<< my $key = $ca->JoinKey($realKey, $subKey); >> |
1995 |
|
|
1996 |
|
Join a real key and a subkey together to make an external key. The external key is the attribute key |
1997 |
|
used by the caller. The real key and the subkey are how the keys are represented in the database. The |
1998 |
|
real key is the key to the B<AttributeKey> entity. The subkey is an attribute of the B<HasValueFor> |
1999 |
|
relationship. |
2000 |
|
|
2001 |
|
=over 4 |
2002 |
|
|
2003 |
|
=item realKey |
2004 |
|
|
2005 |
|
The real attribute key. |
2006 |
|
|
2007 |
|
=item subKey |
2008 |
|
|
2009 |
|
The subordinate portion of the attribute key. |
2010 |
|
|
2011 |
|
=item RETURN |
2012 |
|
|
2013 |
|
Returns a single string representing both keys. |
2014 |
|
|
2015 |
|
=back |
2016 |
|
|
2017 |
|
=cut |
2018 |
|
|
2019 |
|
sub JoinKey { |
2020 |
|
# Get the parameters. |
2021 |
|
my ($self, $realKey, $subKey) = @_; |
2022 |
|
# Declare the return variable. |
2023 |
|
my $retVal; |
2024 |
|
# Check for a subkey. |
2025 |
|
if ($subKey eq '') { |
2026 |
|
# No subkey, so the real key is the key. |
2027 |
|
$retVal = $realKey; |
2028 |
|
} else { |
2029 |
|
# Subkey found, so the two pieces must be joined by a splitter. |
2030 |
|
$retVal = "$realKey$self->{splitter}$subKey"; |
2031 |
|
} |
2032 |
|
# Return the result. |
2033 |
|
return $retVal; |
2034 |
|
} |
2035 |
|
|
2036 |
|
|
2037 |
|
=head3 AttributeTable |
2038 |
|
|
2039 |
|
C<< my $tableHtml = CustomAttributes::AttributeTable($cgi, @attrList); >> |
2040 |
|
|
2041 |
|
Format the attribute data into an HTML table. |
2042 |
|
|
2043 |
|
=over 4 |
2044 |
|
|
2045 |
|
=item cgi |
2046 |
|
|
2047 |
|
CGI query object used to generate the HTML |
2048 |
|
|
2049 |
|
=item attrList |
2050 |
|
|
2051 |
|
List of attribute results, in the format returned by the L</GetAttributes> or |
2052 |
|
L</QueryAttributes> methods. |
2053 |
|
|
2054 |
|
=item RETURN |
2055 |
|
|
2056 |
|
Returns an HTML table displaying the attribute keys and values. |
2057 |
|
|
2058 |
|
=back |
2059 |
|
|
2060 |
|
=cut |
2061 |
|
|
2062 |
|
sub AttributeTable { |
2063 |
|
# Get the parameters. |
2064 |
|
my ($cgi, @attrList) = @_; |
2065 |
|
# Accumulate the table rows. |
2066 |
|
my @html = (); |
2067 |
|
for my $attrData (@attrList) { |
2068 |
|
# Format the object ID and key. |
2069 |
|
my @columns = map { CGI::escapeHTML($_) } @{$attrData}[0,1]; |
2070 |
|
# Now we format the values. These remain unchanged unless one of them is a URL. |
2071 |
|
my $lastValue = scalar(@{$attrData}) - 1; |
2072 |
|
push @columns, map { $_ =~ /^http:/ ? $cgi->a({ href => $_ }, $_) : $_ } @{$attrData}[2 .. $lastValue]; |
2073 |
|
# Assemble the values into a table row. |
2074 |
|
push @html, $cgi->Tr($cgi->td(\@columns)); |
2075 |
|
} |
2076 |
|
# Format the table in the return variable. |
2077 |
|
my $retVal = $cgi->table({ border => 2 }, $cgi->Tr($cgi->th(['Object', 'Key', 'Values'])), @html); |
2078 |
|
# Return it. |
2079 |
|
return $retVal; |
2080 |
|
} |
2081 |
1; |
1; |