]>
Commit | Line | Data |
---|---|---|
f5b7b326 TC |
1 | package BSE::MetaMeta; |
2 | use strict; | |
3 | use Carp qw(confess); | |
4 | use Image::Size; | |
c29434c9 | 5 | use Fcntl ':seek'; |
f5b7b326 | 6 | |
b4c02bf9 | 7 | our $VERSION = "1.005"; |
f5b7b326 | 8 | |
4029e8ab TC |
9 | =head1 NAME |
10 | ||
11 | BSE::MetaMeta - information about metadata. | |
12 | ||
13 | =head1 SYNOPSIS | |
14 | ||
15 | my @metainfo = $class->all_metametadata; | |
16 | ... | |
17 | ||
18 | =head1 INSTANCE METHODS | |
19 | ||
20 | =over | |
21 | ||
22 | =cut | |
23 | ||
24 | my %meta_rules; | |
f5b7b326 TC |
25 | |
26 | my %rule_map = | |
27 | ( | |
4029e8ab | 28 | image => "image", |
f5b7b326 TC |
29 | integer => "integer", |
30 | string => "dh_one_line", | |
4029e8ab | 31 | real => "real", |
f5b7b326 TC |
32 | enum => "meta_enum", # generated |
33 | ); | |
34 | ||
4029e8ab TC |
35 | my %field_defs = |
36 | ( | |
37 | image => | |
38 | { | |
c29434c9 | 39 | type => "image", |
4029e8ab TC |
40 | htmltype => "file", |
41 | }, | |
42 | string => | |
43 | { | |
44 | htmltype => "text", | |
45 | width => 60, | |
46 | }, | |
47 | text => | |
48 | { | |
49 | htmltype => "textarea", | |
50 | width => 60, | |
51 | height => 20, | |
52 | }, | |
53 | integer => | |
54 | { | |
55 | htmltype => "text", | |
56 | width => 8, | |
57 | }, | |
58 | real => | |
59 | { | |
60 | htmltype => "text", | |
61 | width => 10, | |
62 | }, | |
63 | enum => | |
64 | { | |
65 | htmltype => "select", | |
66 | }, | |
67 | ); | |
f5b7b326 | 68 | |
4029e8ab | 69 | =item name |
f5b7b326 | 70 | |
4029e8ab | 71 | The field name of the metadata. |
f5b7b326 | 72 | |
4029e8ab | 73 | =cut |
f5b7b326 | 74 | |
4029e8ab | 75 | sub name { $_[0]{name} } |
f5b7b326 | 76 | |
4029e8ab | 77 | =item type |
f5b7b326 | 78 | |
4029e8ab | 79 | The type of the metadata. |
f5b7b326 | 80 | |
4029e8ab | 81 | =cut |
f5b7b326 TC |
82 | |
83 | sub type { $_[0]{type} } | |
84 | ||
4029e8ab TC |
85 | =item title |
86 | ||
87 | The display name of the metadata. | |
88 | ||
89 | =cut | |
90 | ||
f5b7b326 TC |
91 | sub title { $_[0]{title} } |
92 | ||
4029e8ab TC |
93 | =item rules |
94 | ||
95 | The validation rules for the metadata. | |
96 | ||
97 | =cut | |
98 | ||
f5b7b326 TC |
99 | sub rules { @{$_[0]{rules}} } |
100 | ||
4029e8ab TC |
101 | =item values |
102 | ||
103 | The permitted values for the metadata for enum types. | |
104 | ||
105 | =cut | |
106 | ||
f5b7b326 TC |
107 | sub values { @{$_[0]{values}} } |
108 | ||
4029e8ab TC |
109 | =item labels |
110 | ||
111 | The display labels as a list. | |
112 | ||
113 | =cut | |
114 | ||
f5b7b326 TC |
115 | sub labels { @{$_[0]{labels}} } |
116 | ||
4029e8ab TC |
117 | =item htmltype |
118 | ||
119 | How to display this field. May be ignored depending on C<type>. | |
120 | ||
121 | =cut | |
122 | ||
123 | sub htmltype { $_[0]{htmltype} } | |
124 | ||
da301b75 TC |
125 | =item fieldtype |
126 | ||
127 | How to parse this field. May be ignored depending on type. | |
128 | ||
129 | =cut | |
130 | ||
131 | sub fieldtype { $_[0]{fieldtype} } | |
132 | ||
4029e8ab TC |
133 | =item width |
134 | ||
135 | Display width. May be ignored depending on C<type>. | |
136 | ||
137 | =cut | |
138 | ||
139 | sub width { $_[0]{width} } | |
140 | ||
141 | =item height | |
142 | ||
143 | Display height. May be ignored depending on C<type>. | |
144 | ||
145 | =cut | |
146 | ||
147 | sub height { $_[0]{height} } | |
148 | ||
149 | =item ro | |
150 | ||
151 | Whether this field is read-only. | |
152 | ||
153 | =cut | |
154 | ||
f5b7b326 TC |
155 | sub ro { $_[0]{ro} } |
156 | ||
4029e8ab TC |
157 | =item unit |
158 | ||
159 | Unit of measurement of this field (for display only) | |
160 | ||
161 | =cut | |
162 | ||
f5b7b326 TC |
163 | sub unit { $_[0]{unit} } |
164 | ||
4029e8ab TC |
165 | =item is_text |
166 | ||
167 | True if this is representable as text. | |
168 | ||
169 | =cut | |
170 | ||
f5b7b326 TC |
171 | sub is_text { |
172 | $_[0]{type} ne "image"; | |
173 | } | |
174 | ||
4029e8ab TC |
175 | =item cond |
176 | ||
177 | True if the field should be prompted for if not present. | |
178 | ||
179 | =cut | |
180 | ||
f5b7b326 TC |
181 | sub cond { |
182 | my ($self, $file) = @_; | |
183 | ||
184 | return $self->{cond}->($file); | |
185 | } | |
186 | ||
4029e8ab TC |
187 | =item field |
188 | ||
189 | Return a hash suitable as the validation parameter for the field (and | |
190 | for template field formatting). | |
191 | ||
192 | =cut | |
193 | ||
194 | sub field { | |
195 | my ($self) = @_; | |
196 | ||
197 | my %field = | |
198 | ( | |
4029e8ab TC |
199 | description => scalar $self->title, |
200 | units => scalar $self->unit, | |
c29434c9 | 201 | rules => [ $self->rules ], |
da301b75 | 202 | rawtype => scalar $self->type, |
4029e8ab | 203 | htmltype => scalar $self->htmltype, |
da301b75 TC |
204 | type => scalar $self->fieldtype, |
205 | width => scalar $self->width, | |
206 | height => scalar $self->height, | |
4029e8ab | 207 | ); |
da301b75 TC |
208 | my $defs = $field_defs{$self->type}; |
209 | for my $key (keys %$defs) { | |
210 | defined $field{$key} or $field{$key} = $defs->{$key}; | |
211 | } | |
4029e8ab TC |
212 | if ($self->type =~ /^(?:multi)?enum$/) { |
213 | my $values = [ $self->values ]; | |
214 | my $labels = [ $self->labels ]; | |
215 | my @values = map | |
216 | +{ id => $values->[$_], label => $labels->[$_] }, | |
217 | 0 .. $#$values; | |
218 | $field{select} = | |
219 | { | |
220 | id => "id", | |
221 | label => "label", | |
222 | values => \@values, | |
223 | }; | |
224 | } | |
da301b75 | 225 | my %fields = ( $self->name => \%field ); |
4029e8ab | 226 | |
da301b75 TC |
227 | require BSE::Validate; |
228 | my $configured = | |
229 | BSE::Validate::bse_configure_fields({ $self->name => \%fields }, BSE::Cfg->single, | |
230 | $self->validation_section); | |
231 | ||
232 | return $fields{$self->name}; | |
4029e8ab TC |
233 | } |
234 | ||
b4c02bf9 | 235 | =item validate |
4029e8ab | 236 | |
b4c02bf9 | 237 | Validate a meta data item. |
4029e8ab TC |
238 | |
239 | =cut | |
240 | ||
f5b7b326 TC |
241 | sub validate { |
242 | my ($self, %opts) = @_; | |
243 | ||
244 | my $value = delete $opts{value}; | |
245 | defined $value | |
246 | or confess "value not supplied\n"; | |
247 | my $rerror = delete $opts{error} | |
248 | or confess "error ref not supplied\n"; | |
4029e8ab | 249 | my $section = $self->validation_section; |
f5b7b326 TC |
250 | |
251 | # kind of clumsy | |
252 | require DevHelp::Validate; | |
253 | my @field_rules = $self->rules; | |
254 | $rule_map{$self->type} && unshift @field_rules, $rule_map{$self->type}; | |
255 | my %values = | |
256 | ( | |
257 | value => $value | |
258 | ); | |
259 | my %fields = | |
260 | ( | |
261 | value => | |
262 | { | |
263 | rules => \@field_rules, | |
264 | description => $self->title, | |
265 | }, | |
266 | ); | |
267 | my %rules = %meta_rules; | |
268 | if ($self->type eq "enum") { | |
269 | $rules{meta_enum} = | |
270 | { | |
271 | match => "^(?:" . join("|", map quotemeta, $self->values) . ")\\z", | |
272 | error => '$n must be one of ' . join(", ", $self->values), | |
273 | }; | |
274 | } | |
275 | ||
276 | my $val = DevHelp::Validate::Hash->new | |
277 | ( | |
278 | fields => \%fields, | |
279 | rules => \%rules, | |
280 | cfg => $self->{cfg}, | |
4029e8ab | 281 | section => $section, |
f5b7b326 TC |
282 | ); |
283 | my %errors; | |
284 | $val->validate(\%values, \%errors); | |
285 | if (keys %errors) { | |
286 | $$rerror = $errors{value}; | |
287 | return; | |
288 | } | |
289 | ||
290 | return 1; | |
291 | } | |
292 | ||
b4c02bf9 | 293 | =item metanames |
4029e8ab | 294 | |
b4c02bf9 | 295 | List of form fields that are read for the meta item. |
4029e8ab TC |
296 | |
297 | =cut | |
298 | ||
f5b7b326 TC |
299 | sub metanames { |
300 | my ($self) = @_; | |
301 | ||
302 | if ($self->type eq 'image') { | |
c29434c9 | 303 | return ( $self->data_name, $self->width_name, $self->height_name, $self->display_name ); |
f5b7b326 TC |
304 | } |
305 | else { | |
306 | return $self->name; | |
307 | } | |
308 | } | |
309 | ||
4029e8ab TC |
310 | =item data_name |
311 | ||
312 | The field name of the metadata. | |
313 | ||
314 | =cut | |
315 | ||
f5b7b326 TC |
316 | sub data_name { |
317 | $_[0]{data_name} | |
318 | } | |
319 | ||
4029e8ab TC |
320 | =item width_name |
321 | ||
322 | Where width information is stored for this image | |
323 | ||
324 | =cut | |
325 | ||
f5b7b326 TC |
326 | sub width_name { |
327 | $_[0]{width_name} | |
328 | } | |
329 | ||
4029e8ab TC |
330 | =item height_name |
331 | ||
332 | Where height information is stored for this image. | |
333 | ||
334 | =cut | |
335 | ||
f5b7b326 TC |
336 | sub height_name { |
337 | $_[0]{height_name} | |
338 | } | |
339 | ||
4029e8ab TC |
340 | =item display_name |
341 | ||
342 | Where the original filename is stored for the image. | |
343 | ||
344 | =cut | |
345 | ||
346 | sub display_name { | |
347 | $_[0]{display_name} | |
348 | } | |
349 | ||
b4c02bf9 TC |
350 | =back |
351 | ||
4029e8ab TC |
352 | =head1 CLASS METHODS |
353 | ||
354 | =over | |
355 | ||
356 | =item new | |
357 | ||
358 | =cut | |
359 | ||
360 | sub new { | |
361 | my $class = shift; | |
362 | my %opts = | |
363 | ( | |
364 | rules => '', | |
365 | ro => 0, | |
366 | values => [], | |
367 | cond => "1", | |
368 | type => "string", | |
369 | unit => '', | |
370 | help => '', | |
da301b75 | 371 | fieldtype => "", |
4029e8ab TC |
372 | @_ |
373 | ); | |
374 | ||
da301b75 TC |
375 | |
376 | ||
4029e8ab TC |
377 | $opts{cfg} && $opts{cfg}->can("entry") |
378 | or confess "Missing or invalid cfg parameter"; | |
379 | $opts{name} | |
380 | or confess "Missing name parameter"; | |
381 | $opts{name} =~ /^[A-Za-z_][A-Za-z0-9_-]*$/ | |
382 | or confess "Invalid metadata name parameter"; | |
383 | ||
384 | $field_defs{$opts{type}} | |
385 | or confess "Unknown metadata type '$opts{type}' for field '$opts{name}'"; | |
386 | ||
387 | my $name = $opts{name}; | |
388 | for my $subkey (qw/data width height display/) { | |
389 | my $key = $subkey . "_name"; | |
390 | defined $opts{$key} or $opts{$key} = $name . "_" . $subkey; | |
391 | } | |
392 | $opts{title} ||= $name; | |
393 | ||
394 | if ($opts{type} =~ /^(?:multi)?enum/) { | |
395 | if ($opts{values}) { | |
396 | unless (ref $opts{values}) { | |
397 | $opts{values} = [ split /;/, $opts{values} ]; | |
398 | } | |
399 | @{$opts{values}} | |
400 | or confess "$opts{name} has enum type but no values"; | |
401 | } | |
402 | else { | |
403 | confess "$opts{name} has enum type but no values"; | |
404 | } | |
405 | ||
406 | if ($opts{labels}) { | |
407 | unless (ref $opts{labels}) { | |
408 | $opts{labels} = [ split /;/, $opts{labels} ]; | |
409 | } | |
410 | @{$opts{labels}} | |
411 | or confess "$opts{name} has enum type but no labels"; | |
412 | } | |
413 | else { | |
414 | $opts{labels} = $opts{values}; | |
415 | } | |
416 | } | |
417 | ||
418 | $opts{htmltype} ||= $field_defs{$opts{type}}{htmltype}; | |
419 | ||
420 | ref $opts{rules} or $opts{rules} = [ split /[,;]/, $opts{rules} ]; | |
c29434c9 | 421 | unshift @{$opts{rules}}, $rule_map{$opts{type}}; |
4029e8ab TC |
422 | |
423 | if ($opts{cond}) { | |
424 | my $code = $opts{cond}; | |
425 | $opts{cond} = eval 'sub { my $file = shift; my $obj = $file; ' . $code . ' }' | |
426 | or die "Cannot compile condition code <$code> for $opts{name}: $@"; | |
427 | } | |
428 | else { | |
429 | $opts{cond} = sub { 1 }; | |
430 | } | |
431 | ||
432 | bless \%opts, $class; | |
433 | } | |
434 | ||
f5b7b326 | 435 | sub keys { |
da301b75 | 436 | qw/title help rules ro values labels type data_name width_name height_name cond unit htmltype width height fieldtype/; |
f5b7b326 TC |
437 | } |
438 | ||
439 | sub retrieve { | |
c29434c9 | 440 | my ($class, $req, $owner, $errors, %opts) = @_; |
f5b7b326 | 441 | |
c29434c9 | 442 | my $api = $opts{api}; |
f5b7b326 TC |
443 | my @meta; |
444 | my @meta_delete; | |
445 | my $cgi = $req->cgi; | |
446 | my @metafields = grep !$_->ro, $owner->metafields($req->cfg); | |
447 | my %current_meta = map { $_ => 1 } $owner->metanames; | |
448 | for my $meta (@metafields) { | |
449 | my $name = $meta->name; | |
450 | my $cgi_name = "meta_$name"; | |
451 | if ($cgi->param("delete_$cgi_name")) { | |
452 | for my $metaname ($meta->metanames) { | |
453 | push @meta_delete, $metaname | |
454 | if $current_meta{$metaname}; | |
455 | } | |
456 | } | |
457 | else { | |
458 | my $new; | |
c29434c9 TC |
459 | my %fields = |
460 | ( | |
461 | $cgi_name => $meta->field, | |
462 | ); | |
463 | if ($req->validate(fields => \%fields, | |
464 | rules => \%meta_rules, | |
465 | errors => $errors)) { | |
466 | my $values = $req->cgi_fields | |
467 | ( | |
468 | fields => \%fields, | |
469 | api => $api, | |
470 | ); | |
471 | my $value = $values->{$cgi_name}; | |
472 | if ($meta->is_text) { | |
473 | if (defined $value && | |
474 | ($value =~ /\S/ || $current_meta{$meta->name})) { | |
475 | utf8::encode($value); | |
f5b7b326 TC |
476 | push @meta, |
477 | { | |
478 | name => $name, | |
479 | value => $value, | |
480 | }; | |
481 | } | |
f5b7b326 | 482 | } |
c29434c9 TC |
483 | else { |
484 | if ($value) { | |
485 | my $up = $value->{fh}; | |
486 | binmode $up; | |
487 | seek $up, 0, SEEK_SET; | |
488 | my $data = do { local $/; <$up> }; | |
489 | my ($width, $height, $type) = imgsize(\$data); | |
490 | ||
491 | if ($width && $height) { | |
492 | push @meta, | |
493 | ( | |
494 | { | |
495 | name => $meta->data_name, | |
496 | value => $data, | |
497 | content_type => "image/\L$type", | |
498 | }, | |
499 | { | |
500 | name => $meta->width_name, | |
501 | value => $width, | |
502 | }, | |
503 | { | |
504 | name => $meta->height_name, | |
505 | value => $height, | |
506 | }, | |
507 | { | |
508 | name => $meta->display_name, | |
509 | value => "" . $value->{filename}, | |
510 | }, | |
511 | ); | |
512 | } | |
f5b7b326 TC |
513 | } |
514 | } | |
515 | } | |
516 | } | |
517 | } | |
518 | ||
519 | return { meta => \@meta, delete => \@meta_delete }; | |
520 | } | |
521 | ||
522 | sub save { | |
523 | my ($class, $owner, $meta) = @_; | |
524 | ||
c29434c9 TC |
525 | for my $meta_delete (@{$meta->{delete}}, map $_->{name}, @{$meta->{meta}}) { |
526 | $owner->delete_meta_by_name($meta_delete); | |
f5b7b326 TC |
527 | } |
528 | for my $meta (@{$meta->{meta}}) { | |
529 | $owner->add_meta(%$meta, appdata => 1); | |
530 | } | |
531 | ||
532 | 1; | |
533 | } | |
534 | ||
4029e8ab TC |
535 | sub all_metametadata { |
536 | my ($class, $cfg) = @_; | |
537 | ||
538 | $cfg ||= BSE::Cfg->new; | |
539 | ||
540 | my @metafields; | |
541 | my @keys = $cfg->orderCS($class->fields_section); | |
542 | for my $name (@keys) { | |
543 | my %opts = ( name => $name ); | |
544 | my $section = $class->name_section($name); | |
545 | for my $key ($class->keys) { | |
546 | my $value = $cfg->entry($section, $key); | |
547 | if (defined $value) { | |
548 | $opts{$key} = $value; | |
549 | } | |
550 | } | |
551 | push @metafields, $class->new(%opts, cfg => $cfg); | |
552 | } | |
553 | ||
554 | return @metafields; | |
555 | } | |
556 | ||
f5b7b326 | 557 | 1; |
4029e8ab TC |
558 | |
559 | =back | |
560 | ||
561 | =cut |