iiif_print 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +16 -0
  4. data/.github/workflows/build-lint-test-action.yaml +4 -5
  5. data/.gitignore +5 -4
  6. data/.rubocop.yml +1 -0
  7. data/.solargraph.yml +19 -0
  8. data/Gemfile.lock +1025 -0
  9. data/README.md +98 -9
  10. data/Rakefile +6 -0
  11. data/app/actors/iiif_print/actors/cleanup_file_sets_actor_decorator.rb +24 -0
  12. data/app/actors/iiif_print/actors/file_set_actor_decorator.rb +30 -28
  13. data/app/controllers/iiif_print/split_pdfs_controller.rb +38 -0
  14. data/app/helpers/iiif_print/iiif_helper_decorator.rb +32 -0
  15. data/app/helpers/iiif_print/iiif_print_helper_behavior.rb +23 -0
  16. data/app/helpers/iiif_print_helper.rb +0 -20
  17. data/app/indexers/concerns/iiif_print/child_indexer.rb +9 -3
  18. data/app/indexers/concerns/iiif_print/file_set_indexer.rb +17 -4
  19. data/app/models/concerns/iiif_print/set_child_flag.rb +9 -0
  20. data/app/models/concerns/iiif_print/solr/document.rb +14 -0
  21. data/app/models/iiif_print/iiif_search_decorator.rb +35 -0
  22. data/app/models/iiif_print/iiif_search_response_decorator.rb +25 -2
  23. data/app/models/iiif_print/pending_relationship.rb +3 -0
  24. data/app/presenters/iiif_print/iiif_manifest_presenter_behavior.rb +120 -0
  25. data/app/presenters/iiif_print/iiif_manifest_presenter_factory_behavior.rb +1 -1
  26. data/app/presenters/iiif_print/work_show_presenter_decorator.rb +19 -10
  27. data/app/search_builders/concerns/iiif_print/allinson_flex_fields.rb +15 -0
  28. data/app/search_builders/concerns/iiif_print/highlight_search_params.rb +2 -1
  29. data/app/services/iiif_print/derivative_rodeo_service.rb +382 -0
  30. data/app/services/iiif_print/manifest_builder_service_behavior.rb +88 -31
  31. data/app/services/iiif_print/pluggable_derivative_service.rb +3 -9
  32. data/app/views/catalog/_index_header_list_default.html.erb +13 -0
  33. data/app/views/hyrax/base/_representative_media.html.erb +4 -3
  34. data/app/views/hyrax/base/iiif_viewers/_universal_viewer.html.erb +1 -1
  35. data/app/views/hyrax/file_sets/_actions.html.erb +2 -1
  36. data/app/views/hyrax/file_sets/_show_actions.html.erb +24 -0
  37. data/config/locales/iiif_print.en.yml +4 -0
  38. data/config/routes.rb +3 -0
  39. data/db/migrate/20231110163052_add_model_details_to_iiif_print_pending_relationships.rb +7 -0
  40. data/docker-compose.yml +2 -2
  41. data/iiif_print.gemspec +10 -9
  42. data/lib/generators/iiif_print/install_generator.rb +21 -1
  43. data/lib/generators/iiif_print/templates/config/initializers/iiif_print.rb +11 -4
  44. data/lib/generators/iiif_print/templates/helpers/iiif_print_helper.rb +5 -0
  45. data/lib/iiif_print/base_derivative_service.rb +2 -1
  46. data/lib/iiif_print/blacklight_iiif_search/annotation_decorator.rb +57 -5
  47. data/lib/iiif_print/catalog_search_builder.rb +5 -1
  48. data/lib/iiif_print/configuration.rb +145 -8
  49. data/lib/iiif_print/data/fileset_helper.rb +1 -1
  50. data/lib/iiif_print/data/work_derivatives.rb +3 -3
  51. data/lib/iiif_print/engine.rb +7 -13
  52. data/lib/iiif_print/errors.rb +18 -0
  53. data/lib/iiif_print/homepage_search_builder.rb +17 -0
  54. data/lib/iiif_print/image_tool.rb +12 -8
  55. data/lib/iiif_print/jobs/child_works_from_pdf_job.rb +74 -33
  56. data/lib/iiif_print/jobs/create_relationships_job.rb +80 -31
  57. data/lib/iiif_print/jobs/request_split_pdf_job.rb +31 -0
  58. data/lib/iiif_print/lineage_service.rb +29 -8
  59. data/lib/iiif_print/metadata.rb +67 -48
  60. data/lib/iiif_print/split_pdfs/base_splitter.rb +142 -0
  61. data/lib/iiif_print/split_pdfs/child_work_creation_from_pdf_service.rb +68 -32
  62. data/lib/iiif_print/split_pdfs/derivative_rodeo_splitter.rb +166 -0
  63. data/lib/iiif_print/split_pdfs/destroy_pdf_child_works_service.rb +33 -0
  64. data/lib/iiif_print/split_pdfs/pages_to_jpgs_splitter.rb +19 -0
  65. data/lib/iiif_print/split_pdfs/pages_to_pngs_splitter.rb +26 -0
  66. data/lib/iiif_print/split_pdfs/pages_to_tiffs_splitter.rb +41 -0
  67. data/lib/iiif_print/split_pdfs/pdf_image_extraction_service.rb +64 -59
  68. data/lib/iiif_print/text_extraction/hocr_reader.rb +7 -3
  69. data/lib/iiif_print/text_extraction/page_ocr.rb +5 -4
  70. data/lib/iiif_print/version.rb +1 -1
  71. data/lib/iiif_print.rb +167 -12
  72. data/lib/samvera/derivatives/configuration.rb +83 -0
  73. data/lib/samvera/derivatives/hyrax.rb +129 -0
  74. data/lib/samvera/derivatives.rb +238 -0
  75. data/spec/factories/newspaper_page_solr_document.rb +9 -1
  76. data/spec/fixtures/authorities/licenses.yml +4 -0
  77. data/spec/fixtures/authorities/rights_statements.yml +4 -0
  78. data/spec/iiif_print/base_derivative_service_spec.rb +20 -3
  79. data/spec/iiif_print/blacklight_iiif_search/annotation_decorator_spec.rb +11 -3
  80. data/spec/iiif_print/catalog_search_builder_spec.rb +1 -1
  81. data/spec/iiif_print/configuration_spec.rb +141 -15
  82. data/spec/iiif_print/jobs/child_works_from_pdf_job_spec.rb +7 -2
  83. data/spec/iiif_print/jobs/create_relationships_job_spec.rb +110 -9
  84. data/spec/iiif_print/lineage_service_spec.rb +1 -1
  85. data/spec/iiif_print/metadata_spec.rb +157 -23
  86. data/spec/iiif_print/split_pdfs/base_splitter_spec.rb +27 -0
  87. data/spec/iiif_print/split_pdfs/derivative_rodeo_splitter_spec.rb +80 -0
  88. data/spec/iiif_print/split_pdfs/destroy_pdf_child_works_service_spec.rb +92 -0
  89. data/spec/iiif_print/split_pdfs/pages_to_jpgs_splitter_spec.rb +22 -0
  90. data/spec/iiif_print/split_pdfs/pages_to_pngs_splitter_spec.rb +18 -0
  91. data/spec/iiif_print/split_pdfs/pages_to_tiffs_splitter_spec.rb +19 -0
  92. data/spec/iiif_print/text_extraction/hocr_reader_spec.rb +2 -2
  93. data/spec/iiif_print_spec.rb +125 -5
  94. data/spec/models/iiif_print/iiif_search_decorator_spec.rb +27 -0
  95. data/spec/presenters/iiif_print/iiif_manifest_presenter_behavior_spec.rb +51 -0
  96. data/spec/samvera/derivatives/configuration_spec.rb +41 -0
  97. data/spec/samvera/derivatives/hyrax_spec.rb +62 -0
  98. data/spec/samvera/derivatives_spec.rb +54 -0
  99. data/spec/services/iiif_print/derivative_rodeo_service_spec.rb +103 -0
  100. data/spec/services/iiif_print/manifest_builder_service_behavior_spec.rb +20 -0
  101. data/spec/services/iiif_print/pluggable_derivative_service_spec.rb +8 -11
  102. data/spec/test_app_templates/lib/generators/test_app_generator.rb +1 -1
  103. data/tasks/copy_authorities_to_test_app.rake +11 -0
  104. data/tasks/iiif_print_dev.rake +4 -4
  105. metadata +123 -35
  106. data/app/helpers/hyrax/iiif_helper.rb +0 -22
  107. data/lib/iiif_print/split_pdfs/pages_into_images_service.rb +0 -130
  108. data/spec/iiif_print/split_pdfs/pages_into_images_service_spec.rb +0 -6
