diff --git a/bin/app.pl b/bin/app.pl
new file mode 100755
index 0000000..72025df
--- /dev/null
+++ b/bin/app.pl
@@ -0,0 +1,4 @@
+#!/usr/bin/env perl
+use Dancer;
+use PrimoServices;
+dance;
diff --git a/environments/development.yml.dist b/environments/development.yml.dist
new file mode 100644
index 0000000..1107437
--- /dev/null
+++ b/environments/development.yml.dist
@@ -0,0 +1,27 @@
+# configuration file for development environment
+
+# the logger engine to use
+# console: log messages to STDOUT (your console where you started the
+# application server)
+# file: log message to a file in log/
+logger: "console"
+
+# the log level for this environment
+# core is the lowest, it shows Dancer's core log messages as well as yours
+# (debug, info, warning and error)
+log: "core"
+
+# should Dancer consider warnings as critical errors?
+warnings: 1
+
+# should Dancer show a stacktrace when an error is caught?
+show_errors: 1
+
+# auto_reload is a development and experimental feature
+# you should enable it by yourself if you want it
+# Module::Refresh is needed
+#
+# Be aware it's unstable and may cause a memory leak.
+# DO NOT EVER USE THIS FEATURE IN PRODUCTION
+# OR TINY KITTENS SHALL DIE WITH LOTS OF SUFFERING
+auto_reload: 0
diff --git a/environments/production.yml.dist b/environments/production.yml.dist
new file mode 100644
index 0000000..f16492b
--- /dev/null
+++ b/environments/production.yml.dist
@@ -0,0 +1,17 @@
+# configuration file for production environment
+
+# only log warning and error messages
+log: "warning"
+
+# log message to a file in logs/
+logger: "file"
+
+# don't consider warnings critical
+warnings: 0
+
+# hide errors
+show_errors: 0
+
+# cache route resolution for maximum performance
+route_cache: 1
+
diff --git a/lib/PrimoServices.pm b/lib/PrimoServices.pm
new file mode 100644
index 0000000..672880e
--- /dev/null
+++ b/lib/PrimoServices.pm
@@ -0,0 +1,64 @@
+package PrimoServices;
+use Dancer 1.3132 ':syntax';
+use Dancer::Exception ':all';
+
+use PrimoServices::Dispatcher;
+
+use Time::HiRes();
+use Benchmark(':hireswallclock');
+
+# http://semver.org/
+# X.Y.Z (Major.Minor.Patch)
+use version; our $VERSION = version->declare("v2.2.0");
+
+#
+# Route handlers
+#
+
+get qr{.*} => sub {
+
+ my $data;
+
+ # Parse and reply to the request
+ #$data = PrimoServices::Dispatcher::handle_request();
+
+ try {
+ my $execution_time = timeit( 1, sub{
+ $data = PrimoServices::Dispatcher::handle_request();
+ });
+
+ # Calculate the total execution time of the call
+ $data->{appExecutionTime} = sprintf "%.3fs", $execution_time->real;
+ }
+ catch {
+ status 500;
+ $data->{error} = 'PrimoServices';
+ $data->{exception} = $_;
+ };
+
+ # Insert the request into the response (in development)
+ $data->{request} = params if config->{environment} eq 'development';
+
+ # Delete request_lookup_table (outside development)
+ delete $data->{request_lookup_table} if config->{environment} ne 'development';
+
+ # Count requested and returned number of IDs
+ $data->{totalItems} = param_array 'id';
+ $data->{totalItemsReturned} = @{$data->{items}};
+
+ # Name and version
+ $data->{appName} = config->{appname} . ' ' . $VERSION->normal();
+
+ # Set the serializer to either JSONP or JSON
+ if ( params->{callback} ) {
+ set serializer => 'JSONP';
+ }
+ else {
+ set serializer => 'JSON';
+ }
+
+ # Serialize...
+ return $data;
+};
+
+true;
diff --git a/lib/PrimoServices/Dispatcher.pm b/lib/PrimoServices/Dispatcher.pm
new file mode 100644
index 0000000..ce0d29f
--- /dev/null
+++ b/lib/PrimoServices/Dispatcher.pm
@@ -0,0 +1,533 @@
+package PrimoServices::Dispatcher;
+use 5.10.0;
+use Dancer ':syntax';
+use Dancer::Exception ':all';
+
+use PrimoServices::Parsers ':all';
+
+use HTTP::Async();
+use HTTP::Request();
+use HTTP::Headers();
+use URI();
+use CHI();
+use locale;
+use Digest::SHA();
+use List::MoreUtils();
+use Data::Dumper();
+
+# Setup $cache CHI object
+my $uid = $ENV{LOGNAME} || $ENV{USER} || getpwuid($<);
+my $cache = CHI->new(
+ namespace => __PACKAGE__,
+ driver => config->{caching}{chi_driver},
+ root_dir => config->{caching}{chi_root_dir} . '_' . $uid,
+ depth => config->{caching}{chi_depth},
+ expires_in => config->{caching}{default_expires_in},
+ );
+
+# Setup $async HTTP::Async object
+my $async = HTTP::Async->new(
+ slots => config->{http_async}{slots},
+ timeout => config->{http_async}{max_request_time}
+ );
+my $headers = HTTP::Headers->new(
+ Accept_Encoding => 'gzip',
+ Accept_Charset => 'utf-8',
+ User_Agent => config->{http_async}{user_agent}
+ );
+
+
+# We will use the given/when 'switch' feature of perl 5.10.0
+no if $] >= 5.018, warnings => "experimental";
+use feature 'switch';
+
+
+sub handle_request {
+ # Get URL parameters
+ my %params = request->params;
+
+ # The response that we are building
+ my %data;
+
+ # Dispatch initial web service requests
+ request_webservices(\%params,\%data);
+
+ # Retrieve responses (and dispatch further web service requests)
+ retrieve_webservices(\%data);
+
+
+ return \%data;
+}
+
+sub request_webservices {
+ my $params_ref = shift;
+ my $data_ref = shift;
+
+ # Our helper lookup-table which will link request id numbers to the request type and key
+ my %request_lookup_table;
+
+ #
+ # Primo Central
+ #
+
+ # Query Primo Central (not in case of blended search or call from a Primo Central tab)
+ if ( $params_ref->{scope} && $params_ref->{scope} !~ m{primo_central_multiple_fe} ) {
+
+ # Turn the query string into an unique id
+ my $key;
+ {
+ # Avoid problems with UTF-8 strings
+ use bytes;
+ $key = 'primocentral' . Digest::SHA::sha1_hex( $params_ref->{query} . $params_ref->{scope} );
+ }
+
+ unless ( $cache->is_valid($key) && ! defined params->{nocaching} ) {
+ # Not in cache
+ my $uri = URI->new( config->{primo}{'api'} . config->{primo}{x_search_brief} );
+
+ $uri->query_form(
+ institution => config->{primo}{institution},
+ pcAvailability => 1,
+ indx => 1,
+ bulkSize => 1,
+ json => 'true',
+ loc => 'adaptor,primo_central_multiple_fe',
+ query => 'any,contains,' . param 'query' );
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ $request_lookup_table{$req_id} = { type => 'primocentral', uri => $uri->as_string, key => $key };
+ }
+ else {
+ # Get from cache
+ $data_ref->{primoCentral} = $cache->get($key);
+ }
+ }
+
+ #
+ # Keywords
+ #
+
+ # Query for each individual keyword in the search string
+ if ( $params_ref->{query} ) {
+ my $query = $params_ref->{query};
+
+ # Normalize query string
+ $query =~ s{
+ # Remove 's
+ 's}{}gsxmi;
+
+ $query =~ s{
+ # Concatenate ' parts
+ '}{}gsxm;
+
+ $query =~ s{
+ # Remove non words
+ [\W]}{ }gsxm;
+
+ # Replace any horizontal white space with a single space
+ $query =~ s{\h+}{ }gsxm;
+
+ # Remove heading and / or tailing white space
+ $query =~ s{^\s|\s$}{}gsxm;
+
+ my @keywords;
+
+ foreach my $keyword ( split / /, $query ) {
+ # Skip short keywords
+ if ( length($keyword) < 3 ) {
+ push @{$data_ref->{keywordSearch}}, { $keyword => '...' };
+ next;
+ }
+
+ # Turn the keyword query into an unique id
+ my $key;
+ {
+ # Avoid problems with UTF-8 strings
+ use bytes;
+ $key = 'keyword' . Digest::SHA::sha1_hex( $keyword . $params_ref->{scope} . '' );
+ }
+
+ unless ( $cache->is_valid($key) && ! defined params->{nocaching} ) {
+ # Create a list of individual search scopes
+ my @search_scopes;
+ while ( $params_ref->{scope} =~ m{scope:\((.*?)\)}gsxm ) {
+ push @search_scopes, $1;
+ }
+ my $scope;
+ $scope = 'local,scope:(' . join(',', @search_scopes) . ')' if @search_scopes;
+
+ # Not in cache
+ my $uri = URI->new( config->{primo}{'api'} . config->{primo}{x_search_brief} );
+
+ # Build hash of query parameters
+ my %query_params;
+ $query_params{institution} = config->{primo}{institution};
+ $query_params{indx} = 1;
+ $query_params{bulkSize} = 1;
+ # Is it a blended search scope?
+ if ( $scope && $params_ref->{scope} =~m {primo_central_multiple_fe} ) {
+ $query_params{loc} = [ $scope, 'adaptor,primo_central_multiple_fe' ];
+ }
+ # Is it a local search scope?
+ elsif ( $scope ) {
+ $query_params{loc} = $scope;
+ }
+ # Then it is Primo Central
+ else {
+ $query_params{loc} = 'adaptor,primo_central_multiple_fe';
+ }
+ $query_params{query} = 'any,exact,' . $keyword;
+ $query_params{json} = 'true';
+
+ $uri->query_form(\%query_params);
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ $request_lookup_table{$req_id} = { type => 'keyword', keyword => $keyword, key => $key, uri => $uri->as_string };
+ }
+ else {
+ # Get from cache
+ push @{$data_ref->{keywordSearch}}, $cache->get($key);
+ }
+ }
+ }
+
+ #
+ # Primo records
+ #
+
+ # Array of the responses for each individual IDs we are gathering information about
+ my @items;
+
+ # Iterate through each ID given
+ foreach my $id ( param_array 'id' ) {
+
+ # Skip further handling of Primo Central records
+ if ( $id =~ m {
+ # Primo Central records are prefixed by 'TN_'
+ ^TN_}sxm ) {
+ push @items, { id => $id, primoStatus => 'Not a local record' };
+ next;
+ }
+
+ # If the local record is a frbr group
+ if ( $id =~ m {
+ # frbr groups are prefixed by 'frbg' followed by the id number
+ ^(?:frbg)(.*)
+ }sxm) {
+ my $frbg = $1;
+
+ unless ( $cache->is_valid($id) && ! defined params->{nocaching} ) {
+ # Not in cache
+ my $uri = URI->new( config->{primo}{'api'} . config->{primo}{x_search_brief} );
+
+ $uri->query_form(
+ institution => config->{primo}{institution},
+ loc => 'local',
+ indx => 1,
+ bulkSize => 1000,
+ query => 'facet_frbrgroupid,exact,' . $frbg,
+ json => 'true' );
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ $request_lookup_table{$req_id} = { type => 'frbg', key => $id, uri => $uri->as_string };
+ }
+ else {
+ # Get from cache
+ push @items, $cache->get($id);
+ }
+ }
+ # Then this is a single, local record
+ else {
+ unless ( $cache->is_valid($id) && ! defined params->{nocaching} ) {
+ # Not in cache
+ my $uri = URI->new( config->{primo}{'api'} . config->{primo}{x_search_full} );
+
+ $uri->query_form(
+ institution => config->{primo}{institution},
+ indx => 1,
+ bulkSize => 1,
+ getDelivery => 'true',
+ query => 'any,contains,',
+ docId => $id,
+ json => 'true' );
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ $request_lookup_table{$req_id} = { type => 'record', key => $id, uri => $uri->as_string };
+ }
+ else {
+ # Get from cache
+ my $item = $cache->get($id);
+ push @items, $item;
+ # Get JournalTOCs from cache
+ if ( $item->{metadata}{issn} ) {
+ my $key = 'issn' . lc $item->{metadata}{issn};
+ my $item = $cache->get($key);
+ if ( $item && $item->{id} ) {
+ push @{$data_ref->{journalTOCs}}, $item;
+ }
+ }
+ # Get GoogleBooks from cache
+ if ( $item->{metadata}{isbn} ) {
+ my $key = 'isbn' . lc $item->{metadata}{isbn};
+ my $item = $cache->get($key);
+ if ( $item && $item->{id} ) {
+ push @{$data_ref->{googleBooks}}, $item;
+ }
+ }
+ # Get RSI from cache
+ if ( $item->{id} ) {
+ my $key = 'rsi' . $item->{id};
+ my $item = $cache->get($key);
+ if ( $item && $item->{id} ) {
+ push @{$data_ref->{rsi}}, $item;
+ }
+ }
+ }
+ }
+ }
+
+ $data_ref->{items} = \@items;
+ $data_ref->{request_lookup_table} = \%request_lookup_table;
+}
+
+sub retrieve_webservices {
+ my $data_ref = shift;
+
+ # Wait until a response has arrived
+ while ( my ($response, $req_id) = $async->wait_for_next_response ) {
+
+ # Is it a success?
+ if ( $response->message eq 'OK' ) {
+
+ my $content = $response->decoded_content;
+
+ given ( $data_ref->{request_lookup_table}{$req_id}{type} ) {
+ when ( /record/ ) {
+ my %values = parse_record(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{items}}, \%values;
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+
+ #
+ # Google Books
+ #
+
+ # Send to Google Books API if we have an ISBN numer and the item is in print
+ if ( $values{metadata}{isbn} && grep /Physical Item/, @{$values{delcategory}} ) {
+
+ my $key = 'isbn' . lc $values{metadata}{isbn};
+ $key =~ s/-//g;
+
+ unless ( $cache->is_valid($key) && ! defined params->{nocaching} ) {
+ my ($req_id, $uri) = query_googlebooks($values{metadata}{isbn});
+ $data_ref->{request_lookup_table}{$req_id} = {
+ type => 'googlebooks',
+ key => $key,
+ isbn => $values{metadata}{isbn},
+ uri => $uri,
+ linked_item => $values{id}
+ };
+ }
+ else {
+ # Get from cache
+ push @{$data_ref->{googleBooks}}, $cache->get($key);
+ }
+ }
+
+ #
+ # JournalTOCs
+ #
+
+ # Send to JournalTOCs if we have an ISSN number and the record is a journal
+ if ( $values{metadata}{issn} && $values{type} eq 'journal' ) {
+
+ my $key = 'issn' . lc $values{metadata}{issn};
+
+ unless ( $cache->is_valid($key) && ! defined params->{nocaching} ) {
+ my ($req_id, $uri) = query_journaltocs($values{metadata}{issn});
+ $data_ref->{request_lookup_table}{$req_id} = {
+ type => 'journaltocs',
+ key => $key,
+ issn => $values{metadata}{issn},
+ uri => $uri,
+ linked_item => $values{id}
+ };
+ }
+ else {
+ # Get from cache
+ push @{$data_ref->{journalTOCs}}, $cache->get($key);
+ }
+ }
+
+ #
+ # RSI
+ #
+
+ # Send to SFX RSI API to check for online resource
+ if ( defined @{$values{delcategory}}[0]
+ && @{$values{delcategory}} == 1 && @{$values{delcategory}}[0] eq 'Physical Item'
+ && ($values{metadata}{isbn} || $values{metadata}{issn}) ) {
+
+ my $key = 'rsi' . $values{id};
+
+ unless ( $cache->is_valid($key) && ! defined params->{nocaching} ) {
+ my ($req_id, $uri) = query_rsi(\%values);
+ $data_ref->{request_lookup_table}{$req_id} = {
+ type => 'rsi',
+ key => $key,
+ uri => $uri,
+ linked_item => $values{id},
+ atitle => $values{metadata}{atitle},
+ spage => $values{metadata}{spage},
+ date => $values{metadata}{date},
+ volume => $values{metadata}{volume},
+ issue => $values{metadata}{issue},
+ };
+ }
+ else {
+ # Get from cache
+ push @{$data_ref->{rsi}}, $cache->get($key);
+ }
+ }
+ }
+ when ( /frbg/ ) {
+ my %values = parse_frbg(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{items}}, \%values;
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ when ( /keyword/ ) {
+ my %values = parse_keyword(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{keywordSearch}}, \%values;
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ when ( /primocentral/ ) {
+ my %values = parse_primocentral(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ $data_ref->{primoCentral} = \%values;
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ when ( /googlebooks/ ) {
+ my %values = parse_googlebooks(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{googleBooks}}, \%values if $values{id};
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ when ( /journaltocs/ ) {
+ my %values = parse_journaltocs(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{journalTOCs}}, \%values if $values{id};
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ when ( /rsi/ ) {
+ my %values = parse_rsi(
+ $data_ref,
+ \$content,
+ $data_ref->{request_lookup_table}{$req_id}{key},
+ $req_id );
+ push @{$data_ref->{rsi}}, \%values if $values{id};
+ $cache->set($data_ref->{request_lookup_table}{$req_id}{key}, \%values);
+ }
+ }
+ }
+ # There was an error
+ else {
+ push @{$data_ref->{errors}}, {
+ key => $data_ref->{request_lookup_table}{$req_id}{key},
+ status => $response->status_line
+ };
+ }
+
+ }
+}
+
+#
+# Subs
+#
+
+sub query_googlebooks {
+ my $isbn = shift;
+
+ my $uri = URI->new( config->{google_books}{api} );
+
+ $uri->query_form(
+ key => config->{google_books}{password},
+ filter => 'partial',
+ country => config->{google_books}{country},
+ q => 'isbn:' . $isbn
+ );
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ return $req_id, $uri->as_string;
+}
+
+sub query_journaltocs {
+ my $issn = shift;
+
+ my $uri = URI->new( config->{journaltocs}{api} . $issn );
+
+ $uri->query_form(
+ user => config->{journaltocs}{password},
+ );
+
+ my $req_id = $async->add(HTTP::Request->new( GET => $uri, $headers ));
+
+ return $req_id, $uri->as_string;
+}
+
+sub query_rsi {
+ my $values_ref = shift;
+
+ my $uri = URI->new ( config->{sfx}{rsi_api} );
+
+ # build the RSI API xml payload
+ my $rsi_request = '';
+ $rsi_request .= '