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") {
96 } elsif ($reltype eq "has_many" or $reltype eq "might_have") {
103 throw OpenSRF::EX::ERROR("unexpected reltype for link $piece");
107 $last_join->{join}{$alias} = $new_join;
109 $core_join->{$alias} = $new_join;
112 $last_ident = $class->Identity;
113 $last_join = $new_join;
115 throw new OpenSRF::EX::ERROR("no link '$piece' on $class");
119 return ($core_join, $alias);
122 # When $value is a string (short form of a column definition), it is assumed to
123 # be a dot-delimited path. This will be normalized into a hash (long form)
124 # containing and path key, whose value will be made into an array, and true
125 # values for sort/filter/display.
127 # When $value is already a hash (long form), just make an array of the path key
128 # and explicity set any sort/filter/display values not present to 0.
130 sub _flattened_search_normalize_map_column {
133 if (ref $value eq "HASH") {
134 foreach (qw/sort filter display/) {
135 $value->{$_} = 0 unless exists $value->{$_};
137 $value->{path} = [split /\./, $value->{path}];
140 path => [split /\./, $value],
150 sub _flattened_search_merge_flesh_wad {
151 my ($old, $new) = @_;
154 $old->{flesh} = $old->{flesh} > $new->{flesh} ? $old->{flesh} : $new->{flesh};
156 $old->{flesh_fields} ||= {};
157 foreach my $key (keys %{$new->{flesh_fields}}) {
158 if ($old->{flesh_fields}{$key}) {
159 # For easy bonus points, somebody could take the following block
160 # and make it use Set::Scalar so it's more semantic, which would
161 # mean a new Evergreen dependency.
163 # The nonobvious point of the following code is to merge the
164 # arrays at $old->{flesh_fields}{$key} and
165 # $new->{flesh_fields}{$key}, treating the arrays as sets.
167 my %hash = map { $_ => 1 } (
168 @{ $old->{flesh_fields}{$key} },
169 @{ $new->{flesh_fields}{$key} }
171 $old->{flesh_fields}{$key} = [ keys(%hash) ];
173 $old->{flesh_fields}{$key} = $new->{flesh_fields}{$key};
178 sub _flattened_search_merge_join_clause {
179 my ($old, $new) = @_;
181 %$old = ( %$old, %$new );
184 sub _flattened_search_expand_filter_column {
185 my ($o, $key, $map) = @_;
188 my $table = $map->{$key}{last_join_alias};
189 my $column = $map->{$key}{path}[-1];
192 $table = "+" . $table;
195 $o->{$table}{$column} = $o->{$key};
198 return $o->{$table}{$column};
199 } else { # field must be on core class
200 if ($column ne $key) {
201 $o->{$column} = $o->{$key};
204 return $o->{$column};
211 sub _flattened_search_recursively_apply_map_to_filter {
212 my ($o, $map, $state) = @_;
216 if (ref $o eq "HASH") {
217 foreach my $key (keys %$o) {
218 # XXX this business about "in_expr" may prove inadequate, but it's
219 # intended to avoid trying to map things like "between" in
221 # {"somecolumn": {"between": [1,10]}}
222 # and to that extent, it works.
224 if (not $state->{in_expr} and $key =~ /^[a-z]/) {
225 $state->{in_expr} = 1;
227 _flattened_search_recursively_apply_map_to_filter(
228 _flattened_search_expand_filter_column($o, $key, $map),
232 $state->{in_expr} = 0;
234 _flattened_search_recursively_apply_map_to_filter(
235 $o->{$key}, $map, $state
239 } elsif (ref $o eq "ARRAY") {
240 _flattened_search_recursively_apply_map_to_filter(
243 } # else scalar, nothing to do?
246 # returns a normalized version of the map, and the jffolo (see below)
248 my ($hint, $map) = @_;
250 $map = { %$map }; # clone map, to work on new copy
252 my $jffolo = { # jffolo: join/flesh/flesh_fields/order_by/limit/offset
256 foreach my $k (keys %$map) {
257 my $column = $map->{$k} =
258 _flattened_search_normalize_map_column($map->{$k});
260 # For display columns, we'll need fleshing.
261 if ($column->{display}) {
262 _flattened_search_merge_flesh_wad(
264 _flattened_search_single_flesh_wad($hint, $column->{path})
268 # For filter or sort columns, we'll need joining.
269 if ($column->{filter} or $column->{sort}) {
270 my ($clause, $last_join_alias) =
271 _flattened_search_single_join_clause($k,$hint,$column->{path});
273 $map->{$k}{last_join_alias} = $last_join_alias;
274 _flattened_search_merge_join_clause($jffolo->{join}, $clause);
278 return ($map, $jffolo);
281 # return a filter clause for PCRUD or cstore, by processing the supplied
282 # simplifed $where clause using $map.
284 my ($map, $where) = @_;
286 my $filter = {%$where};
288 _flattened_search_recursively_apply_map_to_filter($filter, $map);
293 # Return a jffolo with sort/limit/offset from the simplified sort hash (slo)
294 # mixed in. limit and offset are copied as-is. sort is translated into
295 # an order_by that calls simplified column named by their real names by checking
298 my ($core_hint, $map, $jffolo, $slo) = @_;
300 $jffolo = { %$jffolo }; # clone
301 $slo = { %$slo }; # clone
303 $jffolo->{limit} = $slo->{limit} if exists $slo->{limit};
304 $jffolo->{offset} = $slo->{offset} if exists $slo->{offset};
306 return $jffolo unless $slo->{sort};
308 # The slo has a special format for 'sort' that gives callers what they
309 # need, but isn't as flexible as json_query's 'order_by'.
311 # "sort": [{"column1": "asc"}, {"column2": "desc"}]
313 # "sort": ["column1", {"column2": "desc"}]
315 # "sort": {"onlycolumn": "asc"}
317 # "sort": "onlycolumn"
319 $jffolo->{order_by} = [];
321 # coerce from optional simpler format (see comment blob above)
322 $slo->{sort} = [ $slo->{sort} ] unless ref $slo->{sort} eq "ARRAY";
324 foreach my $exp (@{ $slo->{sort} }) {
325 $exp = { $exp => "asc" } unless ref $exp;
327 # XXX By assuming that each sort expression is (at most) a single
328 # key/value pair, we preclude the ability to use transforms and the
331 my ($key) = keys(%$exp);
334 my $class = $map->{$key}{last_join_alias} || $core_hint;
335 push @{ $jffolo->{order_by} }, {
337 field => $map->{$key}{path}[-1],
338 direction => $exp->{$key}
342 # If the key wasn't defined in the map, we'll leave it out of our
349 # Given a map and a fieldmapper object, return a flat representation as
350 # specified by the map's display fields
352 my ($map, $fmobj) = @_;
354 if (not ref $fmobj) {
355 throw OpenSRF::EX::ERROR(
356 "process_result() was passed an inappropriate second argument"
362 while (my ($key, $mapping) = each %$map) {
363 next unless $mapping->{display};
365 my @path = @{ $mapping->{path} };
366 my $field = pop @path;
369 while (my $step = shift @path) {
370 $objs = [ map { $_->$step } @$objs ];
371 last unless ref $$objs[0];
374 # We can get arrays of values be either:
375 # - ending on a $field within a has_many reltype
376 # - passing through a path that is a has_many reltype
377 if (@$objs > 1 or ref $$objs[0] eq 'ARRAY') {
378 $flatrow->{$key} = [];
380 push @{ $flatrow->{$key} }, extract_field_value( $o, $field );
383 $flatrow->{$key} = extract_field_value( $$objs[0], $field );
390 sub extract_field_value {
394 if (ref $obj eq 'ARRAY') {
395 # has_many links return arrays
396 return ( map {$_->$field} @$obj );
398 return ref $obj ? $obj->$field : undef;