data/lib/iiif_print.rb CHANGED
@@ -17,10 +17,13 @@ require "iiif_print/works_controller_behavior"
17
17
  require "iiif_print/jobs/application_job"
18
18
  require "iiif_print/blacklight_iiif_search/annotation_decorator"
19
19
  require "iiif_print/jobs/child_works_from_pdf_job"
20
- require "iiif_print/jobs/create_relationships_job"
21
- require "iiif_print/split_pdfs/pages_into_images_service"
20
+ require "iiif_print/jobs/request_split_pdf_job"
21
+ require "iiif_print/split_pdfs/base_splitter"
22
22
  require "iiif_print/split_pdfs/child_work_creation_from_pdf_service"
23
+ require "iiif_print/split_pdfs/derivative_rodeo_splitter"
24
+ require "iiif_print/split_pdfs/destroy_pdf_child_works_service"
23
25
 
26
+ # rubocop:disable Metrics/ModuleLength
24
27
  module IiifPrint
25
28
  extend ActiveSupport::Autoload
26
29
  autoload :Configuration
@@ -28,9 +31,10 @@ module IiifPrint
28
31
 
29
32
  ##
30
33
  # @api public
34
+ #
31
35
  # Exposes the IiifPrint configuration.
32
36
  #
33
- # @yield [IiifPrint::Configuration] if a block is passed
37
+ # @yieldparam [IiifPrint::Configuration] config if a block is passed
34
38
  # @return [IiifPrint::Configuration]
