Update ContentCafe Added Content Module
authorThomas Berezansky <tsbere@mvlc.org>
Mon, 14 Jul 2014 16:13:06 +0000 (12:13 -0400)
committerBen Shum <bshum@biblio.org>
Fri, 8 Aug 2014 01:51:58 +0000 (21:51 -0400)
Now with keyhash support, but losing "No Image" image support.

Also added a "fetch all" function for possible "cache everything" calls.

Signed-off-by: Thomas Berezansky <tsbere@mvlc.org>
Signed-off-by: Ben Shum <bshum@biblio.org>
Open-ILS/examples/opensrf.xml.example
Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent/ContentCafe.pm
docs/RELEASE_NOTES_NEXT/OPAC/addedcontent_contentcafe.txt [new file with mode: 0644]

index cc7df8a..85adf44 100644 (file)
@@ -256,11 +256,12 @@ vim:et:ts=4:sw=4:
                 <password>MY_PASSWORD</password>
 
                 <!--
-                If no cover/jacket image exists for a given ISBN, then a value of T here will
-                return an 80x120 pixel image containing the text "No Image Available".  A
-                value of 1 will return a 1x1 pixel image.
+                Which order to put identifiers in.
+                Default is "isbn,upc", ignoring currently unsupported issn.
+                Should be all lowercase.
+                Remove an identifier from the list to skip it.
                 -->
-                <return_behavior_on_no_jacket_image>T</return_behavior_on_no_jacket_image>
+                <identifier_order>isbn,upc</identifier_order>
             </ContentCafe>
 
             <!--
index e2adf6d..cd24243 100644 (file)
@@ -277,6 +277,20 @@ sub get_url {
     return $res;
 }
 
