From 18a52b9aa2e671e23f162770c0250b8fa9c6a4a9 Mon Sep 17 00:00:00 2001 From: Colin Kuskie Date: Wed, 26 Aug 2009 15:42:18 +0000 Subject: [PATCH] Refactor out the JSON collateral code from FilePump/Bundle.pm to make an aspect that can be used by assets, operations and any other modules in WebGUI. --- docs/changelog/7.x.x.txt | 1 + lib/WebGUI/Asset.pm | 7 +- lib/WebGUI/AssetVersioning.pm | 2 +- lib/WebGUI/JSONCollateral.pm | 297 +++++++++++++++++++ t/Asset/JSONCollateral.t | 334 ++++++++++++++++++++++ t/lib/WebGUI/Asset/JSONCollateralDummy.pm | 79 +++++ 6 files changed, 718 insertions(+), 2 deletions(-) create mode 100644 lib/WebGUI/JSONCollateral.pm create mode 100644 t/Asset/JSONCollateral.t create mode 100644 t/lib/WebGUI/Asset/JSONCollateralDummy.pm diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 24b6c0d2d..e733c03f7 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -1,4 +1,5 @@ 7.7.19 + - refactored out JSON collateral module, to use with any module in WebGUI. - fixed #10825: Text and MediumText fields should be LongText - fixed #10808: Ask user about using rich edit option causes error in IE6 - fixed #10822: spectre pid sucks diff --git a/lib/WebGUI/Asset.pm b/lib/WebGUI/Asset.pm index d218cb1b2..03426414e 100644 --- a/lib/WebGUI/Asset.pm +++ b/lib/WebGUI/Asset.pm @@ -2495,8 +2495,13 @@ sub update { } # set the property + if ($propertyDefinition->{serialize}) { + $setPairs{$property} = JSON->new->canonical->encode($value); + } + else { + $setPairs{$property} = $value; + } $self->{_properties}{$property} = $value; - $setPairs{$property} = $value; } # if there's anything to update, then do so diff --git a/lib/WebGUI/AssetVersioning.pm b/lib/WebGUI/AssetVersioning.pm index 219edb789..5db3c4919 100644 --- a/lib/WebGUI/AssetVersioning.pm +++ b/lib/WebGUI/AssetVersioning.pm @@ -126,7 +126,7 @@ sub addRevision { # get the default values of each property foreach my $property (keys %{$definition->{properties}}) { $defaults{$property} = $definition->{properties}{$property}{defaultValue}; - if (ref($defaults{$property}) eq 'ARRAY') { + if (ref($defaults{$property}) eq 'ARRAY' && !$definition->{properties}{$property}{serialize}) { $defaults{$property} = $defaults{$property}->[0]; } } diff --git a/lib/WebGUI/JSONCollateral.pm b/lib/WebGUI/JSONCollateral.pm new file mode 100644 index 000000000..e252777d2 --- /dev/null +++ b/lib/WebGUI/JSONCollateral.pm @@ -0,0 +1,297 @@ +package WebGUI::JSONCollateral; + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2009 Plain Black Corporation. + ------------------------------------------------------------------- + Please read the legal notices (docs/legal.txt) and the license + (docs/license.txt) that came with this distribution before using + this software. + ------------------------------------------------------------------- + http://www.plainblack.com info@plainblack.com + ------------------------------------------------------------------- + +=cut + +use strict; +use Class::C3; + +=head1 NAME + +Package WebGUI::JSONCollateral + +=head1 DESCRIPTION + +This is an aspect which allows you to use JSON in the database transparently. + +=head1 SYNOPSIS + + use Class::C3; + use base qw(WebGUI::JSONCollateral); + + $self->setJSONCollateral(); + $self->getJSONCollateral(); + $self->moveJSONCollateralUp(); + $self->moveJSONCollateralDown(); + +And then wherever you would call $self->SUPER::someMethodName call $self->next::method instead. + +Classes that use this Aspect must have an update method that transparently serializes and deserializes data +to and from JSON into perl data structures. See WebGUI::Crud->update, and Asset->update for examples. + +=head1 METHODS + +These methods are available from this class: + +=cut + +#------------------------------------------------------------------- + +=head2 deleteJSONCollateral ( fieldName, keyName, keyValue ) + +Deletes a row of collateral data. Returns false if the requested collateral +was not deleted. + +=head3 fieldName + +The name of the field you wish to delete the data from. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to delete. + +=cut + +sub deleteJSONCollateral { + my $self = shift; + my $fieldName = shift; + my $keyName = shift; + my $keyValue = shift; + my $field = $self->get($fieldName); + my $index = $self->getJSONCollateralDataIndex($field, $keyName, $keyValue); + return if $index == -1; + splice @{ $field }, $index, 1; + $self->update({ $fieldName => $field }); + return 1; +} + +#------------------------------------------------------------------- + +=head2 getJSONCollateral ( fieldName, keyName, keyValue ) + +Returns a hash reference containing one row of collateral data from a particular +field. + +=head3 fieldName + +The name of the field you wish to retrieve the data from. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to get. +If this is equal to "new", then an empty hashRef will be returned to avoid +strict errors in the caller. If the requested data does not exist in the +collateral array, it also returns an empty hashRef. + +=cut + +sub getJSONCollateral { + my $self = shift; + my $fieldName = shift; + my $keyName = shift; + my $keyValue = shift; + if ($keyValue eq "new" || $keyValue eq "") { + return {} + } + my $field = $self->get($fieldName); + my $index = $self->getJSONCollateralDataIndex($field, $keyName, $keyValue); + return {} if $index == -1; + my %copy = %{ $field->[$index] }; + return \%copy; +} + + +#------------------------------------------------------------------- + +=head2 getJSONCollateralDataIndex ( field, keyName, keyValue ) + +Returns the index in a set of collateral where an element of the +data (keyName) has a certain value (keyValue). If the criteria +are not found, returns -1. + +=head3 field + +The collateral data to search + +=head3 keyName + +The name of a key in the collateral hash. + +=head3 keyValue + +The value that keyName should have to meet the criteria. + +=cut + +sub getJSONCollateralDataIndex { + my $self = shift; + my $field = shift; + my $keyName = shift; + my $keyValue = shift; + for (my $index=0; $index <= $#{ $field }; $index++) { + return $index + if (exists($field->[$index]->{$keyName}) && ($field->[$index]->{$keyName} eq $keyValue )); + } + return -1; +} + +#------------------------------------------------------------------- + +=head2 moveJSONCollateralDown ( fieldName, keyName, keyValue ) + +Moves a collateral data item down one position, toward the end of the array where the +indices are the highest, swapping the referenced piece of collateral (index) with the collateral +just above it (index+1). For the list of collateral 1,2,3, if called on 2 the resultig +list will be 1,3,2. If called on the last element of the collateral array then it does nothing. + +Returns 1 if the move is successful. Returns undef or the empty array otherwise. + +=head3 fieldName + +A string indicating the field that contains the collateral data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to move. + +=cut + +sub moveJSONCollateralDown { + my $self = shift; + my $fieldName = shift; + my $keyName = shift; + my $keyValue = shift; + + my $field = $self->get($fieldName); + my $index = $self->getJSONCollateralDataIndex($field, $keyName, $keyValue); + return if $index == -1; + return unless (abs($index) < $#{$field}); + @{ $field }[$index,$index+1] = @{ $field }[$index+1,$index]; + $self->update({ $fieldName => $field }); + return 1; +} + +#------------------------------------------------------------------- + +=head2 moveJSONCollateralUp ( fieldName, keyName, keyValue ) + +Moves a collateral data item "up" one position, toward the end of the array where the +indices are the lowest, swapping the referenced piece of collateral (index) with the collateral +just below it (index-1). For the list of collateral 1,2,3, if called on 2 the resultig +list will be 2,1,3. If called on the first element of the collateral array then it does nothing. + +Returns 1 if the move is successful. Returns undef or the empty array otherwise. + +=head3 fieldName + +A string indicating the field that contains the collateral data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to move. + +=cut + +sub moveJSONCollateralUp { + my $self = shift; + my $fieldName = shift; + my $keyName = shift; + my $keyValue = shift; + + my $field = $self->get($fieldName); + my $index = $self->getJSONCollateralDataIndex($field, $keyName, $keyValue); + return unless $index > 0; #-1 means that it could not be found, and we cannot move index 0 + @{ $field }[$index-1,$index] = @{ $field }[$index,$index-1]; + $self->update({ $fieldName => $field }); + return 1; +} + +#----------------------------------------------------------------- + +=head2 setJSONCollateral ( fieldName, keyName, keyValue, properties ) + +Performs and insert/update of collateral data for any wobject's collateral data. +Returns the id of the data that was set, even if a new row was added to the +data. + +=head3 fieldName + +The name of the field to insert the data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to set. +The index of the collateral data to set. If the keyValue = "new", then a +new entry will be appended to the end of the collateral array. Otherwise, +the appropriate entry will be overwritten with the new data. + +=head3 properties + +A hash reference containing the name/value pairs to be inserted into the collateral, using +the criteria mentioned above. + +=cut + +sub setJSONCollateral { + my $self = shift; + my $fieldName = shift; + my $keyName = shift; + my $keyValue = shift; + my $properties = shift; + ##Note, since this returns a reference, it is actually updating + ##the object cache directly. + my $field = $self->get($fieldName); + if ($keyValue eq 'new' || $keyValue eq '') { + if ( ! exists $properties->{$keyName} + or ! $self->session->id->valid($properties->{$keyName})) { + $properties->{$keyName} = $self->session->id->generate; + } + push @{ $field }, $properties; + $self->update({$fieldName => $field}); + return $properties->{$keyName}; + } + my $index = $self->getJSONCollateralDataIndex($field, $keyName, $keyValue); + return if $index == -1; + $field->[$index] = $properties; + $self->update({ $fieldName => $field }); + return $keyValue; +} + + +1; diff --git a/t/Asset/JSONCollateral.t b/t/Asset/JSONCollateral.t new file mode 100644 index 000000000..e3f235be7 --- /dev/null +++ b/t/Asset/JSONCollateral.t @@ -0,0 +1,334 @@ +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 Plain Black Corporation. +#------------------------------------------------------------------- +# Please read the legal notices (docs/legal.txt) and the license +# (docs/license.txt) that came with this distribution before using +# this software. +#------------------------------------------------------------------- +# http://www.plainblack.com info@plainblack.com +#------------------------------------------------------------------- + +use FindBin; +use strict; +use lib "$FindBin::Bin/../lib"; + +use WebGUI::Test; +use WebGUI::Session; +use WebGUI::Asset; +use WebGUI::AssetVersioning; +use WebGUI::VersionTag; +use WebGUI::Asset::JSONCollateralDummy; + +use Test::More; +use Test::Deep; +use Data::Dumper; + +my $session = WebGUI::Test->session; + +$session->db->write(<db->write(< 39; + +my $asset = WebGUI::Asset->getDefault($session)->addChild({ + className => 'WebGUI::Asset::JSONCollateralDummy', + title => 'JSON Collateral Test Asset', +}); + +my $tag = WebGUI::VersionTag->getWorking($session); +$tag->commit; + +################################################################ +# +# Checking Asset serialization +# +################################################################ + +isa_ok($asset, 'WebGUI::Asset::JSONCollateralDummy'); +cmp_deeply( + $asset->get('jsonField'), + [], + 'jsonField set to empty arrayref initially' +); + +is( + $session->db->quickScalar(q|select jsonField from jsonCollateralDummy where assetId=? and revisionDate=?|, [$asset->getId, $asset->get('revisionDate')]), + '[]', + 'bare JSON arrayref stored in the db' +); + +$asset->update({ + jsonField => [ { alpha => "aye", beta => "bee", }, ], +}); + +is( + $session->db->quickScalar(q|select jsonField from jsonCollateralDummy where assetId=? and revisionDate=?|, [$asset->getId, $asset->get('revisionDate')]), + '[{"alpha":"aye","beta":"bee"}]', + 'JSON updated in the db' +); + +cmp_deeply( + $asset->get('jsonField'), + [ { alpha => "aye", beta => "bee"} ], + 'get returns a hash ref with data in it' +); + +$asset->update({ + jsonField => [ ], +}); + +################################################################ +# +# setJSONCollateral, getJSONCollateral +# +################################################################ + +my $key1 = $asset->setJSONCollateral('jsonField', 'jsonId', 'new', { first => "one", second => "two", }); +ok($session->id->valid($key1), 'setJSONCollateral: returns a valid guid, new, no key in collateral'); + +my $key2 = $asset->setJSONCollateral('jsonField', 'jsonId', '', { first => "uno", second => "dos", }); +ok($session->id->valid($key2), '... returns a valid guid, empty key, no key in collateral'); +isnt($key2, $key1, '... returns unique guids each time. Generates GUID if guid is ""'); + +my $key3 = $session->id->generate(); + +my $returnedKey; +$returnedKey = $asset->setJSONCollateral('jsonField', 'jsonId', 'new', { first => 'Aye', second => 'Bay', jsonId => $key3}); +is($returnedKey, $key3, '... created collateral with set GUID'); + +$returnedKey = $asset->setJSONCollateral('jsonField', 'jsonId', 'new', { first => 'Aye', second => 'Bay', jsonId => 'notAGUID'}); +isnt($returnedKey, 'notAGUID', '... created valid GUID when passed a new one'); + +my $collateral; +$collateral = $asset->getJSONCollateral('jsonField', 'jsonId', 'notAGUID'); +cmp_deeply( $collateral, {}, 'getJSONCollateral returns empty hashref for a non-existant id'); + +$collateral = $asset->getJSONCollateral('jsonField', 'jsonId', "new"); +cmp_deeply( $collateral, {}, '... returns empty hashref for id=new'); + +$collateral = $asset->getJSONCollateral('jsonField', 'jsonId', ""); +cmp_deeply( $collateral, {}, '... returns empty hashref for id=""'); + +################################################################ +# +# Setup for move, delete tests. +# +################################################################ + +$asset->update({ + jsonField => [ ], +}); + +my $guid1 = $asset->setJSONCollateral('jsonField', 'jsonId', 'new', { first => 'alpha', second => 'beta'}); +my $guid2 = $asset->setJSONCollateral('jsonField', 'jsonId', '', { first => 'aee', second => 'bee'}); + +cmp_deeply( + $asset->get('jsonField'), + [ + { first => 'alpha', second => 'beta', jsonId => $guid1, }, + { first => 'aee', second => 'bee', jsonId => $guid2, }, + ], + '...checking collateral content, deeply' +); + +my $guid3 = $asset->setJSONCollateral('jsonField', 'jsonId', 'new', { first => 'Aye', second => 'Bay', }); + +my $retVal; +$retVal = $asset->setJSONCollateral('jsonField', 'jsonId', $guid3, { first => 'ahh', second => 'bay', jsonId => $guid3, }); + +is($retVal, $guid3, 'setJSONCollateral returns GUID when it modifies existing collateral'); +cmp_deeply( + $asset->getJSONCollateral('jsonField', 'jsonId', $guid3), + { first => 'ahh', second => 'bay', jsonId => $guid3 }, + '... collateral updated' +); + +$retVal = $asset->setJSONCollateral('jsonField', 'jsonId', scalar reverse $guid3, { first => 'ook', second => 'eek'}); +ok(!$retVal, '... returns false when it fails'); +cmp_deeply( + $asset->getJSONCollateral('jsonField', 'jsonId', $guid3), + { first => 'ahh', second => 'bay', jsonId => $guid3 }, + '... collateral not updated' +); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid1, + $guid2, + $guid3, + ], + '...checking collateral order, ready for moving collateral' +); + +################################################################ +# +# getJSONCollateralDataIndex +# +################################################################ + +is( + $asset->getJSONCollateralDataIndex($asset->get('jsonField'), 'jsonId', $guid1), + 0, + 'getJSONCollateralDataIndex: guid1 in the correct position' +); + +is( + $asset->getJSONCollateralDataIndex($asset->get('jsonField'), 'jsonId', scalar reverse($guid1)), + -1, + '... returns -1 when it cannot be found' +); + +is( + $asset->getJSONCollateralDataIndex($asset->get('jsonField'), 'JSONID', $guid1), + -1, + '... returns -1 when it cannot be found' +); + +################################################################ +# +# moveJSONCollateralDown +# +################################################################ + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid1, + $guid2, + $guid3, + ], + '...checking collateral order, ready for moving collateral' +); + +$retVal = $asset->moveJSONCollateralDown('jsonField', 'jsonId', $guid3); +ok(!$retVal, 'moveJSONCollateralDown returned false'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid1, + $guid2, + $guid3, + ], + '...order did not change. Cannot move last one down' +); +$retVal = $asset->moveJSONCollateralDown('jsonField', 'jsonId', scalar reverse $guid3); +ok(!$retVal, '... returned false again (nonexistant guid)'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid1, + $guid2, + $guid3, + ], + '...order did not change. Cannot move an entry that cannot be found' +); + +$retVal = $asset->moveJSONCollateralDown('jsonField', 'jsonId', $guid1); +ok($retVal, '... returned true'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid1, + $guid3, + ], + '...order changed' +); + +################################################################ +# +# moveJSONCollateralUp +# +################################################################ + +$retVal = $asset->moveJSONCollateralUp('jsonField', 'jsonId', $guid2); +ok(!$retVal, 'moveJSONCollateralUp returned false'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid1, + $guid3, + ], + '...order did not change. Cannot move first one up' +); +$retVal = $asset->moveJSONCollateralUp('jsonField', 'jsonId', scalar reverse $guid3); +ok(!$retVal, '... returned false again (nonexistant guid)'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid1, + $guid3, + ], + '...order did not change. Cannot move an entry that cannot be found' +); + +$retVal = $asset->moveJSONCollateralUp('jsonField', 'jsonId', $guid3); +ok($retVal, '... returned true'); + +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid3, + $guid1, + ], + '...order changed' +); + +################################################################ +# +# deleteJSONCollateral +# +################################################################ + +$retVal = $asset->deleteJSONCollateral('jsonField', 'jsonId', scalar reverse $guid3); +ok(!$retVal, 'deleteJSONCollateral returns false with an invalid id'); +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid3, + $guid1, + ], + '...nothing was deleted' +); + +$retVal = $asset->deleteJSONCollateral('jsonField', 'jsonId', $guid3); +ok($retVal, '... delete was successful'); +cmp_deeply( + [ map { $_->{jsonId} } @{ $asset->get('jsonField') } ], + [ + $guid2, + $guid1, + ], + '...collateral was removed' +); + +diag $asset->getId; + + +$tag->rollback; +$asset->purge; + +$session->db->write(< { + label => 'jsonField', + hoverHelp => 'Not really needed, it is for internal data in this test case', + fieldType => 'textarea', + serialize => 1, + defaultValue => [], + noFormPost => 1, + }, + ); + push(@{$definition}, { + assetName=>'JSON Collateral Dummy', + tableName=>'jsonCollateralDummy', + autoGenerateForms=>1, + className=>'WebGUI::Asset::JSONCollateralDummy', + icon=>'assets.gif', + properties=>\%properties + } + ); + return $class->next::method($session, $definition); +} + +1; + +#vim:ft=perl