35
39
  # @see IiifPrint::Configuration for configuration options
36
40
  def self.config(&block)
@@ -39,28 +43,65 @@ module IiifPrint
39
43
  @config
40
44
  end
41
45
 
46
+ class << self
47
+ delegate :skip_splitting_pdf_files_that_end_with_these_texts, to: :config
48
+ end
49
+
50
+ ##
51
+ # Return the immediate parent of the given :file_set.
52
+ #
53
+ # @param file_set [FileSet]
54
+ # @return [#work?, Hydra::PCDM::Work]
55
+ # @return [NilClass] when no parent is found.
56
+ def self.parent_for(file_set)
57
+ # fallback to Fedora-stored relationships if work's aggregation of
58
+ # file set is not indexed in Solr
59
+ file_set.parent || file_set.member_of.find(&:work?)
60
+ end
61
+
62
+ ##
63
+ # Return the parent's parent of the given :file_set.
64
+ #
65
+ # @param file_set [FileSet]
66
+ # @return [#work?, Hydra::PCDM::Work]
67
+ # @return [NilClass] when no grand parent is found.
68
+ def self.grandparent_for(file_set)
69
+ parent_of_file_set = parent_for(file_set)
70
+ # HACK: This is an assumption about the file_set structure, namely that an image page split from
71
+ # a PDF is part of a file set that is a child of a work that is a child of a single work. That
72
+ # is, it only has one grand parent. Which is a reasonable assumption for IIIF Print but is not
73
+ # valid when extended beyond IIIF Print. That is GenericWork does not have a parent method but
74
+ # does have a parents method.
75
+ parent_of_file_set.try(:parent_works).try(:first) ||
76
+ parent_of_file_set.try(:parents).try(:first) ||
77
+ parent_of_file_set&.member_of&.find(&:work?)
78
+ end
79
+
42
80
  DEFAULT_MODEL_CONFIGURATION = {
43
81
  # Split a PDF into individual page images and create a new child work for each image.
44
82
  pdf_splitter_job: IiifPrint::Jobs::ChildWorksFromPdfJob,
45
- pdf_splitter_service: IiifPrint::SplitPdfs::PagesIntoImagesService,
83
+ pdf_splitter_service: IiifPrint::SplitPdfs::PagesToJpgsSplitter,
46
84
  derivative_service_plugins: [
47
- IiifPrint::JP2DerivativeService,
48
- IiifPrint::PDFDerivativeService,
49
- IiifPrint::TextExtractionDerivativeService,
50
- IiifPrint::TIFFDerivativeService
85
+ IiifPrint::TextExtractionDerivativeService
51
86
  ]
52
87
  }.freeze
53
88
 
54
89
  # This is the record level configuration for PDF split handling.
55
90
  ModelConfig = Struct.new(:pdf_split_child_model, *DEFAULT_MODEL_CONFIGURATION.keys, keyword_init: true)
