File Coverage

blib/lib/REST/Neo4p/Entity.pm
Criterion Covered Total %
statement 81 304 26.6
branch 13 132 9.8
condition 5 59 8.4
subroutine 17 39 43.5
pod 0 13 0.0
total 116 547 21.2


line stmt bran cond sub pod time code
1             #$Id$
2 36     36   531 use v5.10;
  36         114  
3             package REST::Neo4p::Entity;
4 36     36   250 use REST::Neo4p::Exceptions;
  36         76  
  36         1065  
5 36     36   225 use Carp qw(croak carp);
  36         77  
  36         2227  
6 36     36   231 use JSON;
  36         86  
  36         255  
7 36     36   4620 use URI::Escape;
  36         92  
  36         2306  
8 36     36   269 use strict;
  36         69  
  36         922  
9 36     36   194 use warnings;
  36         102  
  36         1898  
10              
11             # base class for nodes, relationships, indexes...
12             BEGIN {
13 36     36   130635 $REST::Neo4p::Entity::VERSION = '0.4003';
14             }
15              
16             our $ENTITY_TABLE = {};
17              
18             # new(\%properties)
19             # creates an entity in the db (with \%properties set), and returns
20             # a Perl object
21              
22             sub new {
23 1     1 0 3754 my $class = shift;
24 1         17 my ($entity_type) = $class =~ /.*::(.*)/;
25 1         5 $entity_type = lc $entity_type;
26 1 50       13 if ($entity_type eq 'entity') {
27 1         42 REST::Neo4p::NotSuppException->throw("Cannot use ".__PACKAGE__." directly\n");
28             }
29 0         0 my ($properties) = (@_);
30 0         0 my $url_components = delete $properties->{_addl_components};
31 0         0 my $agent = REST::Neo4p->agent;
32 0 0       0 REST::Neo4p::CommException->throw("Not connected\n") unless $agent;
33 0         0 my $decoded_resp;
34 0         0 eval {
35 0 0       0 $decoded_resp = $agent->post_data(
36             [$entity_type, $url_components ? @$url_components : ()],
37             $properties
38             );
39             };
40 0 0       0 if (my $e = REST::Neo4p::Exception->caught()) {
    0          
41             # TODO : handle cases
42 0         0 $DB::single=1;
43 0         0 $e->rethrow;
44             }
45             elsif ($e = Exception::Class->caught()) {
46 0         0 $DB::single=1;
47 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
48             }
49             # TODO: examine following line in Neo4j::Driver context
50 0 0 0     0 $decoded_resp->{self} ||= $agent->location if ref $decoded_resp;
51 0 0       0 return ref($decoded_resp) ?
52             $class->new_from_json_response($decoded_resp) :
53             $class->new_from_batch_response($decoded_resp, @$url_components);
54             }
55              
56             # TODO: refactor for when response is from Neo4j::Driver (a Result)
57             sub new_from_json_response {
58 3     3 0 7 my $class = shift;
59 3         16 my ($entity_type) = $class =~ /.*::(.*)/;
60 3         9 $entity_type = lc $entity_type;
61 3 50       8 if ($entity_type eq 'entity') {
62 0         0 REST::Neo4p::NotSuppException->throw("Cannot use ".__PACKAGE__." directly\n");
63             }
64 3         5 my ($decoded_resp) = (@_);
65 3 50       95 unless (defined $decoded_resp) {
66 0         0 REST::Neo4p::LocalException->throw("new_from_json_response() called with undef argument\n");
67             }
68 3         10 my $is_json = !(ref($decoded_resp) =~ /Neo4j::Driver/);
69 3 100 66     17 unless ($ENTITY_TABLE->{$entity_type}{_actions} || !$is_json) {
70             # capture the url suffix patterns for the entity actions:
71 2         11 for (keys %$decoded_resp) {
72 23 50       47 next unless defined $decoded_resp->{$_};
73 23         139 my ($suffix) = $decoded_resp->{$_} =~ m|.*$entity_type/[0-9]+/(.*)|;
74 23         61 $ENTITY_TABLE->{$entity_type}{_actions}{$_} = $suffix;
75             }
76             }
77             # "template" in next line is a kludge for get_indexes
78 3         6 my ($self_url, $obj);
79 3 50       8 if ($is_json) {
80 3   33     8 $self_url = $decoded_resp->{self} || $decoded_resp->{template};
81 3         7 $self_url =~ s/{key}.*$//; # another kludge for get_indexes
82 3         23 ($obj) = $self_url =~ /([a-z0-9_]+)\/?$/i;
83             }
84             else { # Driver
85 0         0 $obj = $decoded_resp->id;
86 0         0 $self_url = "$entity_type/$obj";
87             }
88 3         8 my $tbl_entry = $ENTITY_TABLE->{$entity_type}{$obj};
89 3         5 my ($start_id,$end_id,$type);
90 3 50       6 if ($is_json) {
91 3 100       8 if ($decoded_resp->{start}) {
92 1         6 ($start_id) = $decoded_resp->{start} =~ /([0-9]+)\/?$/;
93 1         5 ($end_id) = $decoded_resp->{end} =~ /([0-9]+)\/?$/;
94 1         3 $type = $decoded_resp->{type};
95             }
96             }
97             else { # Driver
98 0 0       0 if ($decoded_resp->can('start_id')) {
99 0         0 $start_id = $decoded_resp->start_id;
100 0         0 $end_id = $decoded_resp->end_id;
101 0         0 $type = $decoded_resp->type;
102             }
103             }
104 3 50       8 unless (defined $tbl_entry) {
105 3 50 33     13 if ($is_json && $decoded_resp->{template}) { # another kludge for get_indexes
106 0         0 ($decoded_resp->{type}) = $decoded_resp->{template} =~ m|index/([a-z]+)/|;
107 0         0 $type = $decoded_resp->{type};
108             }
109 3         9 $tbl_entry = $ENTITY_TABLE->{$entity_type}{$obj} = {};
110 3         7 $tbl_entry->{entity_type} = $entity_type;
111 3         8 $tbl_entry->{self} = bless \$obj, $class;
112 3         5 $tbl_entry->{self_url} = $self_url;
113 3         7 $tbl_entry->{start_id} = $start_id;
114 3         5 $tbl_entry->{end_id} = $end_id;
115 3         5 $tbl_entry->{batch} = 0;
116 3         5 $tbl_entry->{type} = $type;
117 3         9 $tbl_entry->{_handle} = REST::Neo4p->handle; # current db handle
118             }
119 3 50 33     18 if ($REST::Neo4p::CREATE_AUTO_ACCESSORS && ($entity_type ne 'index')) {
120 0         0 my $self = $tbl_entry->{self};
121 0 0       0 my $props = ($is_json ? $self->get_properties : $decoded_resp->properties);
122 0 0       0 for (keys %$props) { $self->_create_accessors($_) unless $self->can($_); }
  0         0  
123             }
124 3         11 return $tbl_entry->{self};
125             }
126              
127             sub new_from_batch_response {
128 0     0 0 0 my $class = shift;
129 0         0 my ($entity_type) = $class =~ /.*::(.*)/;
130 0         0 $entity_type = lc $entity_type;
131 0 0       0 if ($entity_type eq 'entity') {
132 0         0 REST::Neo4p::NotSuppException->throw("Cannot use ".__PACKAGE__." directly\n");
133             }
134 0         0 my ($id_token) = (@_);
135 0         0 my $tbl_entry = $ENTITY_TABLE->{$entity_type}{$id_token} = {};
136 0         0 $tbl_entry->{entity_type} = $entity_type;
137 0         0 $tbl_entry->{self} = bless \$id_token, $class;
138 0         0 $tbl_entry->{self_url} = $id_token;
139 0         0 $tbl_entry->{_handle} = REST::Neo4p->handle; # current handle
140 0         0 $tbl_entry->{batch} = 1;
141 0         0 $ENTITY_TABLE->{batch_objs}{$id_token} = $tbl_entry->{self};
142 0         0 return $tbl_entry->{self};
143             }
144              
145             # remove() - delete the node and destroy the object
146             sub remove {
147 0     0 0 0 my $self = shift;
148 0 0       0 return 1 unless defined $self->_handle; # gone already
149 0         0 my @url_components = @_;
150 0         0 my $entity_type = ref $self;
151 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
152 0         0 local $REST::Neo4p::HANDLE;
153 0         0 REST::Neo4p->set_handle($self->_handle);
154 0         0 my $agent = REST::Neo4p->agent;
155 0         0 eval {
156 0         0 $agent->delete_data($entity_type, @url_components, $$self);
157             };
158 0 0       0 if (my $e = REST::Neo4p::NotFoundException->caught()) {
    0          
159 0         0 1;
160             }
161             elsif ($e = Exception::Class->caught()) {
162 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
163             }
164 0         0 $self->_deregister;
165 0         0 return 1;
166             }
167             # set_property( { prop1 => $val1, prop2 => $val2, ... } )
168             # ret true if success, false if fail
169             sub set_property {
170 0     0 0 0 my $self = shift;
171 0         0 my ($props) = @_;
172 0 0 0     0 REST::Neo4p::LocalException->throw("Arg must be a hashref\n") unless ref($props) && ref $props eq 'HASH';
173 0         0 my $entity_type = ref $self;
174 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
175 0         0 local $REST::Neo4p::HANDLE;
176 0         0 REST::Neo4p->set_handle($self->_handle);
177 0         0 my $agent = REST::Neo4p->agent;
178 0   0     0 my $suffix = ($self->_get_url_suffix('property') // 'properties/{key}');
179 0         0 my @ret;
180 0         0 $suffix =~ s|/[^/]*$||; # strip the '{key}' placeholder
181 0         0 for (keys %$props) {
182 0         0 eval {
183             $agent->put_data([$entity_type,$$self,$suffix,
184 0         0 $_], $props->{$_});
185             };
186              
187 0 0       0 if (my $e = REST::Neo4p::NotFoundException->caught('REST::Neo4p::Exception')) {
    0          
188             # TODO : handle different classes
189 0         0 $e->rethrow;
190             }
191             elsif ($e = Exception::Class->caught()) {
192 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
193             }
194             }
195             # create accessors
196 0 0       0 if ($REST::Neo4p::CREATE_AUTO_ACCESSORS) {
197 0 0       0 for (keys %$props) { $self->_create_accessors($_) unless $self->can($_) }
  0         0  
198             }
199 0         0 return $self;
200             }
201              
202             # @prop_values = get_property( qw(prop1 prop2 ...) )
203             sub get_property {
204 0     0 0 0 my $self = shift;
205 0         0 my @props = @_;
206 0         0 my $entity_type = ref $self;
207 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
208 0         0 local $REST::Neo4p::HANDLE;
209 0         0 REST::Neo4p->set_handle($self->_handle);
210 0         0 my $agent = REST::Neo4p->agent;
211 0 0       0 REST::Neo4p::CommException->throw("Not connected\n") unless $agent;
212 0   0     0 my $suffix = ($self->_get_url_suffix('property') // 'properties/{key}');
213 0         0 my @ret;
214 0         0 $suffix =~ s|/[^/]*$||; # strip the '{key}' placeholder
215 0         0 for (@props) {
216 0         0 my $decoded_resp;
217 0         0 eval {
218 0         0 $decoded_resp = $agent->get_data($entity_type,$$self,$suffix,$_);
219             };
220              
221 0 0       0 if ( my $e = REST::Neo4p::NotFoundException->caught()) {
    0          
222 0         0 push @ret, undef;
223             }
224             elsif ( $e = Exception::Class->caught()) {
225 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
226             }
227             else {
228             # TODO: handle in Neo4j::Driver case
229 0         0 _unescape($decoded_resp);
230 0         0 push @ret, $decoded_resp;
231             }
232             }
233 0 0       0 return @ret == 1 ? $ret[0] : @ret;
234             }
235              
236             # $prop_hash = get_properties()
237             sub get_properties {
238 0     0 0 0 my $self = shift;
239 0         0 my $entity_type = ref $self;
240 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
241 0         0 local $REST::Neo4p::HANDLE;
242 0         0 REST::Neo4p->set_handle($self->_handle);
243 0         0 my $agent = REST::Neo4p->agent;
244 0 0       0 REST::Neo4p::CommException->throw("Not connected\n") unless $agent;
245 0   0     0 my $suffix = ($self->_get_url_suffix('property') // 'properties/{key}');
246 0         0 $suffix =~ s|/[^/]*$||; # strip the '{key}' placeholder
247 0         0 my $decoded_resp;
248 0         0 eval {
249 0         0 $decoded_resp = $agent->get_data($entity_type,$$self,$suffix);
250             };
251 0         0 my $e;
252 0 0       0 if ($e = REST::Neo4p::NotFoundException->caught()) {
    0          
253 0         0 return;
254             }
255             elsif ($e = Exception::Class->caught()) {
256 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
257             }
258             # TODO: handle in Neo4j::Driver case
259 0         0 _unescape($decoded_resp);
260 0         0 return $decoded_resp;
261             }
262              
263             sub _unescape {
264 0     0   0 local $_ = shift;
265 0 0       0 if (ref eq 'HASH') {
    0          
266 0         0 while ( my ($k,$v) = each %$_ ) {
267 0 0       0 if (ref $v eq '') {
268 0         0 $_->{$k} = uri_unescape($v);
269             }
270             else {
271 0         0 _unescape($v);
272             }
273             }
274             }
275             elsif (ref eq 'ARRAY') {
276 0         0 foreach my $v (@$_) {
277 0         0 _unescape($v);
278             }
279             }
280             }
281             # remove_property( qw(prop1 prop2 ...) )
282             sub remove_property {
283 0     0 0 0 my $self = shift;
284 0         0 my @props = @_;
285 0         0 my $entity_type = ref $self;
286 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
287 0         0 local $REST::Neo4p::HANDLE;
288 0         0 REST::Neo4p->set_handle($self->_handle);
289 0         0 my $agent = REST::Neo4p->agent;
290 0 0       0 REST::Neo4p::CommException->throw("Not connected\n") unless $agent;
291 0   0     0 my $suffix = ($self->_get_url_suffix('property') // 'properties/{key}');
292 0         0 $suffix =~ s|/[^/]*$||; # strip the '{key}' placeholder
293 0         0 foreach (@props) {
294 0         0 eval {
295 0         0 $agent->delete_data($entity_type,$$self,$suffix,$_);
296             };
297 0 0       0 if (my $e = REST::Neo4p::Exception->caught()) {
    0          
298             # TODO : handle different classes
299 0         0 $e->rethrow;
300             }
301             elsif ($e = Exception::Class->caught()) {
302 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
303             }
304             }
305 0         0 return $self;
306             }
307              
308             sub as_simple {
309 0     0 0 0 my $self = shift;
310 0         0 return;
311             }
312              
313             sub simple_from_json_response {
314 0     0 0 0 my $class = shift;
315 0         0 my ($decoded_resp) = @_;
316 0         0 return;
317             }
318              
319 0     0 0 0 sub id { 0 + ${$_[0]} }
  0         0  
320 0     0 0 0 sub is_batch { shift->_entry->{batch} }
321 0     0 0 0 sub entity_type { shift->_entry->{entity_type} }
322              
323             # $obj = REST::Neo4p::Entity->_entity_by_id($entity_type, $id[, $idx_type]) or
324             # $node_obj = REST::Neo4p::Node->_entity_by_id($id);
325             # $relationship_obj = REST::Neo4p::Relationship->_entity_by_id($id)
326             # $index_obj = REST::Neo4p::Index->_entity_by_id($id, $idx_type);
327             sub _entity_by_id {
328 0     0   0 my $class = shift;
329 0 0       0 REST::Neo4p::ClassOnlyException->throw() if (ref $class);
330            
331 0         0 my $entity_type = $class;
332 0         0 my ($id, $idx_type);
333 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
334 0 0       0 if ($entity_type eq 'entity') {
335 0         0 ($entity_type,$id,$idx_type) = @_;
336             }
337             else {
338 0         0 ($id,$idx_type) = @_;
339             }
340 0 0 0     0 if ($entity_type eq 'index' && !$idx_type) {
341 0         0 REST::Neo4p::LocalException->throw("Index requested, but index type not provided in last arg\n");
342             }
343 0         0 my $new;
344 0 0       0 unless ($ENTITY_TABLE->{$entity_type}{$id}) {
345             # not recorded as object yet
346 0         0 my $agent = REST::Neo4p->agent;
347 0 0       0 REST::Neo4p::CommException->throw("Not connected\n") unless $agent;
348 0         0 my ($rq, $decoded_resp);
349 0 0       0 if ($entity_type eq 'index') {
350             # get list of indexes and choose the one (if any) matching the
351             # given index name...
352 0         0 $rq = "get_${idx_type}_index";
353 0         0 eval {
354 0         0 $decoded_resp = $agent->$rq();
355             };
356 0         0 my $e;
357 0 0       0 if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
    0          
358             # TODO : handle different classes
359 0         0 $e->rethrow;
360             }
361             elsif ($@) {
362 0 0       0 ref $@ ? $@->rethrow : die $@;
363             }
364             # TODO: handle for Neo4j::Driver case
365 0         0 $decoded_resp = $decoded_resp->{$id};
366 0 0       0 unless (defined $decoded_resp) {
367 0         0 REST::Neo4p::NotFoundException->throw
368             (
369             message => "Index '$id' not found in db\n",
370             neo4j_message => "Neo4j call was successful, but index '$id'".
371             "was not returned in the list of indexes\n"
372             );
373              
374             }
375             }
376             else {
377             # usual way to get entities...
378 0         0 $rq = "get_${entity_type}";
379              
380 0         0 eval {
381 0         0 $decoded_resp = $agent->$rq($id);
382             };
383              
384 0 0       0 if (my $e = REST::Neo4p::Exception->caught()) {
    0          
385             # TODO : handle different classes
386 0         0 $e->rethrow;
387             }
388             elsif ($e = Exception::Class->caught()) {
389 0 0 0     0 (ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
390             }
391             }
392             # TODO: check this works for Neo4j::Driver case after new_from_json_response refactor
393 0 0       0 return unless defined $decoded_resp;
394 0 0       0 $new = ref($decoded_resp) ? $class->new_from_json_response($decoded_resp) :
395             $class->new_from_batch_response($decoded_resp);
396             }
397 0   0     0 return $ENTITY_TABLE->{$entity_type}{$id}{self} || $new;
398             }
399              
400             sub _get_url_suffix {
401 0     0   0 my $self = shift;
402 0         0 my ($action) = @_;
403 0         0 my $entity_type = ref $self;
404 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
405 0         0 my $a = $ENTITY_TABLE->{$entity_type}{_actions};
406 0   0     0 my $suffix = ($a && $a->{$action}) // REST::Neo4p->agent->{_actions}{$action};
      0        
407             }
408              
409             # get the $ENTITY_TABLE entry for the object
410             sub _entry {
411 0     0   0 my $self = shift;
412 0         0 my $entity_type = ref $self;
413 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
414 0         0 return $ENTITY_TABLE->{$entity_type}{$$self};
415             }
416             sub _self_url {
417 0     0   0 my $self = shift;
418 0 0       0 return $self->_entry->{self_url} if $self->_entry;
419 0         0 return;
420             }
421              
422             # get the $ENTITY_TABLE entry for the object
423             sub _handle {
424 0     0   0 my $self = shift;
425 0 0       0 return $self->_entry->{_handle} if $self->_entry;
426 0         0 return;
427             }
428              
429             sub _deregister {
430 0     0   0 my $self = shift;
431 0         0 my $entity_type = ref $self;
432 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
433 0         0 foreach (sort keys %{$ENTITY_TABLE->{$entity_type}{$$self}}) {
  0         0  
434 0         0 delete $ENTITY_TABLE->{$entity_type}{$$self}{$_};
435             }
436 0         0 delete $ENTITY_TABLE->{$entity_type}{$$self};
437             }
438              
439             sub DESTROY {
440 0     0   0 my $self = shift;
441 0         0 my $entity_type = ref $self;
442 0         0 $entity_type =~ s/.*::(.*)/\L$1\E/;
443 0 0       0 $self->_deregister if $ENTITY_TABLE->{$entity_type}{$$self}{entity_type};
444             }
445              
446             sub _create_accessors {
447 0     0   0 my $self = shift;
448 0         0 my $class = ref $self;
449 0         0 my ($prop_name) = @_;
450 36     36   432 no strict qw(refs);
  36         125  
  36         6321  
451 0         0 *{$class."::$prop_name"} = sub {
452 0     0   0 my $caller = shift;
453 0         0 $caller->get_property( $prop_name );
454 0         0 };
455 0         0 *{$class."::set_$prop_name"} = sub {
456 0     0   0 shift->set_property( {$prop_name => $_[0]} );
457 0         0 };
458             }
459              
460             package REST::Neo4p::Simple;
461 36     36   290 use base 'REST::Neo4p::Entity';
  36         70  
  36         4115  
462 36     36   286 use strict;
  36         75  
  36         990  
463 36     36   253 use warnings;
  36         80  
  36         1691  
464 36     36   228 no warnings qw/once/;
  36         79  
  36         2103  
465             BEGIN {
466 36     36   158 $REST::Neo4p::Simple::VERSION = '0.4003';
467 36         3639 $REST::Neo4p::Simple::VERSION = '0.4003';
468             }
469              
470 72     72   138 sub new { $_[1] }
471              
472             *new_from_json_response = \&new;
473             *simple_from_json_response = \&new;
474              
475             1;
476              
477             =head1 NAME
478              
479             REST::Neo4p::Entity - Base class for Neo4j entities
480              
481             =head1 SYNOPSIS
482              
483             Not intended to be used directly. Use subclasses
484             L,
485             L and
486             L instead.
487              
488             =head1 DESCRIPTION
489              
490             REST::Neo4p::Entity is the base class for the node, relationship and
491             index classes which should be used directly. The base class
492             encapsulates most of the L calls to the Neo4j
493             server, converts JSON responses to Perl references, acknowledges
494             errors, and maintains the main object table.
495              
496             =head1 SEE ALSO
497              
498             L, L, L,
499             L.
500              
501             =head1 AUTHOR
502              
503             Mark A. Jensen
504             CPAN ID: MAJENSEN
505             majensen -at- cpan -dot- org
506              
507             =head1 LICENSE
508              
509             Copyright (c) 2012-2022 Mark A. Jensen. This program is free software; you
510             can redistribute it and/or modify it under the same terms as Perl
511             itself.
512              
513             =cut
514              
515             1;