9 |
use Tracer; |
use Tracer; |
10 |
use ERDBLoad; |
use ERDBLoad; |
11 |
use Stats; |
use Stats; |
12 |
|
use Time::HiRes qw(time); |
13 |
|
use FIGRules; |
14 |
|
|
15 |
=head1 Custom SEED Attribute Manager |
=head1 Custom SEED Attribute Manager |
16 |
|
|
126 |
functions as data to the attribute management process, so if the data is |
functions as data to the attribute management process, so if the data is |
127 |
moved, this file must go with it. |
moved, this file must go with it. |
128 |
|
|
129 |
|
=item attr_default_table |
130 |
|
|
131 |
|
Name of the default relationship for attribute values. If not present, |
132 |
|
C<HasValueFor> is used. |
133 |
|
|
134 |
=back |
=back |
135 |
|
|
136 |
=head2 Public Methods |
=head2 Public Methods |
137 |
|
|
138 |
=head3 new |
=head3 new |
139 |
|
|
140 |
C<< my $attrDB = CustomAttributes->new(%options); >> |
my $attrDB = CustomAttributes->new(%options); |
141 |
|
|
142 |
Construct a new CustomAttributes object. The following options are |
Construct a new CustomAttributes object. The following options are |
143 |
supported. |
supported. |
161 |
sub new { |
sub new { |
162 |
# Get the parameters. |
# Get the parameters. |
163 |
my ($class, %options) = @_; |
my ($class, %options) = @_; |
164 |
|
# Get the name ofthe default table. |
165 |
# Connect to the database. |
# Connect to the database. |
166 |
my $dbh = DBKernel->new($FIG_Config::attrDbms, $FIG_Config::attrDbName, |
my $dbh = DBKernel->new($FIG_Config::attrDbms, $FIG_Config::attrDbName, |
167 |
$FIG_Config::attrUser, $FIG_Config::attrPass, |
$FIG_Config::attrUser, $FIG_Config::attrPass, |
175 |
# Store the user name. |
# Store the user name. |
176 |
$retVal->{user} = $options{user} || '<unknown>'; |
$retVal->{user} = $options{user} || '<unknown>'; |
177 |
Trace("User $retVal->{user} selected for attribute object.") if T(3); |
Trace("User $retVal->{user} selected for attribute object.") if T(3); |
178 |
|
# Compute the default value table name. If it's not overridden, the |
179 |
|
# default is HasValueFor. |
180 |
|
$retVal->{defaultRel} = $FIG_Config::attr_default_table || 'HasValueFor'; |
181 |
# Return the result. |
# Return the result. |
182 |
return $retVal; |
return $retVal; |
183 |
} |
} |
184 |
|
|
185 |
=head3 StoreAttributeKey |
=head3 StoreAttributeKey |
186 |
|
|
187 |
C<< $attrDB->StoreAttributeKey($attributeName, $type, $notes, \@groups); >> |
$attrDB->StoreAttributeKey($attributeName, $notes, \@groups, $table); |
188 |
|
|
189 |
Create or update an attribute for the database. |
Create or update an attribute for the database. |
190 |
|
|
194 |
|
|
195 |
Name of the attribute (the real key). If it does not exist already, it will be created. |
Name of the attribute (the real key). If it does not exist already, it will be created. |
196 |
|
|
|
=item type |
|
|
|
|
|
Data type of the attribute. This must be a valid ERDB data type name. |
|
|
|
|
197 |
=item notes |
=item notes |
198 |
|
|
199 |
Descriptive notes about the attribute. It is presumed to be raw text, not HTML. |
Descriptive notes about the attribute. It is presumed to be raw text, not HTML. |
203 |
Reference to a list of the groups to which the attribute should be associated. |
Reference to a list of the groups to which the attribute should be associated. |
204 |
This will replace any groups to which the attribute is currently attached. |
This will replace any groups to which the attribute is currently attached. |
205 |
|
|
206 |
|
=item table |
207 |
|
|
208 |
|
The name of the relationship in which the attribute's values are to be stored. |
209 |
|
If empty or undefined, the default relationship (usually C<HasValueFor>) will be |
210 |
|
assumed. |
211 |
|
|
212 |
=back |
=back |
213 |
|
|
214 |
=cut |
=cut |
215 |
|
|
216 |
sub StoreAttributeKey { |
sub StoreAttributeKey { |
217 |
# Get the parameters. |
# Get the parameters. |
218 |
my ($self, $attributeName, $type, $notes, $groups) = @_; |
my ($self, $attributeName, $notes, $groups, $table) = @_; |
219 |
# Declare the return variable. |
# Declare the return variable. |
220 |
my $retVal; |
my $retVal; |
221 |
|
# Default the table name. |
222 |
|
if (! $table) { |
223 |
|
$table = $self->{defaultRel}; |
224 |
|
} |
225 |
# Get the data type hash. |
# Get the data type hash. |
226 |
my %types = ERDB::GetDataTypes(); |
my %types = ERDB::GetDataTypes(); |
227 |
# Validate the initial input values. |
# Validate the initial input values. |
228 |
if ($attributeName =~ /$self->{splitter}/) { |
if ($attributeName =~ /$self->{splitter}/) { |
229 |
Confess("Invalid attribute name \"$attributeName\" specified."); |
Confess("Invalid attribute name \"$attributeName\" specified."); |
230 |
} elsif (! $notes || length($notes) < 25) { |
} elsif (! $notes) { |
231 |
Confess("Missing or incomplete description for $attributeName."); |
Confess("Missing description for $attributeName."); |
232 |
} elsif (! exists $types{$type}) { |
} elsif (! grep { $_ eq $table } $self->GetConnectingRelationships('AttributeKey')) { |
233 |
Confess("Invalid data type \"$type\" for $attributeName."); |
Confess("Invalid relationship name \"$table\" specified as a custom attribute table."); |
234 |
} else { |
} else { |
235 |
# Create a variable to hold the action to be displayed for the log (Add or Update). |
# Create a variable to hold the action to be displayed for the log (Add or Update). |
236 |
my $action; |
my $action; |
240 |
# It does, so we do an update. |
# It does, so we do an update. |
241 |
$action = "Update Key"; |
$action = "Update Key"; |
242 |
$self->UpdateEntity('AttributeKey', $attributeName, |
$self->UpdateEntity('AttributeKey', $attributeName, |
243 |
{ description => $notes, 'data-type' => $type }); |
{ description => $notes, |
244 |
|
'relationship-name' => $table}); |
245 |
# Detach the key from its current groups. |
# Detach the key from its current groups. |
246 |
$self->Disconnect('IsInGroup', 'AttributeKey', $attributeName); |
$self->Disconnect('IsInGroup', 'AttributeKey', $attributeName); |
247 |
} else { |
} else { |
248 |
# It doesn't, so we do an insert. |
# It doesn't, so we do an insert. |
249 |
$action = "Insert Key"; |
$action = "Insert Key"; |
250 |
$self->InsertObject('AttributeKey', { id => $attributeName, |
$self->InsertObject('AttributeKey', { id => $attributeName, |
251 |
description => $notes, 'data-type' => $type }); |
description => $notes, |
252 |
|
'relationship-name' => $table}); |
253 |
} |
} |
254 |
# Attach the key to the specified groups. (We presume the groups already |
# Attach the key to the specified groups. (We presume the groups already |
255 |
# exist.) |
# exist.) |
265 |
|
|
266 |
=head3 DeleteAttributeKey |
=head3 DeleteAttributeKey |
267 |
|
|
268 |
C<< my $stats = $attrDB->DeleteAttributeKey($attributeName); >> |
my $stats = $attrDB->DeleteAttributeKey($attributeName); |
269 |
|
|
270 |
Delete an attribute from the custom attributes database. |
Delete an attribute from the custom attributes database. |
271 |
|
|
297 |
|
|
298 |
=head3 NewName |
=head3 NewName |
299 |
|
|
300 |
C<< my $text = CustomAttributes::NewName(); >> |
my $text = CustomAttributes::NewName(); |
301 |
|
|
302 |
Return the string used to indicate the user wants to add a new attribute. |
Return the string used to indicate the user wants to add a new attribute. |
303 |
|
|
307 |
return "(new)"; |
return "(new)"; |
308 |
} |
} |
309 |
|
|
|
=head3 ControlForm |
|
|
|
|
|
C<< my $formHtml = $attrDB->ControlForm($cgi, $name, \%keys); >> |
|
|
|
|
|
Return a form that can be used to control the creation and modification of |
|
|
attributes. Only a subset of the attribute keys will be displayed, as |
|
|
determined by the incoming list. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item cgi |
|
|
|
|
|
CGI query object used to create HTML. |
|
|
|
|
|
=item name |
|
|
|
|
|
Name to give to the form. This should be unique for the web page. |
|
|
|
|
|
=item keys |
|
|
|
|
|
Reference to a hash mapping attribute keys to n-tuples. Each tuple will contain the |
|
|
attribute's data type, its description, and a list of the groups in which it participates. |
|
|
|
|
|
=item RETURN |
|
|
|
|
|
Returns the HTML for a form that can be used to submit instructions to the C<Attributes.cgi> script |
|
|
for loading, creating, displaying, changing, or deleting an attribute. Note that only the form |
|
|
controls are generated. The form tags are left to the caller. |
|
|
|
|
|
=back |
|
|
|
|
|
=cut |
|
|
|
|
|
sub ControlForm { |
|
|
# Get the parameters. |
|
|
my ($self, $cgi, $name, $keys) = @_; |
|
|
# Declare the return list. |
|
|
my @retVal = (); |
|
|
# We'll put the controls in a table. Nothing else ever seems to look nice. |
|
|
push @retVal, $cgi->start_table({ border => 2, cellpadding => 2 }); |
|
|
# The first row is for selecting the field name. |
|
|
push @retVal, $cgi->Tr($cgi->th("Select a Field"), |
|
|
$cgi->td($self->FieldMenu($cgi, 10, 'fieldName', $keys, |
|
|
new => 1, |
|
|
notes => "document.$name.notes.value", |
|
|
type => "document.$name.dataType.value", |
|
|
groups => "document.$name.groups"))); |
|
|
# Now we set up a dropdown for the data types. The values will be the |
|
|
# data type names, and the labels will be the descriptions. |
|
|
my %types = ERDB::GetDataTypes(); |
|
|
my %labelMap = map { $_ => $types{$_}->{notes} } keys %types; |
|
|
my $typeMenu = $cgi->popup_menu(-name => 'dataType', |
|
|
-values => [sort keys %types], |
|
|
-labels => \%labelMap, |
|
|
-default => 'string'); |
|
|
# Allow the user to specify a new field name. This is required if the |
|
|
# user has selected the "(new)" marker. We put a little scriptlet in here that |
|
|
# selects the (new) marker when the user enters the field. |
|
|
push @retVal, "<script language=\"javaScript\">"; |
|
|
my $fieldField = "document.$name.fieldName"; |
|
|
my $newName = "\"" . NewName() . "\""; |
|
|
push @retVal, $cgi->Tr($cgi->th("New Field Name"), |
|
|
$cgi->td($cgi->textfield(-name => 'newName', |
|
|
-size => 30, |
|
|
-value => "", |
|
|
-onFocus => "setIfEmpty($fieldField, $newName);")), |
|
|
); |
|
|
push @retVal, $cgi->Tr($cgi->th("Data type"), |
|
|
$cgi->td($typeMenu)); |
|
|
# The next row is for the notes. |
|
|
push @retVal, $cgi->Tr($cgi->th("Description"), |
|
|
$cgi->td($cgi->textarea(-name => 'notes', |
|
|
-rows => 6, |
|
|
-columns => 80)) |
|
|
); |
|
|
# Now we have the groups, which are implemented as a checkbox group. |
|
|
my @groups = $self->GetGroups(); |
|
|
push @retVal, $cgi->Tr($cgi->th("Groups"), |
|
|
$cgi->td($cgi->checkbox_group(-name=>'groups', |
|
|
-values=> \@groups)) |
|
|
); |
|
|
# Now the four buttons: STORE, SHOW, ERASE, and DELETE. |
|
|
push @retVal, $cgi->Tr($cgi->th(" "), |
|
|
$cgi->td({align => 'center'}, join(" ", |
|
|
$cgi->submit(-name => 'Delete', -value => 'DELETE'), |
|
|
$cgi->submit(-name => 'Store', -value => 'STORE'), |
|
|
$cgi->submit(-name => 'Erase', -value => 'ERASE'), |
|
|
$cgi->submit(-name => 'Show', -value => 'SHOW') |
|
|
)) |
|
|
); |
|
|
# Close the table and the form. |
|
|
push @retVal, $cgi->end_table(); |
|
|
# Return the assembled HTML. |
|
|
return join("\n", @retVal, ""); |
|
|
} |
|
|
|
|
310 |
=head3 LoadAttributesFrom |
=head3 LoadAttributesFrom |
311 |
|
|
312 |
C<< my $stats = $attrDB->LoadAttributesFrom($fileName, %options); >> |
C<< my $stats = $attrDB->LoadAttributesFrom($fileName, %options); >> |
313 |
|
|
314 |
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 |
315 |
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 |
316 |
column, and attribute values in the remaining columns. The attribute values will |
column, and attribute values in the remaining columns. The attribute values must |
317 |
be assembled into a single value using the splitter code. In addition, the key names may |
be assembled into a single value using the splitter code. In addition, the key names may |
318 |
contain a splitter. If this is the case, the portion of the key after the splitter is |
contain a splitter. If this is the case, the portion of the key after the splitter is |
319 |
treated as a subkey. |
treated as a subkey. |
340 |
|
|
341 |
=over 4 |
=over 4 |
342 |
|
|
343 |
|
=item mode |
344 |
|
|
345 |
|
Loading mode. Legal values are C<low_priority> (which reduces the task priority |
346 |
|
of the load) and C<concurrent> (which reduces the locking cost of the load). The |
347 |
|
default is a normal load. |
348 |
|
|
349 |
=item append |
=item append |
350 |
|
|
351 |
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 |
353 |
|
|
354 |
=item archive |
=item archive |
355 |
|
|
356 |
If specified, the name of a file into which the incoming data file should be saved. |
If specified, the name of a file into which the incoming data should be saved. |
357 |
|
If I<resume> is also specified, only the lines actually loaded will be put |
358 |
|
into this file. |
359 |
|
|
360 |
=item objectType |
=item objectType |
361 |
|
|
362 |
If specified, the specified object type will be prefixed to each object ID. |
If specified, the specified object type will be prefixed to each object ID. |
363 |
|
|
364 |
|
=item resume |
365 |
|
|
366 |
|
If specified, key-value pairs already in the database will not be reinserted. |
367 |
|
Specify a number to start checking after the specified number of lines and |
368 |
|
then admit everything after the first line not yet loaded. Specify C<careful> |
369 |
|
to check every single line. Specify C<none> to ignore this option. The default |
370 |
|
is C<none>. So, if you believe that a previous load failed somewhere after 50000 |
371 |
|
lines, a resume value of C<50000> would skip 50000 lines in the file, then |
372 |
|
check each line after that until it finds one not already in the database. The |
373 |
|
first such line found and all lines after that will be loaded. On the other |
374 |
|
hand, if you have a file of 100000 records, and some have been loaded and some |
375 |
|
not, you would use the word C<careful>, so that every line would be checked before |
376 |
|
it is inserted. A resume of C<0> will start checking the first line of the |
377 |
|
input file and then begin loading once it finds a line not in the database. |
378 |
|
|
379 |
|
=item chunkSize |
380 |
|
|
381 |
|
Number of lines to load in each burst. The default is 10,000. |
382 |
|
|
383 |
=back |
=back |
384 |
|
|
385 |
=cut |
=cut |
388 |
# Get the parameters. |
# Get the parameters. |
389 |
my ($self, $fileName, %options) = @_; |
my ($self, $fileName, %options) = @_; |
390 |
# Declare the return variable. |
# Declare the return variable. |
391 |
my $retVal = Stats->new('keys', 'values'); |
my $retVal = Stats->new('keys', 'values', 'linesOut'); |
392 |
|
# Initialize the timers. |
393 |
|
my ($eraseTime, $archiveTime, $checkTime) = (0, 0, 0); |
394 |
# Check for append mode. |
# Check for append mode. |
395 |
my $append = ($options{append} ? 1 : 0); |
my $append = ($options{append} ? 1 : 0); |
396 |
|
# Check for resume mode. |
397 |
|
my $resume = (defined($options{resume}) ? $options{resume} : 'none'); |
398 |
# Create a hash of key names found. |
# Create a hash of key names found. |
399 |
my %keyHash = (); |
my %keyHash = (); |
400 |
|
# Create a hash of table names to files. Most attributes go into the HasValueFor |
401 |
|
# table, but some are put into other tables. Each table name will be mapped |
402 |
|
# to a sub-hash with keys "fileName" (output file for the table) and "count" |
403 |
|
# (number of lines in the file). |
404 |
|
my %tableHash = (); |
405 |
|
# Compute the chunk size. |
406 |
|
my $chunkSize = ($options{chunkSize} ? $options{chunkSize} : 10000); |
407 |
# Open the file for input. Note we must anticipate the possibility of an |
# Open the file for input. Note we must anticipate the possibility of an |
408 |
# open filehandle being passed in. |
# open filehandle being passed in. This occurs when the user is submitting |
409 |
|
# the load file over the web. |
410 |
my $fh; |
my $fh; |
411 |
if (ref $fileName) { |
if (ref $fileName) { |
412 |
Trace("Using file opened by caller.") if T(3); |
Trace("Using file opened by caller.") if T(3); |
415 |
Trace("Attributes will be loaded from $fileName.") if T(3); |
Trace("Attributes will be loaded from $fileName.") if T(3); |
416 |
$fh = Open(undef, "<$fileName"); |
$fh = Open(undef, "<$fileName"); |
417 |
} |
} |
418 |
|
# Trace the mode. |
419 |
|
if (T(3)) { |
420 |
|
if ($options{mode}) { |
421 |
|
Trace("Mode is $options{mode}.") |
422 |
|
} else { |
423 |
|
Trace("No mode specified.") |
424 |
|
} |
425 |
|
} |
426 |
# Now check to see if we need to archive. |
# Now check to see if we need to archive. |
427 |
my $ah; |
my $ah; |
428 |
if ($options{archive}) { |
if (exists $options{archive}) { |
429 |
$ah = Open(undef, ">$options{archive}"); |
my $ah = Open(undef, ">$options{archive}"); |
430 |
Trace("Load file will be archived to $options{archive}.") if T(3); |
Trace("Load file will be archived to $options{archive}.") if T(3); |
431 |
} |
} |
432 |
# Finally, open a database transaction. |
# Insure we recover from errors. |
|
$self->BeginTran(); |
|
|
# Insure we recover from errors. If an error occurs, we will delete the archive file and |
|
|
# roll back the updates. |
|
433 |
eval { |
eval { |
434 |
|
# If we have a resume number, process it here. |
435 |
|
if ($resume =~ /\d+/) { |
436 |
|
Trace("Skipping $resume lines.") if T(2); |
437 |
|
my $startTime = time(); |
438 |
|
# Skip the specified number of lines. |
439 |
|
for (my $skipped = 0; ! eof($fh) && $skipped < $resume; $skipped++) { |
440 |
|
my $line = <$fh>; |
441 |
|
$retVal->Add(skipped => 1); |
442 |
|
} |
443 |
|
$checkTime += time() - $startTime; |
444 |
|
} |
445 |
# Loop through the file. |
# Loop through the file. |
446 |
|
Trace("Starting load.") if T(2); |
447 |
while (! eof $fh) { |
while (! eof $fh) { |
448 |
# Read the current line. |
# Read the current line. |
449 |
my ($id, $key, @values) = Tracer::GetLine($fh); |
my ($id, $key, @values) = Tracer::GetLine($fh); |
450 |
$retVal->Add(linesIn => 1); |
$retVal->Add(linesIn => 1); |
|
# Check to see if we need to fix up the object ID. |
|
|
if ($options{objectType}) { |
|
|
$id = "$options{objectType}:$id"; |
|
|
} |
|
|
# Archive the line (if necessary). |
|
|
if (defined $ah) { |
|
|
Tracer::PutLine($ah, [$id, $key, @values]); |
|
|
} |
|
451 |
# Do some validation. |
# Do some validation. |
452 |
if (! $id) { |
if (! $id) { |
453 |
# We ignore blank lines. |
# We ignore blank lines. |
465 |
Trace("Line $lines for key $key has no attribute values.") if T(1); |
Trace("Line $lines for key $key has no attribute values.") if T(1); |
466 |
$retVal->Add(skipped => 1); |
$retVal->Add(skipped => 1); |
467 |
} else { |
} else { |
468 |
|
# Check to see if we need to fix up the object ID. |
469 |
|
if ($options{objectType}) { |
470 |
|
$id = "$options{objectType}:$id"; |
471 |
|
} |
472 |
# The key contains a real part and an optional sub-part. We need the real part. |
# The key contains a real part and an optional sub-part. We need the real part. |
473 |
my ($realKey, $subKey) = $self->SplitKey($key); |
my ($realKey, $subKey) = $self->SplitKey($key); |
474 |
# Now we need to check for a new key. |
# Now we need to check for a new key. |
475 |
if (! exists $keyHash{$realKey}) { |
if (! exists $keyHash{$realKey}) { |
476 |
if (! $self->Exists('AttributeKey', $realKey)) { |
my $keyObject = $self->GetEntity(AttributeKey => $realKey); |
477 |
|
if (! defined($keyObject)) { |
478 |
|
# Here the specified key does not exist, which is an error. |
479 |
my $line = $retVal->Ask('linesIn'); |
my $line = $retVal->Ask('linesIn'); |
480 |
Confess("Attribute \"$realKey\" on line $line of $fileName not found in database."); |
Confess("Attribute \"$realKey\" on line $line of $fileName not found in database."); |
481 |
} else { |
} else { |
482 |
# Make sure we know this is no longer a new key. |
# Make sure we know this is no longer a new key. We do this by putting |
483 |
$keyHash{$realKey} = 1; |
# its table name in the key hash. |
484 |
|
$keyHash{$realKey} = $keyObject->PrimaryValue('AttributeKey(relationship-name)'); |
485 |
$retVal->Add(keys => 1); |
$retVal->Add(keys => 1); |
486 |
# If this is NOT append mode, erase the key. |
# If this is NOT append mode, erase the key. This does not delete the key |
487 |
|
# itself; it just clears out all the values. |
488 |
if (! $append) { |
if (! $append) { |
489 |
|
my $startTime = time(); |
490 |
$self->EraseAttribute($realKey); |
$self->EraseAttribute($realKey); |
491 |
|
$eraseTime += time() - $startTime; |
492 |
|
Trace("Attribute $realKey erased.") if T(3); |
493 |
} |
} |
494 |
} |
} |
495 |
Trace("Key $realKey found.") if T(3); |
Trace("Key $realKey found.") if T(3); |
496 |
} |
} |
497 |
# Everything is all set up, so add the value. |
# If we're in resume mode, check to see if this insert is redundant. |
498 |
$self->AddAttribute($id, $key, @values); |
my $ok = 1; |
499 |
my $progress = $retVal->Add(values => 1); |
if ($resume ne 'none') { |
500 |
Trace("$progress values loaded.") if T(3) && ($progress % 1000 == 0); |
my $startTime = time(); |
501 |
|
my $count = $self->GetAttributes($id, $key, @values); |
502 |
|
if ($count) { |
503 |
|
# Here the record is found, so we skip it. |
504 |
|
$ok = 0; |
505 |
|
$retVal->Add(skipped => 1); |
506 |
|
} else { |
507 |
|
# Here the record is not found. If we're in non-careful mode, we |
508 |
|
# stop resume checking at this point. |
509 |
|
if ($resume ne 'careful') { |
510 |
|
$resume = 'none'; |
511 |
|
} |
512 |
} |
} |
513 |
|
$checkTime += time() - $startTime; |
514 |
} |
} |
515 |
|
if ($ok) { |
516 |
|
# We're in business. First, archive this row. |
517 |
|
if (defined $ah) { |
518 |
|
my $startTime = time(); |
519 |
|
Tracer::PutLine($ah, [$id, $key, @values]); |
520 |
|
$archiveTime += time() - $startTime; |
521 |
|
} |
522 |
|
# We need to format the attribute data so it will work |
523 |
|
# as if it were a load file. This means we join the |
524 |
|
# values. |
525 |
|
my $valueString = join('::', @values); |
526 |
|
# Now we need to get access to the key's load file. Check for it in the |
527 |
|
# table hash. |
528 |
|
my $keyTable = $keyHash{$realKey}; |
529 |
|
if (! exists $tableHash{$keyTable}) { |
530 |
|
# This is a new table, so we need to set it up. First, we get |
531 |
|
# a temporary file for it. |
532 |
|
my $tempFileName = FIGRules::GetTempFileName(sessionID => $$ . $keyTable, |
533 |
|
extension => 'dtx'); |
534 |
|
my $oh = Open(undef, ">$tempFileName"); |
535 |
|
# Now we create its descriptor in the table hash. |
536 |
|
$tableHash{$keyTable} = {fileName => $tempFileName, handle => $oh, count => 0}; |
537 |
|
} |
538 |
|
# Everything is all set up, so we put the value in the temporary file and |
539 |
|
# count it. |
540 |
|
my $tableData = $tableHash{$keyTable}; |
541 |
|
my $startTime = time(); |
542 |
|
Tracer::PutLine($tableData->{handle}, [$realKey, $id, $subKey, $valueString]); |
543 |
|
$archiveTime += time() - $startTime; |
544 |
|
$retVal->Add(linesOut => 1); |
545 |
|
$tableData->{count}++; |
546 |
|
# See if it's time to load a chunk. |
547 |
|
if ($tableData->{count} >= $chunkSize) { |
548 |
|
# We've filled a chunk, so it's time. |
549 |
|
close $tableData->{handle}; |
550 |
|
$self->_LoadAttributeTable($keyTable, $tableData->{fileName}, $retVal); |
551 |
|
# Reset for the next chunk. |
552 |
|
$tableData->{count} = 0; |
553 |
|
$tableData->{handle} = Open(undef, ">$tableData->{fileName}"); |
554 |
|
} |
555 |
|
} else { |
556 |
|
# Here we skipped because of resume mode. |
557 |
|
$retVal->Add(resumeSkip => 1); |
558 |
|
} |
559 |
|
Trace($retVal->Ask('values') . " values processed.") if $retVal->Check(values => 1000) && T(3); |
560 |
|
} |
561 |
|
} |
562 |
|
# Now we close the archive file. Note we undefine the handle so the error methods know |
563 |
|
# not to worry. |
564 |
|
if (defined $ah) { |
565 |
|
close $ah; |
566 |
|
undef $ah; |
567 |
|
} |
568 |
|
# Now we load the residual from the temporary files (if any). This time we'll do an |
569 |
|
# analyze as well. |
570 |
|
for my $tableName (keys %tableHash) { |
571 |
|
# Get the data for this table. |
572 |
|
my $tableData = $tableHash{$tableName}; |
573 |
|
# Close the handle. ERDB will re-open it for input later. |
574 |
|
close $tableData->{handle}; |
575 |
|
# Check to see if there's anything left to load. |
576 |
|
if ($tableData->{count} > 0) { |
577 |
|
# Yes, load the data. |
578 |
|
$self->_LoadAttributeTable($tableName, $tableData->{fileName}, $retVal); |
579 |
|
} |
580 |
|
# Regardless of whether additional loading was required, we need to |
581 |
|
# analyze the table for performance. |
582 |
|
my $startTime = time(); |
583 |
|
$self->Analyze($tableName); |
584 |
|
$retVal->Add(analyzeTime => time() - $startTime); |
585 |
|
} |
586 |
|
Trace("Attribute load successful.") if T(2); |
587 |
}; |
}; |
588 |
# Check for an error. |
# Check for an error. |
589 |
if ($@) { |
if ($@) { |
590 |
# Here we have an error. Roll back the transaction and delete the archive file. |
# Here we have an error. Display the error message. |
591 |
my $message = $@; |
my $message = $@; |
592 |
Trace("Rolling back attribute updates due to error.") if T(1); |
Trace("Error during attribute load: $message") if T(0); |
593 |
$self->RollbackTran(); |
$retVal->AddMessage($message); |
594 |
if (defined $ah) { |
# Close the archive file if it's open. The archive file can sometimes provide |
595 |
Trace("Deleting archive file $options{archive}.") if T(1); |
# clues as to what happened. |
|
close $ah; |
|
|
unlink $options{archive}; |
|
|
} |
|
|
Confess("Error during attribute load: $message"); |
|
|
} else { |
|
|
# Here the load worked. Commit the transaction and close the archive file. |
|
|
Trace("Committing attribute upload.") if T(2); |
|
|
$self->CommitTran(); |
|
596 |
if (defined $ah) { |
if (defined $ah) { |
|
Trace("Closing archive file $options{archive}.") if T(2); |
|
597 |
close $ah; |
close $ah; |
598 |
} |
} |
599 |
} |
} |
600 |
|
# Store the timers. |
601 |
|
$retVal->Add(eraseTime => $eraseTime); |
602 |
|
$retVal->Add(archiveTime => $archiveTime); |
603 |
|
$retVal->Add(checkTime => $checkTime); |
604 |
# Return the result. |
# Return the result. |
605 |
return $retVal; |
return $retVal; |
606 |
} |
} |
607 |
|
|
608 |
=head3 BackupKeys |
=head3 BackupKeys |
609 |
|
|
610 |
C<< my $stats = $attrDB->BackupKeys($fileName, %options); >> |
my $stats = $attrDB->BackupKeys($fileName, %options); |
611 |
|
|
612 |
Backup the attribute key information from the attribute database. |
Backup the attribute key information from the attribute database. |
613 |
|
|
647 |
while (my $keyData = $keyQuery->Fetch()) { |
while (my $keyData = $keyQuery->Fetch()) { |
648 |
$retVal->Add(key => 1); |
$retVal->Add(key => 1); |
649 |
# Get the fields. |
# Get the fields. |
650 |
my ($id, $type, $description) = $keyData->Values(['AttributeKey(id)', 'AttributeKey(data-type)', |
my ($id, $type, $tableName, $description) = |
651 |
|
$keyData->Values(['AttributeKey(id)', 'AttributeKey(relationship-name)', |
652 |
'AttributeKey(description)']); |
'AttributeKey(description)']); |
653 |
# Escape any tabs or new-lines in the description. |
# Escape any tabs or new-lines in the description. |
654 |
my $escapedDescription = Tracer::Escape($description); |
my $escapedDescription = Tracer::Escape($description); |
655 |
# Write the key data to the output. |
# Write the key data to the output. |
656 |
Tracer::PutLine($fh, [$id, $type, $escapedDescription]); |
Tracer::PutLine($fh, [$id, $type, $tableName, $escapedDescription]); |
657 |
# Get the key's groups. |
# Get the key's groups. |
658 |
my @groups = $self->GetFlat(['IsInGroup'], "IsInGroup(from-link) = ?", [$id], |
my @groups = $self->GetFlat(['IsInGroup'], "IsInGroup(from-link) = ?", [$id], |
659 |
'IsInGroup(to-link)'); |
'IsInGroup(to-link)'); |
670 |
|
|
671 |
=head3 RestoreKeys |
=head3 RestoreKeys |
672 |
|
|
673 |
C<< my $stats = $attrDB->RestoreKeys($fileName, %options); >> |
my $stats = $attrDB->RestoreKeys($fileName, %options); |
674 |
|
|
675 |
Restore the attribute keys and groups from a backup file. |
Restore the attribute keys and groups from a backup file. |
676 |
|
|
697 |
# Loop until we're done. |
# Loop until we're done. |
698 |
while (! eof $fh) { |
while (! eof $fh) { |
699 |
# Get a key record. |
# Get a key record. |
700 |
my ($id, $dataType, $description) = Tracer::GetLine($fh); |
my ($id, $tableName, $description) = Tracer::GetLine($fh); |
701 |
if ($id eq '#GROUPS') { |
if ($id eq '#GROUPS') { |
702 |
Confess("Group record found when key record expected."); |
Confess("Group record found when key record expected."); |
703 |
} elsif (! defined($description)) { |
} elsif (! defined($description)) { |
705 |
} else { |
} else { |
706 |
$retVal->Add("keyIn" => 1); |
$retVal->Add("keyIn" => 1); |
707 |
# Add this key to the database. |
# Add this key to the database. |
708 |
$self->InsertObject('AttributeKey', { id => $id, 'data-type' => $dataType, |
$self->InsertObject('AttributeKey', { id => $id, |
709 |
description => Tracer::UnEscape($description) }); |
description => Tracer::UnEscape($description), |
710 |
|
'relationship-name' => $tableName}); |
711 |
Trace("Attribute $id stored.") if T(3); |
Trace("Attribute $id stored.") if T(3); |
712 |
# Get the group line. |
# Get the group line. |
713 |
my ($marker, @groups) = Tracer::GetLine($fh); |
my ($marker, @groups) = Tracer::GetLine($fh); |
743 |
|
|
744 |
=head3 ArchiveFileName |
=head3 ArchiveFileName |
745 |
|
|
746 |
C<< my $fileName = $ca->ArchiveFileName(); >> |
my $fileName = $ca->ArchiveFileName(); |
747 |
|
|
748 |
Compute a file name for archiving attribute input data. The file will be in the attribute log directory |
Compute a file name for archiving attribute input data. The file will be in the attribute log directory |
749 |
|
|
776 |
|
|
777 |
=head3 BackupAllAttributes |
=head3 BackupAllAttributes |
778 |
|
|
779 |
C<< my $stats = $attrDB->BackupAllAttributes($fileName, %options); >> |
my $stats = $attrDB->BackupAllAttributes($fileName, %options); |
780 |
|
|
781 |
Backup all of the attributes to a file. The attributes will be stored in a |
Backup all of the attributes to a file. The attributes will be stored in a |
782 |
tab-delimited file suitable for reloading via L</LoadAttributesFrom>. |
tab-delimited file suitable for reloading via L</LoadAttributesFrom>. |
807 |
# Declare the return variable. |
# Declare the return variable. |
808 |
my $retVal = Stats->new(); |
my $retVal = Stats->new(); |
809 |
# Get a list of the keys. |
# Get a list of the keys. |
810 |
my @keys = $self->GetFlat(['AttributeKey'], "", [], 'AttributeKey(id)'); |
my %keys = map { $_->[0] => $_->[1] } $self->GetAll(['AttributeKey'], |
811 |
Trace(scalar(@keys) . " keys found during backup.") if T(2); |
"", [], ['AttributeKey(id)', |
812 |
|
'AttributeKey(relationship-name)']); |
813 |
|
Trace(scalar(keys %keys) . " keys found during backup.") if T(2); |
814 |
# Open the file for output. |
# Open the file for output. |
815 |
my $fh = Open(undef, ">$fileName"); |
my $fh = Open(undef, ">$fileName"); |
816 |
# Loop through the keys. |
# Loop through the keys. |
817 |
for my $key (@keys) { |
for my $key (sort keys %keys) { |
818 |
Trace("Backing up attribute $key.") if T(3); |
Trace("Backing up attribute $key.") if T(3); |
819 |
$retVal->Add(keys => 1); |
$retVal->Add(keys => 1); |
820 |
|
# Get the key's relevant relationship name. |
821 |
|
my $relName = $keys{$key}; |
822 |
# Loop through this key's values. |
# Loop through this key's values. |
823 |
my $query = $self->Get(['HasValueFor'], "HasValueFor(from-link) = ?", [$key]); |
my $query = $self->Get([$relName], "$relName(from-link) = ?", [$key]); |
824 |
my $valuesFound = 0; |
my $valuesFound = 0; |
825 |
while (my $line = $query->Fetch()) { |
while (my $line = $query->Fetch()) { |
826 |
$valuesFound++; |
$valuesFound++; |
827 |
# Get this row's data. |
# Get this row's data. |
828 |
my ($id, $key, $subKey, $value) = $line->Values(['HasValueFor(to-link)', |
my ($id, $key, $subKey, $value) = $line->Values(["$relName(to-link)", |
829 |
'HasValueFor(from-link)', |
"$relName(from-link)", |
830 |
'HasValueFor(subkey)', |
"$relName(subkey)", |
831 |
'HasValueFor(value)']); |
"$relName(value)"]); |
832 |
# Check for a subkey. |
# Check for a subkey. |
833 |
if ($subKey ne '') { |
if ($subKey ne '') { |
834 |
$key = "$key$self->{splitter}$subKey"; |
$key = "$key$self->{splitter}$subKey"; |
835 |
} |
} |
836 |
# Write it to the file. |
# Write it to the file. |
837 |
Tracer::PutLine($fh, [$id, $key, $value]); |
Tracer::PutLine($fh, [$id, $key, Escape($value)]); |
838 |
} |
} |
839 |
Trace("$valuesFound values backed up for key $key.") if T(3); |
Trace("$valuesFound values backed up for key $key.") if T(3); |
840 |
$retVal->Add(values => $valuesFound); |
$retVal->Add(values => $valuesFound); |
845 |
return $retVal; |
return $retVal; |
846 |
} |
} |
847 |
|
|
|
=head3 FieldMenu |
|
|
|
|
|
C<< my $menuHtml = $attrDB->FieldMenu($cgi, $height, $name, $keys, %options); >> |
|
|
|
|
|
Return the HTML for a menu to select an attribute field. The menu will |
|
|
be a standard SELECT/OPTION thing which is called "popup menu" in the |
|
|
CGI package, but actually looks like a list. The list will contain |
|
|
one selectable row per field. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item cgi |
|
|
|
|
|
CGI query object used to generate HTML. |
|
|
|
|
|
=item height |
|
|
|
|
|
Number of lines to display in the list. |
|
|
|
|
|
=item name |
|
|
|
|
|
Name to give to the menu. This is the name under which the value will |
|
|
appear when the form is submitted. |
|
|
|
|
|
=item keys |
|
|
|
|
|
Reference to a hash mapping each attribute key name to a list reference, |
|
|
the list itself consisting of the attribute data type, its description, |
|
|
and a list of its groups. |
|
|
|
|
|
=item options |
|
|
|
|
|
Hash containing options that modify the generation of the menu. |
|
|
|
|
|
=item RETURN |
|
|
|
|
|
Returns the HTML to create a form field that can be used to select an |
|
|
attribute from the custom attributes system. |
|
|
|
|
|
=back |
|
|
|
|
|
The permissible options are as follows. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item new |
|
|
|
|
|
If TRUE, then extra rows will be provided to allow the user to select |
|
|
a new attribute. In other words, the user can select an existing |
|
|
attribute, or can choose a C<(new)> marker to indicate a field to |
|
|
be created in the parent entity. |
|
|
|
|
|
=item notes |
|
|
|
|
|
If specified, the name of a variable for displaying the notes attached |
|
|
to the field. This must be in Javascript form ready for assignment. |
|
|
So, for example, if you have a variable called C<notes> that |
|
|
represents a paragraph element, you should code C<notes.innerHTML>. |
|
|
If it actually represents a form field you should code C<notes.value>. |
|
|
If an C<innerHTML> coding is used, the text will be HTML-escaped before |
|
|
it is copied in. Specifying this parameter generates Javascript for |
|
|
displaying the field description when a field is selected. |
|
|
|
|
|
=item type |
|
|
|
|
|
If specified, the name of a variable for displaying the field's |
|
|
data type. Data types are a much more controlled vocabulary than |
|
|
notes, so there is no worry about HTML translation. Instead, the |
|
|
raw value is put into the specified variable. Otherwise, the same |
|
|
rules apply to this value that apply to I<$noteControl>. |
|
|
|
|
|
=item groups |
|
|
|
|
|
If specified, the name of a multiple-selection list control (also called |
|
|
a popup menu) which shall be used to display the selected groups. |
|
|
|
|
|
=back |
|
|
|
|
|
=cut |
|
|
|
|
|
sub FieldMenu { |
|
|
# Get the parameters. |
|
|
my ($self, $cgi, $height, $name, $keys, %options) = @_; |
|
|
# Reformat the list of keys. |
|
|
my %keys = %{$keys}; |
|
|
# Add the (new) key, if needed. |
|
|
if ($options{new}) { |
|
|
$keys{NewName()} = ["string", ""]; |
|
|
} |
|
|
# Get a sorted list of key. |
|
|
my @keys = sort keys %keys; |
|
|
# We need to create the name for the onChange function. This function |
|
|
# may not do anything, but we need to know the name to generate the HTML |
|
|
# for the menu. |
|
|
my $changeName = "${name}_setNotes"; |
|
|
my $retVal = $cgi->popup_menu({name => $name, |
|
|
size => $height, |
|
|
onChange => "$changeName(this.value)", |
|
|
values => \@keys, |
|
|
}); |
|
|
# Create the change function. |
|
|
$retVal .= "\n<script language=\"javascript\">\n"; |
|
|
$retVal .= " function $changeName(fieldValue) {\n"; |
|
|
# The function only has a body if we have a control to store data about the |
|
|
# attribute. |
|
|
if ($options{notes} || $options{type} || $options{groups}) { |
|
|
# Check to see if we're storing HTML or text into the note control. |
|
|
my $noteControl = $options{notes}; |
|
|
my $htmlMode = ($noteControl && $noteControl =~ /innerHTML$/); |
|
|
# We use a CASE statement based on the newly-selected field value. The |
|
|
# field description will be stored in the JavaScript variable "myText" |
|
|
# and the data type in "myType". Note the default data type is a normal |
|
|
# string, but the default notes is an empty string. |
|
|
$retVal .= " var myText = \"\";\n"; |
|
|
$retVal .= " var myType = \"string\";\n"; |
|
|
$retVal .= " switch (fieldValue) {\n"; |
|
|
# Loop through the keys. |
|
|
for my $key (@keys) { |
|
|
# Generate this case. |
|
|
$retVal .= " case \"$key\" :\n"; |
|
|
# Here we either want to update the note display, the |
|
|
# type display, the group list, or a combination of them. |
|
|
my ($type, $notes, @groups) = @{$keys{$key}}; |
|
|
if ($noteControl) { |
|
|
# Insure it's in the proper form. |
|
|
if ($htmlMode) { |
|
|
$notes = ERDB::HTMLNote($notes); |
|
|
} |
|
|
# Escape it for use as a string literal. |
|
|
$notes =~ s/\n/\\n/g; |
|
|
$notes =~ s/"/\\"/g; |
|
|
$retVal .= " myText = \"$notes\";\n"; |
|
|
} |
|
|
if ($options{type}) { |
|
|
# Here we want the type updated. |
|
|
$retVal .= " myType = \"$type\";\n"; |
|
|
} |
|
|
if ($options{groups}) { |
|
|
# Here we want the groups shown. Get a list of this attribute's groups. |
|
|
# We'll search through this list for each group to see if it belongs with |
|
|
# our attribute. |
|
|
my $groupLiteral = "=" . join("=", @groups) . "="; |
|
|
# Now we need some variables containing useful code for the javascript. It's |
|
|
# worth knowing we go through a bit of pain to insure $groupField[i] isn't |
|
|
# parsed as an array element. |
|
|
my $groupField = $options{groups}; |
|
|
my $currentField = $groupField . "[i]"; |
|
|
# Do the javascript. |
|
|
$retVal .= " var groupList = \"$groupLiteral\";\n"; |
|
|
$retVal .= " for (var i = 0; i < $groupField.length; i++) {\n"; |
|
|
$retVal .= " var srchString = \"=\" + $currentField.value + \"=\";\n"; |
|
|
$retVal .= " var srchLoc = groupList.indexOf(srchString);\n"; |
|
|
$retVal .= " $currentField.checked = (srchLoc >= 0);\n"; |
|
|
$retVal .= " }\n"; |
|
|
} |
|
|
# Close this case. |
|
|
$retVal .= " break;\n"; |
|
|
} |
|
|
# Close the CASE statement and make the appropriate assignments. |
|
|
$retVal .= " }\n"; |
|
|
if ($noteControl) { |
|
|
$retVal .= " $noteControl = myText;\n"; |
|
|
} |
|
|
if ($options{type}) { |
|
|
$retVal .= " $options{type} = myType;\n"; |
|
|
} |
|
|
} |
|
|
# Terminate the change function. |
|
|
$retVal .= " }\n"; |
|
|
$retVal .= "</script>\n"; |
|
|
# Return the result. |
|
|
return $retVal; |
|
|
} |
|
848 |
|
|
849 |
=head3 GetGroups |
=head3 GetGroups |
850 |
|
|
851 |
C<< my @groups = $attrDB->GetGroups(); >> |
my @groups = $attrDB->GetGroups(); |
852 |
|
|
853 |
Return a list of the available groups. |
Return a list of the available groups. |
854 |
|
|
865 |
|
|
866 |
=head3 GetAttributeData |
=head3 GetAttributeData |
867 |
|
|
868 |
C<< my %keys = $attrDB->GetAttributeData($type, @list); >> |
my %keys = $attrDB->GetAttributeData($type, @list); |
869 |
|
|
870 |
Return attribute data for the selected attributes. The attribute |
Return attribute data for the selected attributes. The attribute |
871 |
data is a hash mapping each attribute key name to a n-tuple containing the |
data is a hash mapping each attribute key name to a n-tuple containing the |
872 |
data type, the description, and the groups. This is the same format expected in |
data type, the description, the table name, and the groups. |
|
the L</FieldMenu> and L</ControlForm> methods for the list of attributes to display. |
|
873 |
|
|
874 |
=over 4 |
=over 4 |
875 |
|
|
884 |
|
|
885 |
=item RETURN |
=item RETURN |
886 |
|
|
887 |
Returns a hash mapping each attribute key name to its data type, description, and |
Returns a hash mapping each attribute key name to its description, |
888 |
parent groups. |
table name, and parent groups. |
889 |
|
|
890 |
=back |
=back |
891 |
|
|
917 |
} |
} |
918 |
while (my $row = $query->Fetch()) { |
while (my $row = $query->Fetch()) { |
919 |
# Get this attribute's data. |
# Get this attribute's data. |
920 |
my ($key, $type, $notes) = $row->Values(['AttributeKey(id)', 'AttributeKey(data-type)', |
my ($key, $relName, $notes) = $row->Values(['AttributeKey(id)', |
921 |
|
'AttributeKey(relationship-name)', |
922 |
'AttributeKey(description)']); |
'AttributeKey(description)']); |
923 |
# If it's new, get its groups and add it to the return hash. |
# If it's new, get its groups and add it to the return hash. |
924 |
if (! exists $retVal{$key}) { |
if (! exists $retVal{$key}) { |
925 |
my @groups = $self->GetFlat(['IsInGroup'], "IsInGroup(from-link) = ?", |
my @groups = $self->GetFlat(['IsInGroup'], "IsInGroup(from-link) = ?", |
926 |
[$key], 'IsInGroup(to-link)'); |
[$key], 'IsInGroup(to-link)'); |
927 |
$retVal{$key} = [$type, $notes, @groups]; |
$retVal{$key} = [$relName, $notes, @groups]; |
928 |
} |
} |
929 |
} |
} |
930 |
} |
} |
934 |
|
|
935 |
=head3 LogOperation |
=head3 LogOperation |
936 |
|
|
937 |
C<< $ca->LogOperation($action, $target, $description); >> |
$ca->LogOperation($action, $target, $description); |
938 |
|
|
939 |
Write an operation description to the attribute activity log (C<$FIG_Config::var/attributes.log>). |
Write an operation description to the attribute activity log (C<$FIG_Config::var/attributes.log>). |
940 |
|
|
971 |
close $oh; |
close $oh; |
972 |
} |
} |
973 |
|
|
|
=head2 Internal Utility Methods |
|
|
|
|
|
=head3 _KeywordString |
|
|
|
|
|
C<< my $keywordString = $ca->_KeywordString($key, $value); >> |
|
|
|
|
|
Compute the keyword string for a specified key/value pair. This consists of the |
|
|
key name and value converted to lower case with underscores translated to spaces. |
|
|
|
|
|
This method is for internal use only. It is called whenever we need to update or |
|
|
insert a B<HasValueFor> record. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item key |
|
|
|
|
|
Name of the relevant attribute key. |
|
|
|
|
|
=item target |
|
|
|
|
|
ID of the target object to which this key/value pair will be associated. |
|
|
|
|
|
=item value |
|
|
|
|
|
The value to store for this key/object combination. |
|
|
|
|
|
=item RETURN |
|
|
|
|
|
Returns the value that should be stored as the keyword string for the specified |
|
|
key/value pair. |
|
|
|
|
|
=back |
|
|
|
|
|
=cut |
|
|
|
|
|
sub _KeywordString { |
|
|
# Get the parameters. |
|
|
my ($self, $key, $value) = @_; |
|
|
# Get a copy of the key name and convert underscores to spaces. |
|
|
my $keywordString = $key; |
|
|
$keywordString =~ s/_/ /g; |
|
|
# Add the value convert it all to lower case. |
|
|
my $retVal = lc "$keywordString $value"; |
|
|
# Return the result. |
|
|
return $retVal; |
|
|
} |
|
|
|
|
|
=head3 _QueryResults |
|
|
|
|
|
C<< my @attributeList = $attrDB->_QueryResults($query, @values); >> |
|
|
|
|
|
Match the results of a B<HasValueFor> query against value criteria and return |
|
|
the results. This is an internal method that splits the values coming back |
|
|
and matches the sections against the specified section patterns. It serves |
|
|
as the back end to L</GetAttributes> and L</FindAttributes>. |
|
|
|
|
|
=over 4 |
|
|
|
|
|
=item query |
|
|
|
|
|
A query object that will return the desired B<HasValueFor> records. |
|
|
|
|
|
=item values |
|
|
|
|
|
List of the desired attribute values, section by section. If C<undef> |
|
|
or an empty string is specified, all values in that section will match. A |
|
|
generic match can be requested by placing a percent sign (C<%>) at the end. |
|
|
In that case, all values that match up to and not including the percent sign |
|
|
will match. You may also specify a regular expression enclosed |
|
|
in slashes. All values that match the regular expression will be returned. For |
|
|
performance reasons, only values have this extra capability. |
|
|
|
|
|
=item RETURN |
|
|
|
|
|
Returns a list of tuples. The first element in the tuple is an object ID, the |
|
|
second is an attribute key, and the remaining elements are the sections of |
|
|
the attribute value. All of the tuples will match the criteria set forth in |
|
|
the parameter list. |
|
|
|
|
|
=back |
|
|
|
|
|
=cut |
|
|
|
|
|
sub _QueryResults { |
|
|
# Get the parameters. |
|
|
my ($self, $query, @values) = @_; |
|
|
# Declare the return value. |
|
|
my @retVal = (); |
|
|
# Get the number of value sections we have to match. |
|
|
my $sectionCount = scalar(@values); |
|
|
# Loop through the assignments found. |
|
|
while (my $row = $query->Fetch()) { |
|
|
# Get the current row's data. |
|
|
my ($id, $realKey, $subKey, $valueString) = $row->Values(['HasValueFor(to-link)', |
|
|
'HasValueFor(from-link)', |
|
|
'HasValueFor(subkey)', |
|
|
'HasValueFor(value)' |
|
|
]); |
|
|
# Form the key from the real key and the sub key. |
|
|
my $key = $self->JoinKey($realKey, $subKey); |
|
|
# 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. |
|
|
my $value = $values[$i]; |
|
|
Trace("Current value pattern is \"$value\".") if T(4); |
|
|
if (substr($value, -1, 1) eq '%') { |
|
|
Trace("Generic match used.") if T(4); |
|
|
# Here we have a generic match. |
|
|
my $matchLen = length($values[$i]) - 1; |
|
|
$matching = substr($sections[$i], 0, $matchLen) eq |
|
|
substr($values[$i], 0, $matchLen); |
|
|
} elsif ($value =~ m#^/(.+)/[a-z]*$#) { |
|
|
Trace("Regular expression detected.") if T(4); |
|
|
# Here we have a regular expression match. |
|
|
my $section = $sections[$i]; |
|
|
$matching = eval("\$section =~ $value"); |
|
|
} else { |
|
|
# Here we have a strict match. |
|
|
Trace("Strict match used.") if T(4); |
|
|
$matching = ($sections[$i] eq $values[$i]); |
|
|
} |
|
|
} |
|
|
# If we match, output this row to the return list. |
|
|
if ($matching) { |
|
|
push @retVal, [$id, $key, @sections]; |
|
|
} |
|
|
} |
|
|
# Return the rows found. |
|
|
return @retVal; |
|
|
} |
|
|
|
|
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. |
998 |
|
|
999 |
=head3 GetAttributes |
=head3 GetAttributes |
1000 |
|
|
1001 |
C<< my @attributeList = $attrDB->GetAttributes($objectID, $key, @values); >> |
my @attributeList = $attrDB->GetAttributes($objectID, $key, @values); |
1002 |
|
|
1003 |
In the database, attribute values are sectioned into pieces using a splitter |
In the database, attribute values are sectioned into pieces using a splitter |
1004 |
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 |
1083 |
sub GetAttributes { |
sub GetAttributes { |
1084 |
# Get the parameters. |
# Get the parameters. |
1085 |
my ($self, $objectID, $key, @values) = @_; |
my ($self, $objectID, $key, @values) = @_; |
1086 |
# This hash will map "HasValueFor" fields to patterns. We use it to build the |
# This hash will map value-table fields to patterns. We use it to build the |
1087 |
# SQL statement. |
# SQL statement. |
1088 |
my %data; |
my %data; |
|
# Before we do anything else, we must parse the key. The key is treated by the |
|
|
# user as a single field, but to us it's actually a real key and a subkey. |
|
|
# If the key has no splitter and is exact, the real key is the original key |
|
|
# and the subkey is an empty string. If the key has a splitter, it is |
|
|
# split into two pieces and each piece is processed separately. If the key has |
|
|
# no splitter and is generic, the real key is the incoming key and the subkey |
|
|
# is allowed to be wild. Of course, this only matters if an actual key has |
|
|
# been specified. |
|
|
if (defined $key) { |
|
|
if ($key =~ /$self->{splitter}/) { |
|
|
# Here we have a two-part key, so we split it normally. |
|
|
my ($realKey, $subKey) = $self->SplitKey($key); |
|
|
$data{'HasValueFor(from-link)'} = $realKey; |
|
|
$data{'HasValueFor(subkey)'} = $subKey; |
|
|
} elsif (substr($key, -1, 1) eq '%') { |
|
|
$data{'HasValueFor(from-link)'} = $key; |
|
|
} else { |
|
|
$data{'HasValueFor(from-link)'} = $key; |
|
|
$data{'HasValueFor(subkey)'} = ''; |
|
|
} |
|
|
} |
|
1089 |
# Add the object ID to the key information. |
# Add the object ID to the key information. |
1090 |
$data{'HasValueFor(to-link)'} = $objectID; |
$data{'to-link'} = $objectID; |
1091 |
# The first value represents a problem, because we can search it using SQL, but not |
# The first value represents a problem, because we can search it using SQL, but not |
1092 |
# in the normal way. If the user specifies a generic search or exact match for |
# in the normal way. If the user specifies a generic search or exact match for |
1093 |
# every alternative value (remember, the values may be specified as a list), |
# every alternative value (remember, the values may be specified as a list), |
1124 |
} |
} |
1125 |
# If everything works, add the value data to the filtering hash. |
# If everything works, add the value data to the filtering hash. |
1126 |
if ($okValues) { |
if ($okValues) { |
1127 |
$data{'HasValueFor(value)'} = \@valuePatterns; |
$data{value} = \@valuePatterns; |
1128 |
|
} |
1129 |
|
} |
1130 |
|
# Now comes the really tricky part, which is key handling. The key is |
1131 |
|
# actually split in two parts: the real key and a sub-key. The real key |
1132 |
|
# determines which value table contains the relevant values. The information |
1133 |
|
# we need is kept in here. |
1134 |
|
my %tables = map { $_ => [] } $self->_GetAllTables(); |
1135 |
|
# See if we have any key filtering to worry about. |
1136 |
|
if ($key) { |
1137 |
|
# Here we have either a single key or a list. We convert both cases to a list. |
1138 |
|
my $keyList = (ref $key ne 'ARRAY' ? [$key] : $key); |
1139 |
|
# Get easy access to the key/table hash. |
1140 |
|
my $keyTableHash = $self->_KeyTable(); |
1141 |
|
# Loop through the keys, discovering tables. |
1142 |
|
for my $keyChoice (@$keyList) { |
1143 |
|
# Now we have to start thinking about the real key and the subkeys. |
1144 |
|
my ($realKey, $subKey) = $self->_SplitKeyPattern($keyChoice); |
1145 |
|
# Find the matches for the real key in the key hash. For each of |
1146 |
|
# these, we memorize the table name in the hash below. |
1147 |
|
my %tableNames = (); |
1148 |
|
for my $keyInTable (keys %{$keyTableHash}) { |
1149 |
|
if ($self->_CheckSQLPattern($realKey, $keyInTable)) { |
1150 |
|
$tableNames{$keyTableHash->{$key}} = 1; |
1151 |
|
} |
1152 |
|
} |
1153 |
|
# If the key is generic, or didn't match anything, add |
1154 |
|
# the default table to the mix. |
1155 |
|
if (keys %tableNames == 0 || $keyChoice =~ /%/) { |
1156 |
|
$tableNames{$self->{defaultRel}} = 1; |
1157 |
|
} |
1158 |
|
# Now we add this key combination to the key list for each relevant table. |
1159 |
|
for my $tableName (keys %tableNames) { |
1160 |
|
push @{$tables{$tableName}}, [$realKey, $subKey]; |
1161 |
} |
} |
1162 |
} |
} |
1163 |
|
} |
1164 |
|
# Declare the return variable. |
1165 |
|
my @retVal = (); |
1166 |
|
# Now we loop through the tables of interest, performing queries. |
1167 |
|
# Loop through the tables. |
1168 |
|
for my $table (keys %tables) { |
1169 |
|
# Get the key pairs for this table. |
1170 |
|
my $pairs = $tables{$table}; |
1171 |
|
# Does this table have data? It does if there is no key specified or |
1172 |
|
# it has at least one key pair. |
1173 |
|
my $pairCount = scalar @{$pairs}; |
1174 |
|
Trace("Pair count for table $table is $pairCount.") if T(3); |
1175 |
|
if ($pairCount || ! $key) { |
1176 |
# Create some lists to contain the filter fragments and parameter values. |
# Create some lists to contain the filter fragments and parameter values. |
1177 |
my @filter = (); |
my @filter = (); |
1178 |
my @parms = (); |
my @parms = (); |
1179 |
# 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 |
1180 |
# parameter list and generates filters for each. The %data hash that we built above |
# parameter list and generates filters for each. The %data hash that we built above |
1181 |
# contains all the necessary information to do this. |
# contains most of the necessary information to do this. When we're done, we'll |
1182 |
|
# paste on stuff for the key pairs. |
1183 |
for my $field (keys %data) { |
for my $field (keys %data) { |
1184 |
# Accumulate filter information for this field. We will OR together all the |
# Accumulate filter information for this field. We will OR together all the |
1185 |
# elements accumulated to create the final result. |
# elements accumulated to create the final result. |
1186 |
my @fieldFilter = (); |
my @fieldFilter = (); |
1187 |
# Get the specified data from the caller. |
# Get the specified filter for this field. |
1188 |
my $fieldPattern = $data{$field}; |
my $fieldPattern = $data{$field}; |
1189 |
# Only proceed if the pattern is one that won't match everything. |
# Only proceed if the pattern is one that won't match everything. |
1190 |
if (defined($fieldPattern) && $fieldPattern ne "" && $fieldPattern ne "%") { |
if (defined($fieldPattern) && $fieldPattern ne "" && $fieldPattern ne "%") { |
1201 |
if (@patterns) { |
if (@patterns) { |
1202 |
# Loop through the individual patterns. |
# Loop through the individual patterns. |
1203 |
for my $pattern (@patterns) { |
for my $pattern (@patterns) { |
1204 |
# Check for a generic request. |
my ($clause, $value) = _WherePart($table, $field, $pattern); |
1205 |
if (substr($pattern, -1, 1) ne '%') { |
push @fieldFilter, $clause; |
1206 |
# Here we have a normal request. |
push @parms, $value; |
|
push @fieldFilter, "$field = ?"; |
|
|
push @parms, $pattern; |
|
|
} else { |
|
|
# Here we have a generic request, so we will use the LIKE operator to |
|
|
# filter the field to this value pattern. |
|
|
push @fieldFilter, "$field LIKE ?"; |
|
|
# We must convert the pattern value to an SQL match pattern. First |
|
|
# we get a copy of it. |
|
|
my $actualPattern = $pattern; |
|
|
# Now we escape the underscores. Underscores are an SQL wild card |
|
|
# character, but they are used frequently in key names and object IDs. |
|
|
$actualPattern =~ s/_/\\_/g; |
|
|
# Add the escaped pattern to the bound parameter list. |
|
|
push @parms, $actualPattern; |
|
|
} |
|
1207 |
} |
} |
1208 |
# Form the filter for this field. |
# Form the filter for this field. |
1209 |
my $fieldFilterString = join(" OR ", @fieldFilter); |
my $fieldFilterString = join(" OR ", @fieldFilter); |
1211 |
} |
} |
1212 |
} |
} |
1213 |
} |
} |
1214 |
# Now @filter contains one or more filter strings and @parms contains the parameter |
# The final filter is for the key pairs. Only proceed if we have some. |
1215 |
# values to bind to them. |
if ($pairCount) { |
1216 |
|
# We'll accumulate pair filter clauses in here. |
1217 |
|
my @pairFilters = (); |
1218 |
|
# Loop through the key pairs. |
1219 |
|
for my $pair (@$pairs) { |
1220 |
|
my ($realKey, $subKey) = @{$pair}; |
1221 |
|
my ($realClause, $realValue) = _WherePart($table, 'from-link', $realKey); |
1222 |
|
if (! $subKey) { |
1223 |
|
# Here the subkey is wild, so only the real key matters. |
1224 |
|
push @pairFilters, $realClause; |
1225 |
|
push @parms, $realValue; |
1226 |
|
} else { |
1227 |
|
# Here we have to select on both keys. |
1228 |
|
my ($subClause, $subValue) = _WherePart($table, 'subkey', $subKey); |
1229 |
|
push @pairFilters, "($realClause AND $subClause)"; |
1230 |
|
push @parms, $subValue; |
1231 |
|
} |
1232 |
|
} |
1233 |
|
# Join the pair filters together to make a giant key filter. |
1234 |
|
my $pairFilter = "(" . join(" OR ", @pairFilters) . ")"; |
1235 |
|
push @filter, $pairFilter; |
1236 |
|
} |
1237 |
|
# At this point, @filter contains one or more filter strings and @parms |
1238 |
|
# contains the parameter values to bind to them. |
1239 |
my $actualFilter = join(" AND ", @filter); |
my $actualFilter = join(" AND ", @filter); |
1240 |
# Now we're ready to make our query. |
# Now we're ready to make our query. |
1241 |
my $query = $self->Get(['HasValueFor'], $actualFilter, \@parms); |
my $query = $self->Get([$table], $actualFilter, \@parms); |
1242 |
# Format the results. |
# Format the results. |
1243 |
my @retVal = $self->_QueryResults($query, @values); |
push @retVal, $self->_QueryResults($query, $table, @values); |
1244 |
# Return the rows found. |
} |
1245 |
|
} |
1246 |
|
# The above loop ran the query for each necessary value table and merged the |
1247 |
|
# results into @retVal. Now we return the rows found. |
1248 |
return @retVal; |
return @retVal; |
1249 |
} |
} |
1250 |
|
|
1251 |
=head3 AddAttribute |
=head3 AddAttribute |
1252 |
|
|
1253 |
C<< $attrDB->AddAttribute($objectID, $key, @values); >> |
$attrDB->AddAttribute($objectID, $key, @values); |
1254 |
|
|
1255 |
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 |
1256 |
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. |
1291 |
my $valueString = join($self->{splitter}, @values); |
my $valueString = join($self->{splitter}, @values); |
1292 |
# Split up the key. |
# Split up the key. |
1293 |
my ($realKey, $subKey) = $self->SplitKey($key); |
my ($realKey, $subKey) = $self->SplitKey($key); |
1294 |
|
# Find the table containing the key. |
1295 |
|
my $table = $self->_KeyTable($realKey); |
1296 |
# Connect the object to the key. |
# Connect the object to the key. |
1297 |
$self->InsertObject('HasValueFor', { 'from-link' => $realKey, |
$self->InsertObject($table, { 'from-link' => $realKey, |
1298 |
'to-link' => $objectID, |
'to-link' => $objectID, |
1299 |
'subkey' => $subKey, |
'subkey' => $subKey, |
1300 |
'value' => $valueString, |
'value' => $valueString, |
1306 |
|
|
1307 |
=head3 DeleteAttribute |
=head3 DeleteAttribute |
1308 |
|
|
1309 |
C<< $attrDB->DeleteAttribute($objectID, $key, @values); >> |
$attrDB->DeleteAttribute($objectID, $key, @values); |
1310 |
|
|
1311 |
Delete the specified attribute key/value combination from the database. |
Delete the specified attribute key/value combination from the database. |
1312 |
|
|
1340 |
} else { |
} else { |
1341 |
# Split the key into the real key and the subkey. |
# Split the key into the real key and the subkey. |
1342 |
my ($realKey, $subKey) = $self->SplitKey($key); |
my ($realKey, $subKey) = $self->SplitKey($key); |
1343 |
|
# Find the table containing the key's values. |
1344 |
|
my $table = $self->_KeyTable($realKey); |
1345 |
if ($subKey eq '' && scalar(@values) == 0) { |
if ($subKey eq '' && scalar(@values) == 0) { |
1346 |
# Here we erase the entire key for this object. |
# Here we erase the entire key for this object. |
1347 |
$self->DeleteRow('HasValueFor', $key, $objectID); |
$self->DeleteRow('HasValueFor', $key, $objectID); |
1358 |
|
|
1359 |
=head3 DeleteMatchingAttributes |
=head3 DeleteMatchingAttributes |
1360 |
|
|
1361 |
C<< my @deleted = $attrDB->DeleteMatchingAttributes($objectID, $key, @values); >> |
my @deleted = $attrDB->DeleteMatchingAttributes($objectID, $key, @values); |
1362 |
|
|
1363 |
Delete all attributes that match the specified criteria. This is equivalent to |
Delete all attributes that match the specified criteria. This is equivalent to |
1364 |
calling L</GetAttributes> and then invoking L</DeleteAttribute> for each |
calling L</GetAttributes> and then invoking L</DeleteAttribute> for each |
1418 |
|
|
1419 |
=head3 ChangeAttribute |
=head3 ChangeAttribute |
1420 |
|
|
1421 |
C<< $attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); >> |
$attrDB->ChangeAttribute($objectID, $key, \@oldValues, \@newValues); |
1422 |
|
|
1423 |
Change the value of an attribute key/value pair for an object. |
Change the value of an attribute key/value pair for an object. |
1424 |
|
|
1470 |
|
|
1471 |
=head3 EraseAttribute |
=head3 EraseAttribute |
1472 |
|
|
1473 |
C<< $attrDB->EraseAttribute($key); >> |
$attrDB->EraseAttribute($key); |
1474 |
|
|
1475 |
Erase all values for the specified attribute key. This does not remove the |
Erase all values for the specified attribute key. This does not remove the |
1476 |
key from the database; it merely removes all the values. |
key from the database; it merely removes all the values. |
1489 |
sub EraseAttribute { |
sub EraseAttribute { |
1490 |
# Get the parameters. |
# Get the parameters. |
1491 |
my ($self, $key) = @_; |
my ($self, $key) = @_; |
1492 |
# Delete everything connected to the key. |
# Find the table containing the key. |
1493 |
|
my $table = $self->_KeyTable($key); |
1494 |
|
# Is it the default table? |
1495 |
|
if ($table eq $self->{defaultRel}) { |
1496 |
|
# Yes, so the key is mixed in with other keys. |
1497 |
|
# Delete everything connected to it. |
1498 |
$self->Disconnect('HasValueFor', 'AttributeKey', $key); |
$self->Disconnect('HasValueFor', 'AttributeKey', $key); |
1499 |
|
} else { |
1500 |
|
# No. Drop and re-create the table. |
1501 |
|
$self->TruncateTable($table); |
1502 |
|
} |
1503 |
# Log the operation. |
# Log the operation. |
1504 |
$self->LogOperation("Erase Data", $key); |
$self->LogOperation("Erase Data", $key); |
1505 |
# Return a 1, for backward compatability. |
# Return a 1, for backward compatability. |
1508 |
|
|
1509 |
=head3 GetAttributeKeys |
=head3 GetAttributeKeys |
1510 |
|
|
1511 |
C<< my @keyList = $attrDB->GetAttributeKeys($groupName); >> |
my @keyList = $attrDB->GetAttributeKeys($groupName); |
1512 |
|
|
1513 |
Return a list of the attribute keys for a particular group. |
Return a list of the attribute keys for a particular group. |
1514 |
|
|
1536 |
return sort @groups; |
return sort @groups; |
1537 |
} |
} |
1538 |
|
|
1539 |
|
=head3 QueryAttributes |
1540 |
|
|
1541 |
|
my @attributeData = $ca->QueryAttributes($filter, $filterParms); |
1542 |
|
|
1543 |
|
Return the attribute data based on an SQL filter clause. In the filter clause, |
1544 |
|
the name C<$object> should be used for the object ID, C<$key> should be used for |
1545 |
|
the key name, C<$subkey> for the subkey value, and C<$value> for the value field. |
1546 |
|
|
1547 |
|
=over 4 |
1548 |
|
|
1549 |
|
=item filter |
1550 |
|
|
1551 |
|
Filter clause in the standard ERDB format, except that the field names are C<$object> for |
1552 |
|
the object ID field, C<$key> for the key name field, C<$subkey> for the subkey field, |
1553 |
|
and C<$value> for the value field. This abstraction enables us to hide the details of |
1554 |
|
the database construction from the user. |
1555 |
|
|
1556 |
|
=item filterParms |
1557 |
|
|
1558 |
|
Parameters for the filter clause. |
1559 |
|
|
1560 |
|
=item RETURN |
1561 |
|
|
1562 |
|
Returns a list of tuples. Each tuple consists of an object ID, a key (with optional subkey), and |
1563 |
|
one or more attribute values. |
1564 |
|
|
1565 |
|
=back |
1566 |
|
|
1567 |
|
=cut |
1568 |
|
|
1569 |
|
# This hash is used to drive the substitution process. |
1570 |
|
my %AttributeParms = (object => 'to-link', |
1571 |
|
key => 'from-link', |
1572 |
|
subkey => 'subkey', |
1573 |
|
value => 'value'); |
1574 |
|
|
1575 |
|
sub QueryAttributes { |
1576 |
|
# Get the parameters. |
1577 |
|
my ($self, $filter, $filterParms) = @_; |
1578 |
|
# Declare the return variable. |
1579 |
|
my @retVal = (); |
1580 |
|
# Make sue we have filter parameters. |
1581 |
|
my $realParms = (defined($filterParms) ? $filterParms : []); |
1582 |
|
# Loop through all the value tables. |
1583 |
|
for my $table ($self->_GetAllTables()) { |
1584 |
|
# Create the query for this table by converting the filter. |
1585 |
|
my $realFilter = $filter; |
1586 |
|
for my $name (keys %AttributeParms) { |
1587 |
|
$realFilter =~ s/\$$name/$table($AttributeParms{$name})/g; |
1588 |
|
} |
1589 |
|
my $query = $self->Get([$table], $realFilter, $realParms); |
1590 |
|
# Loop through the results, forming the output attribute tuples. |
1591 |
|
while (my $result = $query->Fetch()) { |
1592 |
|
# Get the four values from this query result row. |
1593 |
|
my ($objectID, $key, $subkey, $value) = $result->Values(["$table($AttributeParms{object})", |
1594 |
|
"$table($AttributeParms{key})", |
1595 |
|
"$table($AttributeParms{subkey})", |
1596 |
|
"$table($AttributeParms{value})"]); |
1597 |
|
# Combine the key and the subkey. |
1598 |
|
my $realKey = ($subkey ? $key . $self->{splitter} . $subkey : $key); |
1599 |
|
# Split the value. |
1600 |
|
my @values = split $self->{splitter}, $value; |
1601 |
|
# Output the result. |
1602 |
|
push @retVal, [$objectID, $realKey, @values]; |
1603 |
|
} |
1604 |
|
} |
1605 |
|
# Return the result. |
1606 |
|
return @retVal; |
1607 |
|
} |
1608 |
|
|
1609 |
=head2 Key and ID Manipulation Methods |
=head2 Key and ID Manipulation Methods |
1610 |
|
|
1611 |
=head3 ParseID |
=head3 ParseID |
1612 |
|
|
1613 |
C<< my ($type, $id) = CustomAttributes::ParseID($idValue); >> |
my ($type, $id) = CustomAttributes::ParseID($idValue); |
1614 |
|
|
1615 |
Determine the type and object ID corresponding to an ID value from the attribute database. |
Determine the type and object ID corresponding to an ID value from the attribute database. |
1616 |
Most ID values consist of a type name and an ID, separated by a colon (e.g. C<Family:aclame|cluster10>); |
Most ID values consist of a type name and an ID, separated by a colon (e.g. C<Family:aclame|cluster10>); |
1649 |
if ($idValue =~ /^([A-Za-z]+):(.+)/) { |
if ($idValue =~ /^([A-Za-z]+):(.+)/) { |
1650 |
# Here we have a typed ID. |
# Here we have a typed ID. |
1651 |
($type, $id) = ($1, $2); |
($type, $id) = ($1, $2); |
1652 |
|
# Fix the case sensitivity on PDB IDs. |
1653 |
|
if ($type eq 'PDB') { $id = lc $id; } |
1654 |
} elsif ($idValue =~ /fig\|/) { |
} elsif ($idValue =~ /fig\|/) { |
1655 |
# Here we have a feature ID. |
# Here we have a feature ID. |
1656 |
($type, $id) = (Feature => $idValue); |
($type, $id) = (Feature => $idValue); |
1667 |
|
|
1668 |
=head3 FormID |
=head3 FormID |
1669 |
|
|
1670 |
C<< my $idValue = CustomAttributes::FormID($type, $id); >> |
my $idValue = CustomAttributes::FormID($type, $id); |
1671 |
|
|
1672 |
Convert an object type and ID pair into an object ID string for the attribute system. Subsystems, |
Convert an object type and ID pair into an object ID string for the attribute system. Subsystems, |
1673 |
genomes, and features are stored in the database without type information, but all other object IDs |
genomes, and features are stored in the database without type information, but all other object IDs |
1708 |
|
|
1709 |
=head3 GetTargetObject |
=head3 GetTargetObject |
1710 |
|
|
1711 |
C<< my $object = CustomAttributes::GetTargetObject($erdb, $idValue); >> |
my $object = CustomAttributes::GetTargetObject($erdb, $idValue); |
1712 |
|
|
1713 |
Return the database object corresponding to the specified attribute object ID. The |
Return the database object corresponding to the specified attribute object ID. The |
1714 |
object type associated with the ID value must correspond to an entity name in the |
object type associated with the ID value must correspond to an entity name in the |
1747 |
|
|
1748 |
=head3 SplitKey |
=head3 SplitKey |
1749 |
|
|
1750 |
C<< my ($realKey, $subKey) = $ca->SplitKey($key); >> |
my ($realKey, $subKey) = $ca->SplitKey($key); |
1751 |
|
|
1752 |
Split an external key (that is, one passed in by a caller) into the real key and the sub key. |
Split an external key (that is, one passed in by a caller) into the real key and the sub key. |
1753 |
The real and sub keys are separated by a splitter value (usually C<::>). If there is no splitter, |
The real and sub keys are separated by a splitter value (usually C<::>). If there is no splitter, |
1781 |
return ($realKey, $subKey); |
return ($realKey, $subKey); |
1782 |
} |
} |
1783 |
|
|
1784 |
|
|
1785 |
=head3 JoinKey |
=head3 JoinKey |
1786 |
|
|
1787 |
C<< my $key = $ca->JoinKey($realKey, $subKey); >> |
my $key = $ca->JoinKey($realKey, $subKey); |
1788 |
|
|
1789 |
Join a real key and a subkey together to make an external key. The external key is the attribute key |
Join a real key and a subkey together to make an external key. The external key is the attribute key |
1790 |
used by the caller. The real key and the subkey are how the keys are represented in the database. The |
used by the caller. The real key and the subkey are how the keys are represented in the database. The |
1826 |
return $retVal; |
return $retVal; |
1827 |
} |
} |
1828 |
|
|
1829 |
|
|
1830 |
|
=head3 AttributeTable |
1831 |
|
|
1832 |
|
my $tableHtml = CustomAttributes::AttributeTable($cgi, @attrList); |
1833 |
|
|
1834 |
|
Format the attribute data into an HTML table. |
1835 |
|
|
1836 |
|
=over 4 |
1837 |
|
|
1838 |
|
=item cgi |
1839 |
|
|
1840 |
|
CGI query object used to generate the HTML |
1841 |
|
|
1842 |
|
=item attrList |
1843 |
|
|
1844 |
|
List of attribute results, in the format returned by the L</GetAttributes> or |
1845 |
|
L</QueryAttributes> methods. |
1846 |
|
|
1847 |
|
=item RETURN |
1848 |
|
|
1849 |
|
Returns an HTML table displaying the attribute keys and values. |
1850 |
|
|
1851 |
|
=back |
1852 |
|
|
1853 |
|
=cut |
1854 |
|
|
1855 |
|
sub AttributeTable { |
1856 |
|
# Get the parameters. |
1857 |
|
my ($cgi, @attrList) = @_; |
1858 |
|
# Accumulate the table rows. |
1859 |
|
my @html = (); |
1860 |
|
for my $attrData (@attrList) { |
1861 |
|
# Format the object ID and key. |
1862 |
|
my @columns = map { CGI::escapeHTML($_) } @{$attrData}[0,1]; |
1863 |
|
# Now we format the values. These remain unchanged unless one of them is a URL. |
1864 |
|
my $lastValue = scalar(@{$attrData}) - 1; |
1865 |
|
push @columns, map { $_ =~ /^http:/ ? $cgi->a({ href => $_ }, $_) : $_ } @{$attrData}[2 .. $lastValue]; |
1866 |
|
# Assemble the values into a table row. |
1867 |
|
push @html, $cgi->Tr($cgi->td(\@columns)); |
1868 |
|
} |
1869 |
|
# Format the table in the return variable. |
1870 |
|
my $retVal = $cgi->table({ border => 2 }, $cgi->Tr($cgi->th(['Object', 'Key', 'Values'])), @html); |
1871 |
|
# Return it. |
1872 |
|
return $retVal; |
1873 |
|
} |
1874 |
|
|
1875 |
|
|
1876 |
|
=head2 Internal Utility Methods |
1877 |
|
|
1878 |
|
=head3 _KeyTable |
1879 |
|
|
1880 |
|
my $tableName = $ca->_KeyTable($keyName); |
1881 |
|
|
1882 |
|
Return the name of the table that contains the attribute values for the |
1883 |
|
specified key. |
1884 |
|
|
1885 |
|
Most attribute values are stored in the default table (usually C<HasValueFor>). |
1886 |
|
Some, however, are placed in private tables by themselves for performance reasons. |
1887 |
|
|
1888 |
|
=over 4 |
1889 |
|
|
1890 |
|
=item keyName (optional) |
1891 |
|
|
1892 |
|
Name of the attribute key whose table name is desired. If not specified, the |
1893 |
|
entire key/table hash is returned. |
1894 |
|
|
1895 |
|
=item RETURN |
1896 |
|
|
1897 |
|
Returns the name of the table containing the specified attribute key's values, |
1898 |
|
or a reference to a hash that maps key names to table names. |
1899 |
|
|
1900 |
|
=back |
1901 |
|
|
1902 |
|
=cut |
1903 |
|
|
1904 |
|
sub _KeyTable { |
1905 |
|
# Get the parameters. |
1906 |
|
my ($self, $keyName) = @_; |
1907 |
|
# Declare the return variable. |
1908 |
|
my $retVal; |
1909 |
|
# Insure the key table hash is present. |
1910 |
|
if (! exists $self->{keyTables}) { |
1911 |
|
$self->{keyTables} = { map { $_->[0] => $_->[1] } $self->GetAll(['AttributeKey'], |
1912 |
|
"AttributeKey(relationship-name) <> ?", |
1913 |
|
[$self->{defaultRel}], |
1914 |
|
['AttributeKey(id)', 'AttributeKey(relationship-name)']) }; |
1915 |
|
} |
1916 |
|
# Get the key hash. |
1917 |
|
my $keyHash = $self->{keyTables}; |
1918 |
|
# Does the user want a specific table or the whole thing? |
1919 |
|
if ($keyName) { |
1920 |
|
# Here we want a specific table. Is this key in the hash? |
1921 |
|
if (exists $keyHash->{$keyName}) { |
1922 |
|
# It's there, so return the specified table. |
1923 |
|
$retVal = $keyHash->{$keyName}; |
1924 |
|
} else { |
1925 |
|
# No, return the default table name. |
1926 |
|
$retVal = $self->{defaultRel}; |
1927 |
|
} |
1928 |
|
} else { |
1929 |
|
# Here we want the whole hash. |
1930 |
|
$retVal = $keyHash; |
1931 |
|
} |
1932 |
|
# Return the result. |
1933 |
|
return $retVal; |
1934 |
|
} |
1935 |
|
|
1936 |
|
|
1937 |
|
=head3 _QueryResults |
1938 |
|
|
1939 |
|
my @attributeList = $attrDB->_QueryResults($query, $table, @values); |
1940 |
|
|
1941 |
|
Match the results of a query against value criteria and return |
1942 |
|
the results. This is an internal method that splits the values coming back |
1943 |
|
and matches the sections against the specified section patterns. It serves |
1944 |
|
as the back end to L</GetAttributes> and L</FindAttributes>. |
1945 |
|
|
1946 |
|
=over 4 |
1947 |
|
|
1948 |
|
=item query |
1949 |
|
|
1950 |
|
A query object that will return the desired records. |
1951 |
|
|
1952 |
|
=item table |
1953 |
|
|
1954 |
|
Name of the value table for the query. |
1955 |
|
|
1956 |
|
=item values |
1957 |
|
|
1958 |
|
List of the desired attribute values, section by section. If C<undef> |
1959 |
|
or an empty string is specified, all values in that section will match. A |
1960 |
|
generic match can be requested by placing a percent sign (C<%>) at the end. |
1961 |
|
In that case, all values that match up to and not including the percent sign |
1962 |
|
will match. You may also specify a regular expression enclosed |
1963 |
|
in slashes. All values that match the regular expression will be returned. For |
1964 |
|
performance reasons, only values have this extra capability. |
1965 |
|
|
1966 |
|
=item RETURN |
1967 |
|
|
1968 |
|
Returns a list of tuples. The first element in the tuple is an object ID, the |
1969 |
|
second is an attribute key, and the remaining elements are the sections of |
1970 |
|
the attribute value. All of the tuples will match the criteria set forth in |
1971 |
|
the parameter list. |
1972 |
|
|
1973 |
|
=back |
1974 |
|
|
1975 |
|
=cut |
1976 |
|
|
1977 |
|
sub _QueryResults { |
1978 |
|
# Get the parameters. |
1979 |
|
my ($self, $query, $table, @values) = @_; |
1980 |
|
# Declare the return value. |
1981 |
|
my @retVal = (); |
1982 |
|
# Get the number of value sections we have to match. |
1983 |
|
my $sectionCount = scalar(@values); |
1984 |
|
# Loop through the assignments found. |
1985 |
|
while (my $row = $query->Fetch()) { |
1986 |
|
# Get the current row's data. |
1987 |
|
my ($id, $realKey, $subKey, $valueString) = $row->Values(["$table(to-link)", |
1988 |
|
"$table(from-link)", |
1989 |
|
"$table(subkey)", |
1990 |
|
"$table(value)" |
1991 |
|
]); |
1992 |
|
# Form the key from the real key and the sub key. |
1993 |
|
my $key = $self->JoinKey($realKey, $subKey); |
1994 |
|
# Break the value into sections. |
1995 |
|
my @sections = split($self->{splitter}, $valueString); |
1996 |
|
# Match each section against the incoming values. We'll assume we're |
1997 |
|
# okay unless we learn otherwise. |
1998 |
|
my $matching = 1; |
1999 |
|
for (my $i = 0; $i < $sectionCount && $matching; $i++) { |
2000 |
|
# We need to check to see if this section is generic. |
2001 |
|
my $value = $values[$i]; |
2002 |
|
Trace("Current value pattern is \"$value\".") if T(4); |
2003 |
|
if ($value =~ m#^/(.+)/[a-z]*$#) { |
2004 |
|
Trace("Regular expression detected.") if T(4); |
2005 |
|
# Here we have a regular expression match. |
2006 |
|
my $section = $sections[$i]; |
2007 |
|
$matching = eval("\$section =~ $value"); |
2008 |
|
} else { |
2009 |
|
# Here we have a normal match. |
2010 |
|
Trace("SQL match used.") if T(4); |
2011 |
|
$matching = _CheckSQLPattern($values[$i], $sections[$i]); |
2012 |
|
} |
2013 |
|
} |
2014 |
|
# If we match, output this row to the return list. |
2015 |
|
if ($matching) { |
2016 |
|
push @retVal, [$id, $key, @sections]; |
2017 |
|
} |
2018 |
|
} |
2019 |
|
# Return the rows found. |
2020 |
|
return @retVal; |
2021 |
|
} |
2022 |
|
|
2023 |
|
|
2024 |
|
=head3 _LoadAttributeTable |
2025 |
|
|
2026 |
|
$attr->_LoadAttributeTable($tableName, $fileName, $stats, $mode); |
2027 |
|
|
2028 |
|
Load a file's data into an attribute table. This is an internal method |
2029 |
|
provided for the convenience of L</LoadAttributesFrom>. It loads the |
2030 |
|
specified file into the specified table and updates the statistics |
2031 |
|
object. |
2032 |
|
|
2033 |
|
=over 4 |
2034 |
|
|
2035 |
|
=item tableName |
2036 |
|
|
2037 |
|
Name of the table being loaded. This is usually C<HasValueFor>, but may |
2038 |
|
be a different table for some specific attribute keys. |
2039 |
|
|
2040 |
|
=item fileName |
2041 |
|
|
2042 |
|
Name of the file containing a chunk of attribute data to load. |
2043 |
|
|
2044 |
|
=item stats |
2045 |
|
|
2046 |
|
Statistics object into which counts and times should be placed. |
2047 |
|
|
2048 |
|
=item mode |
2049 |
|
|
2050 |
|
Load mode for the file, usually C<low_priority>, C<concurrent>, or |
2051 |
|
an empty string. The mode is used by some applications to control access |
2052 |
|
to the table while it's being loaded. The default (empty string) is to lock the |
2053 |
|
table until all the data's in place. |
2054 |
|
|
2055 |
|
=back |
2056 |
|
|
2057 |
|
=cut |
2058 |
|
|
2059 |
|
sub _LoadAttributeTable { |
2060 |
|
# Get the parameters. |
2061 |
|
my ($self, $tableName, $fileName, $stats, $mode) = @_; |
2062 |
|
# Load the table from the file. Note that we don't do an analyze. |
2063 |
|
# The analyze is done only after everything is complete. |
2064 |
|
my $startTime = time(); |
2065 |
|
Trace("Loading attributes from $fileName: " . (-s $fileName) . |
2066 |
|
" characters.") if T(3); |
2067 |
|
my $loadStats = $self->LoadTable($fileName, $tableName, |
2068 |
|
mode => $mode, partial => 1); |
2069 |
|
# Record the load time. |
2070 |
|
$stats->Add(insertTime => time() - $startTime); |
2071 |
|
# Roll up the other statistics. |
2072 |
|
$stats->Accumulate($loadStats); |
2073 |
|
} |
2074 |
|
|
2075 |
|
|
2076 |
|
=head3 _GetAllTables |
2077 |
|
|
2078 |
|
my @tables = $ca->_GetAllTables(); |
2079 |
|
|
2080 |
|
Return a list of the names of all the tables used to store attribute |
2081 |
|
values. |
2082 |
|
|
2083 |
|
=cut |
2084 |
|
|
2085 |
|
sub _GetAllTables { |
2086 |
|
# Get the parameters. |
2087 |
|
my ($self) = @_; |
2088 |
|
# Start with the default table. |
2089 |
|
my @retVal = $self->{defaultRel}; |
2090 |
|
# Add the tables named in the key hash. These tables are automatically |
2091 |
|
# NOT the default, and each can only occur once, because alternate tables |
2092 |
|
# are allocated on a per-key basis. |
2093 |
|
my $keyHash = $self->_KeyTable(); |
2094 |
|
push @retVal, values %$keyHash; |
2095 |
|
# Return the result. |
2096 |
|
return @retVal; |
2097 |
|
} |
2098 |
|
|
2099 |
|
|
2100 |
|
=head3 _SplitKeyPattern |
2101 |
|
|
2102 |
|
my ($realKey, $subKey) = $ca->_SplitKeyPattern($keyChoice); |
2103 |
|
|
2104 |
|
Split a key pattern into the main part (the I<real key>) and a sub-part |
2105 |
|
(the I<sub key>). This method differs from L</SplitKey> in that it treats |
2106 |
|
the key as an SQL pattern instead of a raw string. Also, if there is no |
2107 |
|
incoming sub-part, the sub-key will be undefined instead of an empty |
2108 |
|
string. |
2109 |
|
|
2110 |
|
=over 4 |
2111 |
|
|
2112 |
|
=item keyChoice |
2113 |
|
|
2114 |
|
SQL key pattern to be examined. This can either be a literal, an SQL pattern, |
2115 |
|
a literal with an internal splitter code (usually C<::>) or an SQL pattern with |
2116 |
|
an internal splitter. Note that the only SQL pattern we support is a percent |
2117 |
|
sign (C<%>) at the end. This is the way we've declared things in the documentation, |
2118 |
|
so users who try anything else will have problems. |
2119 |
|
|
2120 |
|
=item RETURN |
2121 |
|
|
2122 |
|
Returns a two-element list. The first element is the SQL pattern for the |
2123 |
|
real key and the second is the SQL pattern for the sub-key. If the value |
2124 |
|
for either one does not matter (e.g., the user wants a real key value of |
2125 |
|
C<iedb> and doesn't care about the sub-key value), it will be undefined. |
2126 |
|
|
2127 |
|
=back |
2128 |
|
|
2129 |
|
=cut |
2130 |
|
|
2131 |
|
sub _SplitKeyPattern { |
2132 |
|
# Get the parameters. |
2133 |
|
my ($self, $keyChoice) = @_; |
2134 |
|
# Declare the return variables. |
2135 |
|
my ($realKey, $subKey); |
2136 |
|
# Look for a splitter in the input. |
2137 |
|
if ($keyChoice =~ /^(.*?)$self->{splitter}(.*)/) { |
2138 |
|
# We found one. This means we can treat both sides of the |
2139 |
|
# splitter as known patterns. |
2140 |
|
($realKey, $subKey) = ($1, $2); |
2141 |
|
} elsif ($keyChoice =~ /%$/) { |
2142 |
|
# Here we have a generic pattern for the whole key. The pattern |
2143 |
|
# is treated as the correct pattern for the real key, but the |
2144 |
|
# sub-key is considered to be wild. |
2145 |
|
$realKey = $keyChoice; |
2146 |
|
} else { |
2147 |
|
# Here we have a literal pattern for the whole key. The pattern |
2148 |
|
# is treated as the correct pattern for the real key, and the |
2149 |
|
# sub-key is required to be blank. |
2150 |
|
$realKey = $keyChoice; |
2151 |
|
$subKey = ''; |
2152 |
|
} |
2153 |
|
# Return the results. |
2154 |
|
return ($realKey, $subKey); |
2155 |
|
} |
2156 |
|
|
2157 |
|
|
2158 |
|
=head3 _WherePart |
2159 |
|
|
2160 |
|
my ($sqlClause, $escapedValue) = _WherePart($tableName, $fieldName, $sqlPattern); |
2161 |
|
|
2162 |
|
Return the SQL clause and value for checking a field against the |
2163 |
|
specified SQL pattern value. If the pattern is generic (ends in a C<%>), |
2164 |
|
then a C<LIKE> expression is returned. Otherwise, an equality expression |
2165 |
|
is returned. We take in information describing the field being checked, |
2166 |
|
and the pattern we're checking against it. The output is a WHERE clause |
2167 |
|
fragment for the comparison and a value to be used as a bound parameter |
2168 |
|
value for the clause. |
2169 |
|
|
2170 |
|
=over 4 |
2171 |
|
|
2172 |
|
=item tableName |
2173 |
|
|
2174 |
|
Name of the table containing the field we want checked by the clause. |
2175 |
|
|
2176 |
|
=item fieldName |
2177 |
|
|
2178 |
|
Name of the field to check in that table. |
2179 |
|
|
2180 |
|
=item sqlPattern |
2181 |
|
|
2182 |
|
Pattern to be compared against the field. If the last character is a percent sign |
2183 |
|
(C<%>), it will be treated as a generic SQL pattern; otherwise, it will be treated |
2184 |
|
as a literal. |
2185 |
|
|
2186 |
|
=item RETURN |
2187 |
|
|
2188 |
|
Returns a two-element list. The first element will be an SQL comparison expression |
2189 |
|
and the second will be the value to be used as a bound parameter for the expression |
2190 |
|
in order to |
2191 |
|
|
2192 |
|
=back |
2193 |
|
|
2194 |
|
=cut |
2195 |
|
|
2196 |
|
sub _WherePart { |
2197 |
|
# Get the parameters. |
2198 |
|
my ($tableName, $fieldName, $sqlPattern) = @_; |
2199 |
|
# Declare the return variables. |
2200 |
|
my ($sqlClause, $escapedValue); |
2201 |
|
# Copy the pattern into the return area. |
2202 |
|
$escapedValue = $sqlPattern; |
2203 |
|
# Check the pattern. Is it generic or exact? |
2204 |
|
if ($sqlPattern =~ /%$/) { |
2205 |
|
# Yes, it is. We need a LIKE clause and we must escape the underscores |
2206 |
|
# and percents in the pattern. |
2207 |
|
$escapedValue =~ s/(%|_)/\\$1/g; |
2208 |
|
$sqlClause = "$tableName($fieldName) LIKE ?"; |
2209 |
|
} else { |
2210 |
|
# No, it isn't. We use an equality clause. |
2211 |
|
$sqlClause = "$tableName($fieldName) = ?"; |
2212 |
|
} |
2213 |
|
# Return the results. |
2214 |
|
return ($sqlClause, $escapedValue); |
2215 |
|
} |
2216 |
|
|
2217 |
|
|
2218 |
|
=head3 _CheckSQLPattern |
2219 |
|
|
2220 |
|
my $flag = _CheckSQLPattern($pattern, $value); |
2221 |
|
|
2222 |
|
Return TRUE if the specified SQL pattern matches the specified value, |
2223 |
|
else FALSE. The pattern is not a true full-blown SQL LIKE pattern: the |
2224 |
|
only wild-carding allowed is a percent sign (C<%>) at the end. |
2225 |
|
|
2226 |
|
=over 4 |
2227 |
|
|
2228 |
|
=item pattern |
2229 |
|
|
2230 |
|
SQL pattern to match against a value. |
2231 |
|
|
2232 |
|
=item value |
2233 |
|
|
2234 |
|
Value to match against an SQL pattern. |
2235 |
|
|
2236 |
|
=item RETURN |
2237 |
|
|
2238 |
|
Returns TRUE if the pattern matches the value, else FALSE. |
2239 |
|
|
2240 |
|
=back |
2241 |
|
|
2242 |
|
=cut |
2243 |
|
|
2244 |
|
sub _CheckSQLPattern { |
2245 |
|
# Get the parameters. |
2246 |
|
my ($pattern, $value) = @_; |
2247 |
|
# Declare the return variable. |
2248 |
|
my $retVal; |
2249 |
|
# Check for a generic pattern. |
2250 |
|
if ($pattern =~ /(.*)%$/) { |
2251 |
|
# Here we have one. Do a substring match. |
2252 |
|
$retVal = (substr($value, 0, length $1) eq $1); |
2253 |
|
} else { |
2254 |
|
# Here it's an exact match. |
2255 |
|
$retVal = ($pattern eq $value); |
2256 |
|
} |
2257 |
|
# Return the result. |
2258 |
|
return $retVal; |
2259 |
|
} |
2260 |
|
|
2261 |
1; |
1; |