91
+ private_constant :ModelConfig
56
92
 
57
- # This method is responsible for assisting in the configuration of a "model".
93
+ ##
94
+ # @api public
95
+ # This method is responsible for configuring a model for additional derivative generation.
58
96
  #
59
97
  # @example
60
98
  # class Book < ActiveFedora::Base
61
99
  # include IiifPrint.model_configuration(
62
100
  # pdf_split_child_model: Page,
63
101
  # derivative_service_plugins: [
102
+ # IiifPrint::JP2DerivativeService,
103
+ # IiifPrint::PDFDerivativeService,
104
+ # IiifPrint::TextExtractionDerivativeService,
64
105
  # IiifPrint::TIFFDerivativeService
65
106
  # ]
66
107
  # )
@@ -68,6 +109,18 @@ module IiifPrint
68
109
  #
69
110
  # @param kwargs [Hash<Symbol,Object>] the configuration values that overrides the
70
111
  # DEFAULT_MODEL_CONFIGURATION.
112
+ # @option kwargs [Array<Class>] derivative_service_plugins the various derivatives to run on the
113
+ # "original" files associated with this work. Options include:
114
+ # {IiifPrint::JP2DerivativeService}, {IiifPrint::PDFDerivativeService},
115
+ # {IiifPrint::TextExtractionDerivativeService}, {IiifPrint::TIFFDerivativeService}
116
+ # @option kwargs [Class] pdf_splitter_job responsible for handling the splitting of the original file
117
+ # @option kwargs [Class] pdf_split_child_model when we split the file into pages, what's the child model
118
+ # we want for those pages? Often times this is likely the same model as the parent.
119
+ # @option kwargs [Class] pdf_splitter_service the specific service that splits the PDF. Options are:
120
+ # {IiifPrint::SplitPdfs::PagesToJpgsSplitter},
121
+ # {IiifPrint::SplitPdfs::PagesToTiffsSplitter},
122
+ # {IiifPrint::SplitPdfs::PagesToPngsSplitter},
123
+ # {IiifPrint::SplitPdfs::DerivativeRodeoSplitter}
71
124
  #
72
125
  # @return [Module]
73
126
  #
@@ -107,7 +160,7 @@ module IiifPrint
107
160
  # @see Hyrax::IiifManifestPresenter#manifest_metadata
108
161
  def self.manifest_metadata_for(work:,
109
162
  version: config.default_iiif_manifest_version,
110
- fields: default_fields_for(work),
163
+ fields: defined?(AllinsonFlex) ? fields_for_allinson_flex : default_fields,
111
164
  current_ability:,
112
165
  base_url:)
113
166
  Metadata.build_metadata_for(work: work,
@@ -117,6 +170,11 @@ module IiifPrint
117
170
  base_url: base_url)
118
171
  end
119
172
 
173
+ def self.manifest_metadata_from(work:, presenter:)
174
+ current_ability = presenter.try(:ability) || presenter.try(:current_ability)
175
+ base_url = presenter.try(:base_url) || presenter.try(:request)&.base_url
176
+ IiifPrint.manifest_metadata_for(work: work, current_ability: current_ability, base_url: base_url)
177
+ end
120
178
  # Hash is an arbitrary attribute key/value pairs
121
179
  # Struct is a defined set of attribute "keys". When we favor defined values,
122
180
  # then we are naming the concept and defining the range of potential values.
@@ -124,13 +182,110 @@ module IiifPrint
124
182
 
125
183
  # @api private
126
184
  # @todo Figure out a way to use a custom label, right now it takes it get rendered from the title.
127
- def self.default_fields_for(_work, fields: config.metadata_fields)
185
+ def self.default_fields(fields: config.metadata_fields)
128
186
  fields.map do |field|
129
187
  Field.new(
130
188
  name: field.first,
131
- label: Hyrax::Renderers::AttributeRenderer.new(field, nil).label,
189
+ label: Hyrax::Renderers::AttributeRenderer.new(field.first, nil).label,
132
190
  options: field.last
133
191
  )
134
192
  end
135
193
  end
