File Coverage

blib/lib/REST/Neo4p/Index.pm
Criterion Covered Total %
statement 19 182 10.4
branch 0 90 0.0
condition 0 52 0.0
subroutine 7 25 28.0
pod 8 17 47.0
total 34 366 9.2


line stmt bran cond sub pod time code
1             #$Id$#
2             package REST::Neo4p::Index;
3 36     36   300 use base 'REST::Neo4p::Entity';
  36         75  
  36         2700  
4 36     36   223 use REST::Neo4p::Exceptions;
  36         88  
  36         888  
5 36     36   191 use Carp qw(croak carp);
  36         78  
  36         1771  
6 36     36   273 use URI::Escape;
  36         71  
  36         2030  
7 36     36   265 use strict;
  36         123  
  36         1040  
8 36     36   194 use warnings;
  36         94  
  36         1586  
9              
10             BEGIN {
11 36     36   81484 $REST::Neo4p::Index::VERSION = '0.4001';
12             }
13              
14             my $unsafe = "^A-Za-z0-9\-\._\ ~";
15              
16             # TODO: auto index objects ready-made
17              
18             # new( 'node|relationship', $index_name )
19              
20             sub new {
21 0     0 1   my $class = shift;
22 0           my ($index_type, $name, $config) = @_;
23             # $config is for configuring an index (fulltext lucene e.g.)
24 0 0         if (grep /^$name$/,qw(node relationship)) {
25 0           my $a = $name;
26 0           $name = $index_type;
27 0           $index_type = $a;
28             }
29 0 0         unless (grep /^$index_type$/,qw(node relationship)) {
30 0           REST::Neo4p::LocalException->throw("Index type must be either node or relationship\n");
31             }
32 0           my $properties = {
33             _addl_components => [$index_type],
34             name => $name
35             };
36 0           $properties->{type} = delete $config->{rtype};
37 0 0         $properties->{config} = $config if defined $config;
38 0           my $idx;
39 0           eval {
40 0           $idx = $class->_entity_by_id($name,$index_type);
41             };
42 0 0         return $idx if $idx;
43 0           return $class->SUPER::new($properties);
44             }
45              
46             sub new_from_json_response {
47 0     0 0   my $class = shift;
48 0           my ($decoded_resp) = @_;
49 0           my $obj = $class->SUPER::new_from_json_response($decoded_resp);
50 0           $obj->_entry->{action} = $obj->_entry->{type}."_index";
51 0           return $obj;
52             }
53              
54             sub new_from_batch_response {
55 0     0 0   my $class = shift;
56 0           my ($id_token,$type) = @_;
57 0           my $obj = $class->SUPER::new_from_batch_response($id_token);
58 0           $obj->_entry->{type} = $type;
59 0           $obj->_entry->{action} = "${type}_index";
60 0           return $obj;
61             }
62              
63             sub remove {
64 0     0 1   my $self = shift;
65 0           $self->SUPER::remove($self->type);
66             }
67              
68             # add an entity to an index
69             # add_entry($node, 'rating' => 'best')
70             # add_entry($node, %hash_of_entries)
71              
72             sub add_entry {
73 0     0 1   my $self = shift;
74 0           my ($entity, @entry_hash) = @_;
75 0 0         unless ($self->type eq $entity->entity_type) {
76 0           REST::Neo4p::LocalException->throw(
77             "Can't add a ".$entity->entity_type." to a ".$self->type." index\n"
78             );
79             }
80 0 0 0       unless (@entry_hash &&
      0        
81             ((ref $entry_hash[0] eq 'HASH') || !(@entry_hash % 2))) {
82 0           REST::Neo4p::LocalException->throw("A hash of key => value pairs is required\n");
83             }
84             my %entry_hash = (ref $entry_hash[0] eq 'HASH') ?
85 0 0         %{$entry_hash[0]} : @entry_hash;
  0            
86 0           local $REST::Neo4p::HANDLE;
87 0           REST::Neo4p->set_handle($self->_handle);
88 0           my $agent = REST::Neo4p->agent;
89 0           my $rq = "post_".$self->_action;
90 0           my $decoded_resp;
91 0           while (my ($key, $value) = each %entry_hash) {
92 0           eval {
93 0           $decoded_resp = $agent->$rq([$self->name],
94             { uri => $entity->_self_url,
95             key => $key,
96             value => uri_escape($value,$unsafe) }
97             );
98             };
99 0 0         if (my $e = REST::Neo4p::Exception->caught()) {
    0          
100             # TODO : handle different classes?
101 0           $e->rethrow;
102             }
103             elsif ($e = Exception::Class->caught()) {
104 0 0 0       (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
105             }
106             }
107 0           return 1;
108             }
109              
110             # remove_entry(entity), remove_entry(entity, key), remove_entry(entity, key, value)
111             sub remove_entry {
112 0     0 1   my $self = shift;
113 0           my ($entity, $key, $value) = @_;
114 0 0         unless ($self->type eq $entity->entity_type) {
115 0           REST::Neo4p::LocalException->throw(
116             "Can't modify a ".$self->type." index by referring to a ".$entity->entity_type."\n"
117             );
118             }
119 0           my @addl_components;
120 0           local $REST::Neo4p::HANDLE;
121 0           REST::Neo4p->set_handle($self->_handle);
122 0           my $agent = REST::Neo4p->agent;
123 0           my $rq = 'delete_'.$self->_action;
124 0 0         if (defined $key) {
125 0 0         if (defined $value) {
126 0           @addl_components = ($key, uri_escape($value,$unsafe), $$entity);
127             }
128             else { # !defined $value
129 0           @addl_components = ($key, $$entity);
130             }
131             }
132             else { # !defined $key && !defined $value
133 0           @addl_components = ($$entity);
134             }
135 0           eval {
136 0           $agent->$rq($self->name, @addl_components);
137             };
138 0           my $e;
139 0 0         if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
    0          
140             # TODO : handle different classes
141 0           $e->rethrow;
142             }
143             elsif ($@) {
144 0 0         ref $@ ? $@->rethrow : die $@;
145             }
146 0           return 1;
147             }
148              
149             sub find_entries {
150 0     0 1   my $self = shift;
151 0 0         if ($self->is_batch) {
152 0           REST::Neo4p::NotSuppException->throw("find_entries method not supported in batch mode (yet)\n");
153             }
154 0           my ($key, $value) = @_;
155 0           my ($query) = @_;
156 0           my $decoded_resp;
157 0           local $REST::Neo4p::HANDLE;
158 0           REST::Neo4p->set_handle($self->_handle);
159 0           my $agent = REST::Neo4p->agent;
160 0           my $rq = 'get_'.$self->_action;
161 0 0         if ($value) { # exact key->value match
162 0           eval {
163 0           $decoded_resp = $agent->$rq( $self->name,
164             $key, uri_escape($value,$unsafe) );
165             };
166 0           my $e;
167 0 0         if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
    0          
168             # TODO : handle different classes
169 0           $e->rethrow;
170             }
171             elsif ($@) {
172 0 0         ref $@ ? $@->rethrow : die $@;
173             }
174             }
175             else { # a lucene query string is first arg
176             # note in below: cannot pass { query => $query } to
177             # request, neo4j interface doesn't work with "form fills"
178             # must add the ?query string to the request url.
179 0           eval {
180 0           $decoded_resp = $agent->$rq( $self->name,
181             "?query=".uri_escape($query,$unsafe) );
182             };
183 0 0         if (my $e = Exception::Class->caught('REST::Neo4p::Exception')) {
    0          
184             # TODO : handle different classes
185 0           $e->rethrow;
186             }
187             elsif ($e = Exception::Class->caught) {
188 0 0 0       (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
189             }
190             }
191 0           my @ret;
192 0 0         my $class = $self->type eq 'node' ? 'REST::Neo4p::Node' :
193             'REST::Neo4p::Relationship';
194 0           for (@$decoded_resp) {
195 0           push @ret, $class->new_from_json_response($_);
196             }
197 0           return @ret;
198             }
199              
200             # create_unique : route to correct method
201             sub create_unique {
202 0     0 1   my $self = shift;
203 0           my $method = 'create_unique_'.$self->type;
204 0           $self->$method(@_);
205             }
206              
207             # single key => value pair
208             sub create_unique_node {
209 0     0 0   my $self = shift;
210 0           my ($key, $value, $properties, $on_found) = @_;
211 0   0       $on_found ||= 'get';
212 0           $on_found = lc $on_found;
213 0 0         unless ($self->type eq 'node') {
214 0           REST::Neo4p::LocalException->throw("Can't create node on a non-node index\n");
215             }
216 0 0 0       unless (defined $key && defined $value &&
      0        
      0        
217             defined $properties && (ref $properties eq 'HASH')) {
218 0           REST::Neo4p::LocalException->throw("Args required: key => value, hashref_of_properties\n");
219             }
220 0 0         unless ( $on_found =~ /^get|fail$/ ) {
221 0           REST::Neo4p::LocalException->throw("on_found parameter (4th arg) must be one of 'get', 'fail'\n");
222             }
223 0           local $REST::Neo4p::HANDLE;
224 0           REST::Neo4p->set_handle($self->_handle);
225 0           my $agent = REST::Neo4p->agent;
226 0           my $rq = "post_".$self->_action;
227 0 0         my $restq = 'uniqueness='.($on_found eq 'get' ? 'get_or_create' : 'create_or_fail');
228 0           my $decoded_resp;
229 0           eval {
230 0           $decoded_resp = $agent->$rq([join('?',$self->name,$restq)],
231             { key => $key,
232             value => $value,
233             properties => $properties}
234             );
235             };
236 0 0         if (my $e = Exception::Class->caught('REST::Neo4p::ConflictException')) {
    0          
237 0 0         if ($on_found eq 'fail') {
238 0           return; # user expects to get nothing back if not found
239             }
240             else {
241 0           $e->rethrow; # uh oh, better bail
242             }
243             }
244             elsif ($e = Exception::Class->caught) {
245 0 0 0       (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
246             }
247 0           return REST::Neo4p::Node->new_from_json_response($decoded_resp);
248             }
249              
250             sub create_unique_relationship {
251 0     0 0   my $self = shift;
252 0           my ($key, $value, $from_node, $to_node, $rel_type, $properties, $on_found) = @_;
253 0   0       $on_found ||= 'get';
254 0           $on_found = lc $on_found;
255 0 0         unless ($self->type eq 'relationship') {
256 0           REST::Neo4p::LocalException->throw("Can't create relationship on a non-relationship index\n");
257             }
258 0 0 0       unless (defined $key && defined $value &&
      0        
      0        
      0        
      0        
      0        
259             defined $from_node && defined $to_node &&
260             defined $rel_type &&
261             (ref $from_node eq 'REST::Neo4p::Node') &&
262             (ref $to_node eq 'REST::Neo4p::Node') ) {
263 0           REST::Neo4p::LocalException->throw("Args required: key => value, from_node => to_node, rel_type\n");
264             }
265 0 0 0       unless (!defined $properties || (ref $properties eq 'HASH')) {
266 0           REST::Neo4p::LocalException->throw("properties parameter (6th arg) must be undef or hashref of properties\n");
267             }
268 0 0         unless ( $on_found =~ /^get|fail$/ ) {
269 0           REST::Neo4p::LocalException->throw("on_found parameter (7th arg) must be one of 'get', 'fail'\n");
270             }
271 0           local $REST::Neo4p::HANDLE;
272 0           REST::Neo4p->set_handle($self->_handle);
273 0           my $agent = REST::Neo4p->agent;
274 0           my $rq = "post_".$self->_action;
275 0 0         my $restq = 'uniqueness='.($on_found eq 'get' ? 'get_or_create' : 'create_or_fail');
276 0           my $decoded_resp;
277 0           my %json_params = ( key => $key,
278             value => $value,
279             start => $from_node->_self_url,
280             end => $to_node->_self_url,
281             type => $rel_type );
282 0 0         $json_params{properties} = $properties if defined $properties;
283 0           eval {
284 0           $decoded_resp = $agent->$rq([join('?',$self->name,$restq)],
285             \%json_params);
286             };
287 0 0         if (my $e = Exception::Class->caught('REST::Neo4p::ConflictException')) {
    0          
288 0 0         if ($on_found eq 'fail') {
289 0           return; # user expects to get nothing back if not found
290             }
291             else {
292 0           $e->rethrow; # uh oh, better bail
293             }
294             }
295             elsif ($e = Exception::Class->caught) {
296 0 0 0       (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
297             }
298 0           return REST::Neo4p::Relationship->new_from_json_response($decoded_resp);
299             }
300              
301             # index name
302 0     0 1   sub name { ${$_[0]} }
  0            
303             # index type (node or relationship)
304             sub type {
305 0     0 1   my $self = shift;
306             $self->_entry && $self->_entry->{type}
307 0 0         }
308             sub _action {
309 0     0     my $self = shift;
310             $self->_entry && $self->_entry->{action}
311 0 0         }
312              
313             # unused Entity methods
314 0     0 0   sub set_property { not_supported() }
315 0     0 0   sub get_property { not_supported() }
316 0     0 0   sub get_properties { not_supported() }
317 0     0 0   sub remove_property { not_supported() }
318              
319             sub not_supported {
320 0     0 0   REST::Neo4p::NotSuppException->throw( __PACKAGE__." does not support this method\n" );
321             }
322              
323             =head1 NAME
324              
325             REST::Neo4p::Index - Neo4j index object
326              
327             =head1 SYNOPSIS
328              
329             $node_idx = REST::Neo4p::Index->new('node', 'my_node_index');
330             $rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index');
331             $fulltext_idx = REST::Neo4p::Index->new('node', 'my_ft_index',
332             { type => 'fulltext',
333             provider => 'lucene' });
334             $node_idx->add_entry( $ShaggyNode, 'pet' => 'ScoobyDoo' );
335             $node_idx->add_entry( $ShaggyNode,
336             'pet' => 'ScoobyDoo',
337             'species' => 'Dog',
338             'genotype' => 'ScSc',
339             'episodes_featured' => 2343 );
340              
341             @returned_nodes = $node_idx->find_entries('pet' => 'ScoobyDoo');
342             @returned_nodes = $node_idx->find_entries('pet:Scoob*');
343             $node_idx->remove_entry( $JosieNode, 'hair' => 'red' );
344              
345             =head1 DESCRIPTION
346              
347             REST::Neo4p::Index objects represent Neo4j node and relationship indexes.
348              
349             =head1 USAGE NOTE - VERSION 4.0
350              
351             I
352              
353             Index objects were originally designed to encapsulate Neo4j "explicit"
354             indexes, which map nodes/relationships to a key-value pair.
355              
356             As of Neo4j version 4.0, explicit indexes are not supported. Since
357             there may be applications using REST::Neo4p depending on the Index
358             functionality, the agent based on L uses fulltext
359             indexes under the hood to emulate explicit indexes. This agent is used
360             automatically with Neo4j version 4.0 servers.
361              
362             =head1 METHODS
363              
364             =over
365              
366             =item new()
367              
368             $node_idx = REST::Neo4p::Index->new('node', 'my_node_index');
369             $rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index');
370             $fulltext_idx = REST::Neo4p::Index->new('node', 'my_ft_index',
371             { type => 'fulltext',
372             provider => 'lucene' });
373             # Neo4j 4.0+
374             $rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index', {rtype => "my_reln_type"});
375              
376              
377             Creates a new index of the type given in the first argument, with the
378             name given in the second argument. The optional third argument is a
379             hashref containing an index configuration as provided for in the Neo4j
380             API.
381              
382             I: For Neo4j 4.0+, REST::Neo4p emulates an explicit index using a
383             fulltext index. Fulltext indexes on relationships require specifying a
384             relationship type. To do this, include the key C in the third
385             argument hashref.
386              
387             =item remove()
388              
389             $index->remove()
390              
391             B: This method removes the index from the database and destroys the object.
392              
393             =item name()
394              
395             $idx_name = $index->name()
396              
397             =item type()
398              
399             if ($index->type eq 'node') { $index->add_entry( $node, $key => $value ); }
400              
401             =item add_entry()
402              
403             $index->add_entry( $node, $key => $value );
404             $index->add_entry( $node, $key1 => $value1, $key2 => $value2,...);
405             $index->add_entry( $node, $key_value_hashref );
406              
407             =item remove_entry()
408              
409             $index->remove_entry($node);
410             $index->remove_entry($node, $key);
411             $index->remove_entry($node, $key => $value);
412              
413             =item find_entries()
414              
415             @returned_nodes = $node_index->find_entries($key => $value);
416             @returned_rels = $rel_index->find_entries('pet:Scoob*');
417              
418             In the first form, an exact match is sought. In the second (i.e., when
419             a single string argument is passed), the argument is interpreted as a
420             query string and passed to the index as such. The Neo4j default is
421             L.
422              
423             C is not supported in batch mode.
424              
425             =item create_unique()
426              
427             $node = $index->create_unique( name => 'fred',
428             { name => 'fred', state => 'unshaven'} );
429              
430             $reln = $index->create_unique( name => 'married_to',
431             $node => $wilma_node,
432             'MARRIED_TO');
433              
434             Creates a unique node or relationship on the basis of presence or absence
435             of a matching item in the index.
436              
437             Optional final argument: one of 'get' or 'fail'. If 'get' (default), the
438             matching item is returned if present. If 'fail', false is returned.
439              
440             =back
441              
442             =head1 SEE ALSO
443              
444             L, L, L.
445              
446             =head1 AUTHOR
447              
448             Mark A. Jensen
449             CPAN ID: MAJENSEN
450             majensen -at- cpan -dot- org
451              
452             =head1 LICENSE
453              
454             Copyright (c) 2012-2021 Mark A. Jensen. This program is free software; you
455             can redistribute it and/or modify it under the same terms as Perl
456             itself.
457              
458             =cut
459              
460             1;