1 package OpenILS::Application::Flattener;
3 # This package is not meant to be registered as a stand-alone OpenSRF
4 # application, but to be used by high level methods in other services.
6 use base qw/OpenILS::Application/;
11 use OpenSRF::EX qw/:try/;
12 use OpenSRF::Utils::Logger qw/:logger/;
13 use OpenILS::Utils::CStoreEditor q/:funcs/;
14 use OpenSRF::Utils::JSON;
16 sub _fm_link_from_class {
17 my ($class, $field) = @_;
19 return Fieldmapper->publish_fieldmapper->{$class}{links}{$field};
22 sub _flattened_search_single_flesh_wad {
23 my ($hint, $path) = @_;
25 $path = [ @$path ]; # clone for processing here
26 my $class = OpenSRF::Utils::JSON->lookup_class($hint);
29 my $flesh_fields = {};
31 pop @$path; # last part is just field
35 while ($piece = shift @$path) {
36 my $link = _fm_link_from_class($class, $piece);
38 $flesh_fields->{$hint} ||= [];
39 push @{ $flesh_fields->{$hint} }, $piece;
40 $hint = $link->{class};
41 $class = OpenSRF::Utils::JSON->lookup_class($hint);
44 throw OpenSRF::EX::ERROR("no link $piece on $class");
49 flesh => $flesh_depth,
50 flesh_fields => $flesh_fields
54 # returns a join clause AND a string representing the deepest join alias
56 sub _flattened_search_single_join_clause {
57 my ($column_name, $hint, $path) = @_;
59 my $class = OpenSRF::Utils::JSON->lookup_class($hint);
60 my $last_ident = $class->Identity;
62 $path = [ @$path ]; # clone for processing here
64 pop @$path; # last part is just field
69 my $alias; # yes, we need it out at this scope.
71 while ($piece = shift @$path) {
72 my $link = _fm_link_from_class($class, $piece);
74 $hint = $link->{class};
75 $class = OpenSRF::Utils::JSON->lookup_class($hint);
77 my $reltype = $link->{reltype};
78 my $field = $link->{key};
80 # XXX having a non-blank value for map means we'll need
81 # an additional level of join. TODO.
82 throw OpenSRF::EX::ERROR(
83 "support not yet implemented for links like '$piece' with" .
84 " non-blank 'map' IDL attribute"
88 $alias = "__${column_name}_${hint}";
90 if ($reltype eq "has_a") {
97 } elsif ($reltype eq "has_many" or $reltype eq "might_have") {
105 throw OpenSRF::EX::ERROR("unexpected reltype for link $piece");
109 $last_join->{join}{$alias} = $new_join;
111 $core_join->{$alias} = $new_join;
114 $last_ident = $class->Identity;
115 $last_join = $new_join;
117 throw new OpenSRF::EX::ERROR("no link '$piece' on $class");
121 return ($core_join, $alias);
124 # When $value is a string (short form of a column definition), it is assumed to
125 # be a dot-delimited path. This will be normalized into a hash (long form)
126 # containing and path key, whose value will be made into an array, and true
127 # values for sort/filter/display.
129 # When $value is already a hash (long form), just make an array of the path key
130 # and explicity set any sort/filter/display values not present to 0.
132 sub _flattened_search_normalize_map_column {
135 if (ref $value eq "HASH") {
136 foreach (qw/sort filter display/) {
137 $value->{$_} = 0 unless exists $value->{$_};
139 $value->{path} = [split /\./, $value->{path}];
142 path => [split /\./, $value],
152 sub _flattened_search_merge_flesh_wad {
153 my ($old, $new) = @_;
156 $old->{flesh} = $old->{flesh} > $new->{flesh} ? $old->{flesh} : $new->{flesh};
158 $old->{flesh_fields} ||= {};
159 foreach my $key (keys %{$new->{flesh_fields}}) {
160 if ($old->{flesh_fields}{$key}) {
161 # For easy bonus points, somebody could take the following block
162 # and make it use Set::Scalar so it's more semantic, which would
163 # mean a new Evergreen dependency.
165 # The nonobvious point of the following code is to merge the
166 # arrays at $old->{flesh_fields}{$key} and
167 # $new->{flesh_fields}{$key}, treating the arrays as sets.
169 my %hash = map { $_ => 1 } (
170 @{ $old->{flesh_fields}{$key} },
171 @{ $new->{flesh_fields}{$key} }
173 $old->{flesh_fields}{$key} = [ keys(%hash) ];
175 $old->{flesh_fields}{$key} = $new->{flesh_fields}{$key};
180 sub _flattened_search_merge_join_clause {
181 my ($old, $new) = @_;
183 %$old = ( %$old, %$new );
186 sub _flattened_search_expand_filter_column {
187 my ($o, $key, $map) = @_;
190 my $table = $map->{$key}{last_join_alias};
191 my $column = $map->{$key}{path}[-1];
194 $table = "+" . $table;
197 $o->{$table}{$column} = $o->{$key};
200 return $o->{$table}{$column};
201 } else { # field must be on core class
202 if ($column ne $key) {
203 $o->{$column} = $o->{$key};
206 return $o->{$column};
213 sub _flattened_search_recursively_apply_map_to_filter {
214 my ($o, $map, $state) = @_;
218 if (ref $o eq "HASH") {
219 foreach my $key (keys %$o) {
220 # XXX this business about "in_expr" may prove inadequate, but it's
221 # intended to avoid trying to map things like "between" in
223 # {"somecolumn": {"between": [1,10]}}
224 # and to that extent, it works.
226 if (not $state->{in_expr} and $key =~ /^[a-z]/) {
227 $state->{in_expr} = 1;
229 _flattened_search_recursively_apply_map_to_filter(
230 _flattened_search_expand_filter_column($o, $key, $map),
234 $state->{in_expr} = 0;
236 _flattened_search_recursively_apply_map_to_filter(
237 $o->{$key}, $map, $state
241 } elsif (ref $o eq "ARRAY") {
242 _flattened_search_recursively_apply_map_to_filter(
245 } # else scalar, nothing to do?
248 # returns a normalized version of the map, and the jffolo (see below)
250 my ($hint, $map) = @_;
252 $map = { %$map }; # clone map, to work on new copy
254 my $jffolo = { # jffolo: join/flesh/flesh_fields/order_by/limit/offset
258 foreach my $k (keys %$map) {
259 my $column = $map->{$k} =
260 _flattened_search_normalize_map_column($map->{$k});
262 # For display columns, we'll need fleshing.
263 if ($column->{display}) {
264 _flattened_search_merge_flesh_wad(
266 _flattened_search_single_flesh_wad($hint, $column->{path})
270 # For filter or sort columns, we'll need joining.
271 if ($column->{filter} or $column->{sort}) {
272 my ($clause, $last_join_alias) =
273 _flattened_search_single_join_clause($k,$hint,$column->{path});
275 $map->{$k}{last_join_alias} = $last_join_alias;
276 _flattened_search_merge_join_clause($jffolo->{join}, $clause);
280 return ($map, $jffolo);
283 # return a filter clause for PCRUD or cstore, by processing the supplied
284 # simplifed $where clause using $map.
286 my ($map, $where) = @_;
288 my $filter = {%$where};
290 _flattened_search_recursively_apply_map_to_filter($filter, $map);
295 # Return a jffolo with sort/limit/offset from the simplified sort hash (slo)
296 # mixed in. limit and offset are copied as-is. sort is translated into
297 # an order_by that calls simplified column named by their real names by checking
300 my ($core_hint, $map, $jffolo, $slo) = @_;
302 $jffolo = { %$jffolo }; # clone
303 $slo = { %$slo }; # clone
305 $jffolo->{limit} = $slo->{limit} if exists $slo->{limit};
306 $jffolo->{offset} = $slo->{offset} if exists $slo->{offset};
308 return $jffolo unless $slo->{sort};
310 # The slo has a special format for 'sort' that gives callers what they
311 # need, but isn't as flexible as json_query's 'order_by'.
313 # "sort": [{"column1": "asc"}, {"column2": "desc"}]
315 # "sort": ["column1", {"column2": "desc"}]
317 # "sort": {"onlycolumn": "asc"}
319 # "sort": "onlycolumn"
321 $jffolo->{order_by} = [];
323 # coerce from optional simpler format (see comment blob above)
324 $slo->{sort} = [ $slo->{sort} ] unless ref $slo->{sort} eq "ARRAY";
326 foreach my $exp (@{ $slo->{sort} }) {
327 $exp = { $exp => "asc" } unless ref $exp;
329 # XXX By assuming that each sort expression is (at most) a single
330 # key/value pair, we preclude the ability to use transforms and the
333 my ($key) = keys(%$exp);
336 my $class = $map->{$key}{last_join_alias} || $core_hint;
337 push @{ $jffolo->{order_by} }, {
339 field => $map->{$key}{path}[-1],
340 direction => $exp->{$key}
344 # If the key wasn't defined in the map, we'll leave it out of our
351 # Given a map and a fieldmapper object, return a flat representation as
352 # specified by the map's display fields
354 my ($map, $fmobj) = @_;
356 if (not ref $fmobj) {
357 throw OpenSRF::EX::ERROR(
358 "process_result() was passed an inappropriate second argument"
364 while (my ($key, $mapping) = each %$map) {
365 next unless $mapping->{display};
367 my @path = @{ $mapping->{path} };
368 my $field = pop @path;
371 while (my $step = shift @path) {
372 $objs = [ map { $_->$step } @$objs ];
373 last unless ref $$objs[0];
376 # We can get arrays of values be either:
377 # - ending on a $field within a has_many reltype
378 # - passing through a path that is a has_many reltype
379 if (@$objs > 1 or ref $$objs[0] eq 'ARRAY') {
380 $flatrow->{$key} = [];
382 push @{ $flatrow->{$key} }, extract_field_value( $o, $field );
385 $flatrow->{$key} = extract_field_value( $$objs[0], $field );
392 sub extract_field_value {
396 if (ref $obj eq 'ARRAY') {
397 # has_many links return arrays
398 return ( map {$_->$field} @$obj );
400 return ref $obj ? $obj->$field : undef;