194
+
195
+ ##
196
+ # @param fields [Array<IiifPrint::Field>]
197
+ def self.fields_for_allinson_flex(fields: allinson_flex_fields, sort_order: IiifPrint.config.iiif_metadata_field_presentation_order)
198
+ fields = sort_af_fields!(fields, sort_order: sort_order)
199
+ fields.each_with_object({}) do |field, hash|
200
+ # filters out admin_only fields
201
+ next if field.indexing&.include?('admin_only')
202
+
203
+ # WARNING: This is assuming A LOT
204
+ # This is taking the Allinson Flex fields that have the same name and only
205
+ # using the first one while discarding the rest. There currently no way to
206
+ # controller which one(s) are discarded but this fits for the moment.
207
+ next if hash.key?(field.name)
208
+
209
+ # currently only supports the faceted option
210
+ # Why the `render_as:`? This was originally derived from Hyku default attributes
211
+ # @see https://github.com/samvera/hyku/blob/c702844de4c003eaa88eb5a7514c7a1eae1b289e/app/views/hyrax/base/_attribute_rows.html.erb#L3
212
+ hash[field.name] = Field.new(
213
+ name: field.name,
214
+ label: field.value,
215
+ options: field.indexing&.include?('facetable') ? { render_as: :faceted } : nil
216
+ )
217
+ end.values
218
+ end
219
+
220
+ CollectionFieldShim = Struct.new(:name, :value, :indexing, keyword_init: true)
221
+
222
+ ##
223
+ # @return [Array<IiifPrint::Field>]
224
+ def self.allinson_flex_fields
225
+ return @allinson_flex_fields if defined?(@allinson_flex_fields)
226
+
227
+ allinson_flex_relation = AllinsonFlex::ProfileProperty
228
+ .joins(:texts)
229
+ .where(allinson_flex_profile_texts: { name: 'display_label' })
230
+ .distinct
231
+ .select(:name, :value, :indexing)
232
+ flex_fields = allinson_flex_relation.to_a
233
+ unless allinson_flex_relation.exists?(name: 'collection')
234
+ collection_field = CollectionFieldShim.new(name: :collection, value: 'Collection', indexing: [])
235
+ flex_fields << collection_field
236
+ end
237
+ @allinson_flex_fields = flex_fields
238
+ end
239
+
240
+ ##
241
+ # @param fields [Array<IiifPrint::Field>]
242
+ # @param sort_order [Array<Symbol>]
243
+ def self.sort_af_fields!(fields, sort_order:)
244
+ return fields if sort_order.blank?
245
+
246
+ fields.sort_by do |field|
247
+ sort_order_index = sort_order.index(field.name.to_sym)
248
+ sort_order_index.nil? ? sort_order.length : sort_order_index
249
+ end
250
+ end
251
+
252
+ ##
253
+ # @api public
254
+ #
255
+ # @param work [ActiveFedora::Base]
256
+ # @param file_set [FileSet]
257
+ # @param locations [Array<String>]
258
+ # @param user [User]
259
+ #
260
+ # @return [Symbol] when none of the locations are to be split.
261
+ def self.conditionally_submit_split_for(work:, file_set:, locations:, user:, skip_these_endings: skip_splitting_pdf_files_that_end_with_these_texts)
262
+ locations = locations.select { |location| split_for_path_suffix?(location, skip_these_endings: skip_these_endings) }
263
+ return :no_pdfs_for_splitting if locations.empty?
264
+
265
+ work.try(:iiif_print_config)&.pdf_splitter_job&.perform_later(
266
+ file_set,
267
+ locations,
268
+ user,
269
+ work.admin_set_id,
270
+ 0 # A no longer used parameter; but we need to preserve the method signature (for now)
271
+ )
272
+ end
273
+
274
+ ##
275
+ # @api public
276
+ #
277
+ # @param path [String] the path, hopefully with an extension, to the file we're considering
278
+ # splitting.
279
+ # @param skip_these_endings [Array<#downcase>] the endings that we should skip for splitting
280
+ # purposes.
281
+ # @return [TrueClass] when the path is one we should split
282
+ # @return [FalseClass] when the path is one we should not split
283
+ #
284
+ # @see .skip_splitting_pdf_files_that_end_with_these_texts
285
+ def self.split_for_path_suffix?(path, skip_these_endings: skip_splitting_pdf_files_that_end_with_these_texts)
286
+ return false unless path.downcase.end_with?('.pdf')
287
+ return true if skip_these_endings.empty?
288
+ !path.downcase.end_with?(*skip_these_endings.map(&:downcase))
289
+ end
136
290
  end