+# returns an HTPP::Response object
+sub post_url {
+    my( $self, $url, $content ) = @_;
+
+    $logger->info("added content getting [timeout=$net_timeout, errors_remaining=$error_countdown] URL = $url");
+    my $agent = LWP::UserAgent->new(timeout => $net_timeout);
+
+    my $res = $agent->post($url, Content => $content);
+    $logger->info("added content request returned with code " . $res->code);
+    die "added content request failed: " . $res->status_line ."\n" unless $res->is_success;
+
+    return $res;
+}
+
 sub lookups_enabled {
     if( $cache->get_cache('ac.no_lookup') ) {
         $logger->info("added content lookup disabled");
index 9ecaebe..4dd5b0c 100644 (file)
@@ -7,11 +7,11 @@ use OpenSRF::EX qw/:try/;
 use OpenILS::WWW::AddedContent;
 use XML::LibXML;
 use MIME::Base64;
+use DateTime;
 
 my $AC = 'OpenILS::WWW::AddedContent';
 
-my $base_url = 'http://contentcafe2.btol.com/ContentCafe/ContentCafe.asmx/Single';
-my $cover_base_url = 'http://contentcafe2.btol.com/ContentCafe/Jacket.aspx';
+my $post_url = 'http://contentcafe2.btol.com/ContentCafe/ContentCafe.asmx/XmlPost';
 
 sub new {
     my( $class, $args ) = @_;
@@ -29,39 +29,93 @@ sub password {
     return $self->{ContentCafe}->{password};
 }
 
-sub return_behavior_on_no_jacket_image {
+sub identifier_order {
     my $self = shift;
-    return $self->{ContentCafe}->{return_behavior_on_no_jacket_image};
+    if ($self->{ContentCafe}->{identifier_order}) {
+        my $order = [ split(',',$self->{ContentCafe}->{identifier_order}) ];
+        return $order;
+    }
+    return ['isbn','upc'];
+}
+
+sub expects_keyhash {
+    # we expect a keyhash as opposed to a simple scalar containing an ISBN
+    return 1;
+}
+
+# --------------------------------------------------------------------------
+
+# This function fetches everything and returns:
+#     0 if we had no usable keys
+#     0 if we had a lookup failure
+#     A hash of format_type => result if you called that directly
+sub fetch_all {
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc([
+        'TocDetail', # toc_*
+        'BiographyDetail', #anotes_*
+        'ExcerptDetail', #excerpt_*
+        'ReviewDetail', #reviews_*
+        'AnnotationDetail', #summary_*
+        {name => 'JacketDetail', attributes => [['Type','S'],['Encoding','HEX']]}, # jacket_small
+        {name => 'JacketDetail', attributes => [['Type','M'],['Encoding','HEX']]}, # jacket_medium
+        {name => 'JacketDetail', attributes => [['Type','L'],['Encoding','HEX']]}, # jacket_large
+    ], $keyhash);
+    return 0 unless defined $doc;
+    my $resulthash = {
+        toc_html        => $self->parse_toc_html($doc),
+        toc_json        => $self->send_json($doc, 'TocItems'),
+        toc_xml         => $self->send_xml($doc, 'TocItems'),
+        anotes_html     => $self->parse_anotes_html($doc),
+        anotes_json     => $self->send_json($doc, 'BiographyItems'),
+        anotes_xml      => $self->send_xml($doc, 'BiographyItems'),
+        excerpt_html    => $self->parse_excerpt_html($doc),
+        excerpt_json    => $self->send_json($doc, 'ExcerptItems'),
+        excerpt_xml     => $self->send_xml($doc, 'ExcerptItems'),
+        reviews_html    => $self->parse_reviews_html($doc),
+        reviews_json    => $self->send_json($doc, 'ReviewItems'),
+        reviews_xml     => $self->send_xml($doc, 'ReviewItems'),
+        summary_html    => $self->parse_summary_html($doc),
+        summary_json    => $self->send_json($doc, 'AnnotationItems'),
+        summary_xml     => $self->send_xml($doc, 'AnnotationItems'),
+        jacket_small    => $self->parse_jacket($doc, 'S'),
+        jacket_medium   => $self->parse_jacket($doc, 'M'),
+        jacket_large    => $self->parse_jacket($doc, 'L')
+    };
+    return $resulthash;
 }
 
 # --------------------------------------------------------------------------
 sub jacket_small {
-    my( $self, $key ) = @_;
-    return $self->send_img(
-        $self->fetch_cover_response('S', $key));
+    my( $self, $keyhash ) = @_;
+    return $self->send_jacket( $keyhash, 'S' );
 }
 
 sub jacket_medium {
-    my( $self, $key ) = @_;
-    return $self->send_img(
-        $self->fetch_cover_response('M', $key));
-
+    my( $self, $keyhash ) = @_;
+    return $self->send_jacket( $keyhash, 'M' );
 }
+
 sub jacket_large {
-    my( $self, $key ) = @_;
-    return $self->send_img(
-        $self->fetch_cover_response('L', $key));
+    my( $self, $keyhash ) = @_;
+    return $self->send_jacket( $keyhash, 'L' );
 }
 
 # --------------------------------------------------------------------------
 
 sub toc_html {
-    my( $self, $key ) = @_;
-    my $xml = $self->fetch_content('TocDetail', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc('TocDetail', $keyhash);
+    return 0 unless defined $doc;
+    return $self->parse_toc_html($doc);
+}
+
+sub parse_toc_html {
+    my( $self, $doc ) = @_;
     my $html = '';
-    my @nodes = $doc->findnodes('//cc:Toc');
+    my @nodes = $doc->findnodes('//cc:TocItems[*]');
+    return 0 if (scalar(@nodes) < 1);
+    @nodes = $nodes[0]->findnodes('.//cc:Toc');
     return 0 if (scalar(@nodes) < 1);
     foreach my $node ( @nodes ) {
         $html .= $node->textContent . '</P></P>';
@@ -70,26 +124,34 @@ sub toc_html {
 }
 
 sub toc_xml {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_xml(
-        $self->fetch_content('TocDetail', $key));
+        $self->fetch_xmldoc('TocDetail', $keyhash),
+        'TocItems');
 }
 
 sub toc_json {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_json(
-        $self->fetch_content('TocDetail', $key));
+        $self->fetch_xmldoc('TocDetail', $keyhash),
+        'TocItems');
 }
 
 # --------------------------------------------------------------------------
 
 sub anotes_html {
-    my( $self, $key ) = @_;
-    my $xml = $self->fetch_content('BiographyDetail', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc('BiographyDetail', $keyhash);
+    return 0 unless defined $doc;
+    return $self->parse_anotes_html($doc);
+}
+
+sub parse_anotes_html {
+    my( $self, $doc ) = @_;
     my $html = '';
-    my @nodes = $doc->findnodes('//cc:Biography');
+    my @nodes = $doc->findnodes('//cc:BiographyItems[*]');
+    return 0 if (scalar(@nodes) < 1);
+    @nodes = $nodes[0]->findnodes('.//cc:Biography');
     return 0 if (scalar(@nodes) < 1);
     foreach my $node ( @nodes ) {
         $html .= '<P class="biography">' . $node->textContent . '</P>';
@@ -98,27 +160,35 @@ sub anotes_html {
 }
 
 sub anotes_xml {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_xml(
-        $self->fetch_content('BiographyDetail', $key));
+        $self->fetch_xmldoc('BiographyDetail', $keyhash),
+        'BiographyItems');
 }
 
 sub anotes_json {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_json(
-        $self->fetch_content('BiographyDetail', $key));
+        $self->fetch_xmldoc('BiographyDetail', $keyhash),
+        'BiographyItems');
 }
 
 
 # --------------------------------------------------------------------------
 
 sub excerpt_html {
-    my( $self, $key ) = @_;
-    my $xml = $self->fetch_content('ExcerptDetail', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc('ExcerptDetail', $keyhash);
+    return 0 unless defined $doc;
+    return $self->parse_excerpt_html($doc);
+}
+
+sub parse_excerpt_html {
+    my( $self, $doc ) = @_;
     my $html = '';
-    my @nodes = $doc->findnodes('//cc:Excerpt');
+    my @nodes = $doc->findnodes('//cc:ExcerptItems[*]');
+    return 0 if (scalar(@nodes) < 1);
+    @nodes = $nodes[0]->findnodes('.//cc:Excerpt');
     return 0 if (scalar(@nodes) < 1);
     foreach my $node ( @nodes ) {
         $html .= $node->textContent;
@@ -127,26 +197,34 @@ sub excerpt_html {
 }
 
 sub excerpt_xml {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_xml(
-        $self->fetch_content('ExcerptDetail', $key));
+        $self->fetch_xmldoc('ExcerptDetail', $keyhash),
+        'ExcerptItems');
 }
 
 sub excerpt_json {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_json(
-        $self->fetch_content('ExcerptDetail', $key));
+        $self->fetch_xmldoc('ExcerptDetail', $keyhash),
+        'ExcerptItems');
 }
 
 # --------------------------------------------------------------------------
 
 sub reviews_html {
-    my( $self, $key ) = @_;
-    my $xml = $self->fetch_content('ReviewDetail', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc('ReviewDetail', $keyhash);
+    return 0 unless defined $doc;
+    return $self->parse_reviews_html($doc);
+}
+
+sub parse_reviews_html {
+    my( $self, $doc ) = @_;
     my $html = '<ul>';
-    my @nodes = $doc->findnodes('//cc:ReviewItem');
+    my @nodes = $doc->findnodes('//cc:ReviewItems[*]');
+    return 0 if (scalar(@nodes) < 1);
+    @nodes = $nodes[0]->findnodes('.//cc:ReviewItem');
     return 0 if (scalar(@nodes) < 1);
     foreach my $node ( @nodes ) {
         my @s_nodes = $node->findnodes('./cc:Supplier');
@@ -163,26 +241,34 @@ sub reviews_html {
 }
 
 sub reviews_xml {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_xml(
-        $self->fetch_content('ReviewDetail', $key));
+        $self->fetch_xmldoc('ReviewDetail', $keyhash),
+        'ReviewItems');
 }
 
 sub reviews_json {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_json(
-        $self->fetch_content('ReviewDetail', $key));
+        $self->fetch_xmldoc('ReviewDetail', $keyhash),
+        'ReviewItems');
 }
 
 # --------------------------------------------------------------------------
 
 sub summary_html {
-    my( $self, $key ) = @_;
-    my $xml = $self->fetch_content('AnnotationDetail', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    my( $self, $keyhash ) = @_;
+    my $doc = $self->fetch_xmldoc('AnnotationDetail', $keyhash);
+    return 0 unless defined $doc;
+    return $self->parse_summary_html($doc);
+}
+
+sub parse_summary_html {
+    my( $self, $doc ) = @_;
     my $html = '<ul>';
-    my @nodes = $doc->findnodes('//cc:AnnotationItem');
+    my @nodes = $doc->findnodes('//cc:AnnotationItems[*]');
+    return 0 if (scalar(@nodes) < 1);
+    @nodes = $nodes[0]->findnodes('.//cc:AnnotationItem');
     return 0 if (scalar(@nodes) < 1);
     foreach my $node ( @nodes ) {
         my @s_nodes = $node->findnodes('./cc:Supplier');
@@ -195,77 +281,56 @@ sub summary_html {
 }
 
 sub summary_xml {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_xml(
-        $self->fetch_content('AnnotationDetail', $key));
+        $self->fetch_xmldoc('AnnotationDetail', $keyhash),
+        'AnnotationItems');
 }
 
 sub summary_json {
-    my( $self, $key ) = @_;
+    my( $self, $keyhash ) = @_;
     return $self->send_json(
-        $self->fetch_content('AnnotationDetail', $key));
-}
-
-sub available_json {
-    my($self, $key) = @_;
-    my $xml = $self->fetch_content('AvailableContent', $key);
-    my $doc = XML::LibXML->new->parse_string($xml);
-
-    my @avail;
-    for my $node ($doc->findnodes('//*[text()="true"]')) {
-        push(@avail, 'summary') if $node->nodeName eq 'Annotation';
-        push(@avail, 'jacket') if $node->nodeName eq 'Jacket';
-        push(@avail, 'toc') if $node->nodeName eq 'TOC';
-        push(@avail, 'anotes') if $node->nodeName eq 'Biography';
-        push(@avail, 'excerpt') if $node->nodeName eq 'Excerpt';
-        push(@avail, 'reviews') if $node->nodeName eq 'Review';
-    }
-
-    return { 
-        content_type => 'text/plain', 
-        content => OpenSRF::Utils::JSON->perl2JSON(\@avail)
-    };
+        $self->fetch_xmldoc('AnnotationDetail', $keyhash),
+        'AnnotationItems');
 }
 
-
 # --------------------------------------------------------------------------
 
-
-sub data_exists {
-    my( $self, $data ) = @_;
-    return 0 if $data =~ m/<title>error<\/title>/iog;
-    return 1;
+sub build_keylist {
+    my ( $self, $keyhash ) = @_;
+    my $keys = []; # Start with an empty array
+    foreach my $identifier (@{$self->identifier_order}) {
+        foreach my $key (@{$keyhash->{$identifier}}) {
+            push @{$keys}, $key;
+        }
+    }
+    return $keys;
 }
 
-
 sub send_json {
-    my( $self, $xml ) = @_;
-    return 0 unless $self->data_exists($xml);
-    my $doc;
-
-    try {
-        $doc = XML::LibXML->new->parse_string($xml);
-    } catch Error with {
-        my $err = shift;
-        $logger->error("added content XML parser error: $err\n\n$xml");
-        $doc = undef;
-    };
-
-    return 0 unless $doc;
-    my $perl = OpenSRF::Utils::SettingsParser::XML2perl($doc->documentElement);
+    my( $self, $doc, $contentNode ) = @_;
+    return 0 unless defined $doc;
+    my @nodes = $doc->findnodes('//cc:' . $contentNode . '[*]');
+    return 0 if (scalar(@nodes) < 1);
+    my $perl = OpenSRF::Utils::SettingsParser::XML2perl($nodes[0]);
     my $json = OpenSRF::Utils::JSON->perl2JSON($perl);
     return { content_type => 'text/plain', content => $json };
 }
 
 sub send_xml {
-    my( $self, $xml ) = @_;
-    return 0 unless $self->data_exists($xml);
-    return { content_type => 'application/xml', content => $xml };
+    my( $self, $doc, $contentNode ) = @_;
+    return 0 unless defined $doc;
+    my @nodes = $doc->findnodes('//cc:' . $contentNode . '[*]');
+    return 0 if (scalar(@nodes) < 1);
+    my $newdoc = XML::LibXML::Document->new( '1.0', 'utf-8' );
+    my $clonenode = $nodes[0]->cloneNode(1);
+    $newdoc->adoptNode($clonenode);
+    $newdoc->setDocumentElement($clonenode);
+    return { content_type => 'application/xml', content => $newdoc->toString() };
 }
 
 sub send_html {
     my( $self, $content ) = @_;
-    return 0 unless $self->data_exists($content);
 
     # Hide anything that might contain a link since it will be broken
     my $HTML = <<"    HTML";
@@ -282,39 +347,87 @@ sub send_html {
     return { content_type => 'text/html', content => $HTML };
 }
 
-sub send_img {
-    my($self, $response) = @_;
-    return { 
-        content_type => $response->header('Content-type'),
-        content => $response->content, 
+sub send_jacket {
+    my( $self, $keyhash, $size ) = @_;
+
+    my $doc = $self->fetch_xmldoc({name => 'JacketDetail', attributes => [['Type',$size],['Encoding','HEX']]}, $keyhash);
+    return 0 unless defined $doc;
+
+    return $self->parse_jacket($doc, $size);
+}
+
+sub parse_jacket {
+    my( $self, $doc, $size ) = @_;
+    my @nodes = $doc->findnodes("//cc:JacketItem[cc:Type/\@Code = '$size']");
+    return 0 if (scalar(@nodes) < 1);
+
+    my $jacketItem = shift(@nodes); # We only care about the first jacket
+    my @formatNodes = $jacketItem->findnodes('./cc:Format');
+    my $format = $formatNodes[0]->textContent;
+    my @jacketNodes = $jacketItem->findnodes('./cc:Jacket');
+    my $imageData = pack('H*',$jacketNodes[0]->textContent);
+
+    return {
+        content_type => 'image/' . lc($format),
+        content => $imageData,
         binary => 1 
     };
 }
 
-# returns the raw content returned from the URL fetch
-sub fetch_content {
-    my( $self, $contentType, $key ) = @_;
-    return $self->fetch_response($contentType, $key)->content;
+# returns an XML document ready for parsing if $keyhash contained usable keys
+# otherwise returns undef
+sub fetch_xmldoc {
+    my( $self, $contentTypes, $keyhash ) = @_;
+
+    my $keys = $self->build_keylist($keyhash);
+    return undef unless @{$keys};
+
+    my $content = $self->fetch_response($contentTypes, $keys)->content;
+    my $doc = XML::LibXML->new->parse_string($content);
+    $doc->documentElement->setNamespace('http://ContentCafe2.btol.com', 'cc');
+    return $doc;
 }
 
 # returns the HTTP response object from the URL fetch
 sub fetch_response {
-    my( $self, $contentType, $key ) = @_;
-    my $userid = $self->userid;
-    my $password = $self->password;
-    my $url = $base_url . "?UserID=$userid&Password=$password&Key=$key&Content=$contentType";
-    return $AC->get_url($url);
-}
+    my( $self, $contentTypes, $keys ) = @_;
 
-# returns the HTTP response object from the URL fetch
-sub fetch_cover_response {
-    my( $self, $size, $key ) = @_;
-    my $userid = $self->userid;
-    my $password = $self->password;
-    my $return = $self->return_behavior_on_no_jacket_image;
-    my $url = $cover_base_url . "?UserID=$userid&Password=$password&Return=$return&Type=$size&Value=$key";
-    return $AC->get_url($url);
-}
+    if (ref($contentTypes) ne 'ARRAY') {
+        $contentTypes = [ $contentTypes ];
+    }
 
+    my $xmlRequest = XML::LibXML::Document->new( '1.0', 'utf-8' );
+    my $root = $xmlRequest->createElementNS('http://ContentCafe2.btol.com','ContentCafe');
+    $root->addChild($xmlRequest->createAttribute('DateTime', DateTime->now()->datetime));
+    $xmlRequest->setDocumentElement($root);
+    my $requestItems = $xmlRequest->createElement('RequestItems');
+    $requestItems->addChild($xmlRequest->createAttribute('UserID', $self->userid));
+    $requestItems->addChild($xmlRequest->createAttribute('Password', $self->password));
+    $root->addChild($requestItems);
+
+    foreach my $key (@{$keys}) {
+        my $requestItem = $xmlRequest->createElement('RequestItem');
+        my $keyNode = $xmlRequest->createElement('Key');
+        $keyNode->addChild($xmlRequest->createTextNode($key));
+        $requestItem->addChild($keyNode);
+
+        foreach my $contentType (@{$contentTypes}) {
+            my $contentNode = $xmlRequest->createElement('Content');
+            if (ref($contentType) eq 'HASH') {
+                $contentNode->addChild($xmlRequest->createTextNode($contentType->{name}));
+                foreach my $contentAttribute (@{$contentType->{attributes}}) {
+                    $contentNode->addChild($xmlRequest->createAttribute($contentAttribute->[0], $contentAttribute->[1]));
+                }
+            } else {
+                $contentNode->addChild($xmlRequest->createTextNode($contentType));
+            }
+            $requestItem->addChild($contentNode);
+        }
+
+        $requestItems->addChild($requestItem);
+    }
+    my $response = $AC->post_url($post_url, $xmlRequest->toString);
+    return $response;
+}
 
 1;
diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/addedcontent_contentcafe.txt b/docs/RELEASE_NOTES_NEXT/OPAC/addedcontent_contentcafe.txt
new file mode 100644 (file)
index 0000000..74a98bb
--- /dev/null
@@ -0,0 +1,22 @@
+Content Cafe Added Content Update
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The OpenILS::WWW::AddedContent::ContentCafe provider has been updated to use the
+newer Content Cafe 2 API in full. With this update the ability to load content
+based on ISBN or UPC is now enabled.
+
+
+"No Image" Images
++++++++++++++++++
+With the updated code the option for displaying a "No Image" image or a 1x1
+pixel image is no longer available. Instead the Apache-level "blank image" rules
+will trigger when no image is available. The configuration option controlling
+this behavior can thus be removed from opensrf.xml entirely.
+
+
+Identifier Selection
+++++++++++++++++++++
+By default the module will prefer ISBNs over UPCs, but will request information
+for both. If you wish for UPCs to be preferred, or wish one of the two
+identifier types to not be considered at all, you can change the
+"identifier_order" option in opensrf.xml. When the option is present only the
+identifier(s) listed will be sent.