291
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samvera
4
+ module Derivatives
5
+ ##
6
+ # The purpose of this class is to contain the explicit derivative generation directives for the
7
+ # upstream application.
8
+ #
9
+ # @note The implicit deriviate types for Hyrax are as follows:
10
+ # - type :extracted_text with sources [:pdf, :office_document]
11
+ # - type :thumbnail with sources [:pdf, :office_document, :thumbnail, :image]
12
+ # - type :mp3 with sources [:audio]
13
+ # - type :ogg with sources [:audio]
14
+ # - type :webm with sources [:video]
15
+ # - type :mp4 with sources [:video]
16
+ #
17
+ # @note A long-standing practice of Samvera's Hyrax has been to have assumptive and implicit
18
+ # derivative generation (see Hyrax::FileSetDerivativesService). In being implicit, a
19
+ # challenge arises, namely overriding and configuring. There exists a crease in the code
20
+ # to allow for a different derivative approach (see Hyrax::DerivativeService). Yet that
21
+ # approach continues the tradition of implicit work.
22
+ class Configuration
23
+ def initialize
24
+ # Favoring a Hash for ease of lookup as well as the concept that there can be only one entry
25
+ # per type.
26
+ @registered_types = {}
27
+ end
28
+
29
+ # TODO: Consider the appropriate extension
30
+ RegisteredType = Struct.new(:type, :locators, :applicators, :applicability, keyword_init: true) do
31
+ def applicable_for?(file_set:)
32
+ applicability.call(file_set)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # @api pulic
38
+ #
39
+ # @param type [Symbol] The named type of derivative
40
+ # @param locators [Array<Samvera::Derivatives::FileLocator::Strategy>] The strategies that
41
+ # we'll attempt in finding the derivative that we will later apply.
42
+ # @param applicators [Array<Samvera::Derivatives::FileApplicator::Strategy>] The strategies
43
+ # that we'll use to apply the found derivative to the {FileSet}
44
+ #
45
+ # @yieldparam applicability [#call]
46
+ #
47
+ # @return [RegisteredType]
48
+ #
49
+ # @note What is the best mechanism for naming the sources? At present we're doing a lot of
50
+ # assumption on the types.
51
+ def register(type:, locators:, applicators:, &applicability)
52
+ # Should the validator be required?
53
+ @registered_types[type.to_sym] = RegisteredType.new(
54
+ type: type.to_sym,
55
+ locators: Array(locators),
56
+ applicators: Array(applicators),
57
+ applicability: applicability || default_applicability
58
+ )
59
+ end
60
+
61
+ ##
62
+ # @api public
63
+ #
64
+ # @param type [Symbol]
65
+ #
66
+ # @return [RegisteredType]
67
+ def registry_for(type:)
68
+ @registered_types.fetch(type.to_sym) { empty_registry_for(type: type.to_sym) }
69
+ end
70
+
71
+ private
72
+
73
+ def empty_registry_for(type:)
74
+ RegisteredType.new(type: type, locators: [], applicators: [], applicability: ->(_file_set) { false })
75
+ end
76
+
77
+ # We're going to assume this is true unless configured otherwise.
78
+ def default_applicability
79
+ ->(_file_set) { true }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samvera
4
+ module Derivatives
5
+ # The default behavior of {Hyrax::FileSetDerivativesService} is to create a derivative and then
6
+ # apply it to the FileSet. This module wraps that behavior such that we can leverage the
7
+ # {Samvera::Derivatives} module and interfaces to handle cases where some of the derivatives
8
+ # already exist.
9
+ module Hyrax
10
+ # @note This conforms to the {Hyrax::DerivativeService} interface. The intention of this
11
+ # class is to be the sole registered {Hyrax::DerivativeService.services}
12
+ class ServiceShim
13
+ # @param file_set [FileSet]
14
+ # @param candidate_derivative_types [Array<Symbol>] the possible types of derivatives that
15
+ # we could create for this file_set.
16
+ # @param config [#registry_for]
17
+ #
18
+ # @todo We will want some kind of lambda to determine the candidate_derivative_types for this
19
+ # file_set.
20
+ def initialize(file_set, candidate_derivative_types: [], config: Samvera::Derivatives.config)
21
+ @file_set = file_set
22
+ @config = config
23
+ @derivatives = candidate_derivative_types.map { |type| config.registry_for(type: type) }
24
+ end
25
+
26
+ attr_reader :file_set
27
+
28
+ # @return [Array<Samvera::Derivatives::Configuration::RegisteredType>]
29
+ attr_reader :derivatives
30
+ attr_reader :config
31
+
32
+ def valid?
33
+ # We have a file set, which also means a parent work. I believe we always want this to be
34
+ # valid, because we want to leverage the locator/applicator behavior instead of the
35
+ # implicit work.
36
+ true
37
+ end
38
+
39
+ def cleanup_derivatives; end
40
+
41
+ # We have two vectors of consideration for derivative generation:
42
+ #
43
+ # - The desired derivatives for a file_set's parent work (e.g. the candidate derivatives)
44
+ # - The available derivatives for a file_set's mime type
45
+ def create_derivatives(file_path)
46
+ derivatives.each do |derivative|
47
+ Samvera::Derivatives.locate_and_apply_derivative_for(
48
+ file_set: file_set,
49
+ file_path: file_path,
50
+ derivative: derivative
51
+ )
52
+ end
53
+ end
54
+
55
+ def derivative_url(_destination_name)
56
+ ""
57
+ end
58
+ end
59
+
60
+ class FileApplicatorStrategy < Samvera::Derivatives::FileApplicator::Strategy
61
+ # With this set to true, we're telling the applicator to use the from_location
62
+ # (e.g. {Samvera::Derivatives::Hyrax::FileSetDerivativesServiceWrapper}) to apply the
63
+ # derivatives.
64
+ self.delegate_apply_to_given_from_location = true
65
+ end
66
+
67
+ class FileLocatorStrategy < Samvera::Derivatives::FileLocator::Strategy
68
+ # Implements {Samvera::Derivatives::FileLocator::Strategy} interface.
69
+ #
70
+ # @see Samvera::Derivatives::FileLocator::Strategy
71
+ #
72
+ # @return [Samvera::Derivatives::Hyrax::FileSetDerivativesServiceWrapper]
73
+ def self.locate(file_set:, file_path:, **)
74
+ file_set.samvera_derivatives_default_from_location_wrapper(file_path: file_path)
75
+ end
76
+ end
77
+
78
+ class FileSetDerivativesServiceWrapper
79
+ class_attribute :wrapped_derivative_service_class, default: ::Hyrax::FileSetDerivativesService
80
+
81
+ # @param file_set [FileSet]
82
+ # @param file_path [String]
83
+ def initialize(file_set:, file_path:)
84
+ @file_set = file_set
85
+ @file_path = file_path
86
+ @wrapped_derivative_service = wrapped_derivative_service_class.new(file_set)
87
+ end
88
+ attr_reader :file_set, :wrapped_derivative_service, :file_path
89
+
90
+ # @see Samvera::Derivatives::FileLocator.call
91
+ def present?
92
+ true
93
+ end
94
+
95
+ # @see Samvera::Derivatives::FileApplicator::Strategy
96
+ def apply!(*)
97
+ # Why the short-circuit? By the nature of the underlying
98
+ # ::Hyrax::FileSetDerivativesService, we generate multiple derivatives in one pass. But
99
+ # with the implementation of Samvera::Derivatives, we declare the derivatives and then
100
+ # iterate on locating and applying them. With this short-circuit, we will only apply the
101
+ # derivatives once.
102
+ return true if defined?(@already_applied)
103
+
104
+ return false unless wrapped_derivative_service.valid?
105
+
106
+ wrapped_derivative_service.create_derivatives(file_path)
107
+ @already_applied = true
108
+ end
109
+ end
110
+
111
+ ##
112
+ # The purpose of this module is to preserve the existing Hyrax derivative behavior while also
113
+ # allowing for the two-step tango of locator and applicator.
114
+ #
115
+ # @see Samvera::Derivatives.locate_and_apply_derivative_for
116
+ module FileSetDecorator
117
+ # @return [Samvera::Derivatives::Hyrax::FileSetDerivativesServiceWrapper]
118
+ def samvera_derivatives_default_from_location_wrapper(file_path:)
119
+ @samvera_derivatives_default_from_location_wrapper ||=
120
+ Samvera::Derivatives::Hyrax::FileSetDerivativesServiceWrapper.new(file_set: self, file_path: file_path)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ # TODO: We are likely going to want that.
128
+ # Hyrax::DerivativeService.services = [Samvera::Derivatives::Hyrax::ServiceShim]
129
+ FileSet.prepend(Samvera::Derivatives::Hyrax::FileSetDecorator)