dor-services 4.25.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/bin/dor-indexer +20 -19
  3. data/bin/dor-indexerd +3 -2
  4. data/config/certs/robots-dor-dev.crt +29 -0
  5. data/config/certs/robots-dor-dev.key +27 -0
  6. data/config/config_defaults.yml +0 -6
  7. data/config/dev_console_env.rb +65 -0
  8. data/config/environments/development.rb +84 -0
  9. data/config/environments/development.rb.old +84 -0
  10. data/config/environments/test.rb +84 -0
  11. data/lib/dor-services.rb +8 -18
  12. data/lib/dor/config.rb +18 -24
  13. data/lib/dor/datastreams/administrative_metadata_ds.rb +8 -7
  14. data/lib/dor/datastreams/content_metadata_ds.rb +200 -278
  15. data/lib/dor/datastreams/datastream_spec_solrizer.rb +1 -1
  16. data/lib/dor/datastreams/default_object_rights_ds.rb +10 -8
  17. data/lib/dor/datastreams/desc_metadata_ds.rb +30 -34
  18. data/lib/dor/datastreams/embargo_metadata_ds.rb +17 -13
  19. data/lib/dor/datastreams/events_ds.rb +12 -12
  20. data/lib/dor/datastreams/geo_metadata_ds.rb +3 -244
  21. data/lib/dor/datastreams/identity_metadata_ds.rb +34 -30
  22. data/lib/dor/datastreams/role_metadata_ds.rb +6 -6
  23. data/lib/dor/datastreams/simple_dublin_core_ds.rb +12 -9
  24. data/lib/dor/datastreams/version_metadata_ds.rb +14 -33
  25. data/lib/dor/datastreams/workflow_definition_ds.rb +18 -18
  26. data/lib/dor/datastreams/workflow_ds.rb +74 -65
  27. data/lib/dor/migrations/identifiable/assert_adminPolicy.rb +1 -1
  28. data/lib/dor/migrations/identifiable/fix_model_assertions.rb +1 -1
  29. data/lib/dor/migrations/identifiable/record_remediation.rb +2 -2
  30. data/lib/dor/migrations/identifiable/uriify_augmented_contentlocation_refs.rb +1 -1
  31. data/lib/dor/migrations/identifiable/uriify_contentlocation_refs.rb +1 -1
  32. data/lib/dor/migrations/processable/unify_workflows.rb +4 -4
  33. data/lib/dor/migrations/versionable/add_missing_version_md.rb +1 -1
  34. data/lib/dor/models/admin_policy_object.rb +1 -1
  35. data/lib/dor/models/assembleable.rb +3 -4
  36. data/lib/dor/models/collection.rb +0 -2
  37. data/lib/dor/models/contentable.rb +34 -35
  38. data/lib/dor/models/describable.rb +80 -122
  39. data/lib/dor/models/editable.rb +57 -73
  40. data/lib/dor/models/embargoable.rb +13 -15
  41. data/lib/dor/models/eventable.rb +3 -3
  42. data/lib/dor/models/geoable.rb +8 -9
  43. data/lib/dor/models/governable.rb +36 -54
  44. data/lib/dor/models/identifiable.rb +119 -115
  45. data/lib/dor/models/item.rb +4 -4
  46. data/lib/dor/models/itemizable.rb +9 -9
  47. data/lib/dor/models/presentable.rb +133 -0
  48. data/lib/dor/models/preservable.rb +4 -4
  49. data/lib/dor/models/processable.rb +29 -28
  50. data/lib/dor/models/publishable.rb +36 -30
  51. data/lib/dor/models/releasable.rb +310 -0
  52. data/lib/dor/models/shelvable.rb +14 -14
  53. data/lib/dor/models/upgradable.rb +13 -13
  54. data/lib/dor/models/versionable.rb +4 -7
  55. data/lib/dor/models/workflow_object.rb +16 -36
  56. data/lib/dor/services/cleanup_reset_service.rb +28 -34
  57. data/lib/dor/services/cleanup_service.rb +4 -4
  58. data/lib/dor/services/digital_stacks_service.rb +10 -10
  59. data/lib/dor/services/merge_service.rb +1 -1
  60. data/lib/dor/services/metadata_handlers/mdtoolkit_handler.rb +2 -2
  61. data/lib/dor/services/metadata_service.rb +20 -20
  62. data/lib/dor/services/registration_service.rb +26 -27
  63. data/lib/dor/services/reset_workspace_service.rb +15 -15
  64. data/lib/dor/services/sdr_ingest_service.rb +4 -4
  65. data/lib/dor/services/search_service.rb +4 -9
  66. data/lib/dor/services/suri_service.rb +5 -5
  67. data/lib/dor/services/technical_metadata_service.rb +3 -2
  68. data/lib/dor/utils/ng_tidy.rb +9 -9
  69. data/lib/dor/utils/predicate_patch.rb +1 -1
  70. data/lib/dor/utils/solr_doc_helper.rb +13 -5
  71. data/lib/dor/version.rb +1 -1
  72. data/lib/dor/workflow/document.rb +28 -30
  73. data/lib/dor/workflow/graph.rb +36 -36
  74. data/lib/dor/workflow/process.rb +12 -12
  75. data/lib/tasks/dor.rake +1 -1
  76. data/lib/tasks/rdoc.rake +3 -3
  77. metadata +67 -76
  78. data/lib/dor/datastreams/geo2mods.xsl +0 -867
  79. data/lib/dor/models/discoverable.rb +0 -64
  80. data/lib/dor/models/releaseable.rb +0 -357
  81. data/lib/dor/services/indexing_service.rb +0 -64
  82. data/lib/dor/utils/sdr_client.rb +0 -23
  83. data/lib/dor/utils/utc_date_field_mapper.rb +0 -7
@@ -17,11 +17,11 @@ module Dor
17
17
  ds.label = 'Provenance Metadata'
18
18
  end
19
19
  ds.ng_xml = workflow_provenance
20
- ds.content=ds.ng_xml.to_s
20
+ ds.content=ds.ng_xml.to_s
21
21
  ds.save
22
22
  end
23
23
 
24
- def build_technicalMetadata_datastream(ds = nil)
24
+ def build_technicalMetadata_datastream(ds=nil)
25
25
  TechnicalMetadataService.add_update_technical_metadata(self)
26
26
  end
27
27
 
@@ -33,9 +33,9 @@ module Dor
33
33
  # @return [Nokogiri::Document]
34
34
  def create_workflow_provenance(workflow_id, event_text)
35
35
  builder = Nokogiri::XML::Builder.new do |xml|
36
- xml.provenanceMetadata(:objectId => pid) {
36
+ xml.provenanceMetadata(:objectId => self.pid) {
37
37
  xml.agent(:name => 'DOR') {
38
- xml.what(:object => pid) {
38
+ xml.what(:object => self.pid) {
39
39
  xml.event(:who => "DOR-#{workflow_id}", :when => Time.new.iso8601) {
40
40
  xml.text(event_text)
41
41
  }
@@ -42,11 +42,11 @@ module Dor
42
42
  # This is a work-around for some strange logic in ActiveFedora that
43
43
  # don't allow self.workflows.new? to work if we load the object using
44
44
  # .load_instance_from_solr.
45
- return if self.respond_to?(:inner_object) && inner_object.is_a?(ActiveFedora::SolrDigitalObject)
45
+ return if self.respond_to? :inner_object and self.inner_object.is_a? ActiveFedora::SolrDigitalObject
46
46
 
47
- if workflows.new?
47
+ if self.workflows.new?
48
48
  workflows.mimeType = 'application/xml'
49
- workflows.dsLocation = File.join(Dor::Config.workflow.url,"dor/objects/#{pid}/workflows")
49
+ workflows.dsLocation = File.join(Dor::Config.workflow.url,"dor/objects/#{self.pid}/workflows")
50
50
  end
51
51
  end
52
52
 
@@ -54,7 +54,7 @@ module Dor
54
54
  if datastream.new?
55
55
  true
56
56
  elsif datastream.class.respond_to?(:xml_template)
57
- datastream.content.to_s.empty? || EquivalentXml.equivalent?(datastream.content, datastream.class.xml_template)
57
+ datastream.content.to_s.empty? or EquivalentXml.equivalent?(datastream.content, datastream.class.xml_template)
58
58
  else
59
59
  datastream.content.to_s.empty?
60
60
  end
@@ -65,7 +65,7 @@ module Dor
65
65
  # Returns the path to it or nil.
66
66
  def find_metadata_file(datastream)
67
67
  druid = DruidTools::Druid.new(pid, Dor::Config.stacks.local_workspace_root)
68
- druid.find_metadata("#{datastream}.xml")
68
+ return druid.find_metadata("#{datastream}.xml")
69
69
  end
70
70
 
71
71
  # Takes the name of a datastream, as a string (fooMetadata).
@@ -84,10 +84,10 @@ module Dor
84
84
  ds.content = content
85
85
  ds.ng_xml = Nokogiri::XML(content) if ds.respond_to?(:ng_xml)
86
86
  ds.save unless ds.digital_object.new?
87
- elsif force || empty_datastream?(ds)
87
+ elsif force or empty_datastream?(ds)
88
88
  meth = "build_#{datastream}_datastream".to_sym
89
89
  if respond_to?(meth)
90
- content = send(meth, ds)
90
+ content = self.send(meth, ds)
91
91
  ds.save unless ds.digital_object.new?
92
92
  end
93
93
  end
@@ -95,7 +95,7 @@ module Dor
95
95
  if is_required && empty_datastream?(ds)
96
96
  raise "Required datastream #{datastream} could not be populated!"
97
97
  end
98
- ds
98
+ return ds
99
99
  end
100
100
 
101
101
  def cleanup()
@@ -103,13 +103,13 @@ module Dor
103
103
  end
104
104
 
105
105
  def milestones
106
- Dor::WorkflowService.get_milestones('dor',pid)
106
+ Dor::WorkflowService.get_milestones('dor',self.pid)
107
107
  end
108
108
 
109
109
  def status_info()
110
110
  current_version = '1'
111
111
  begin
112
- current_version = versionMetadata.current_version_id
112
+ current_version = self.versionMetadata.current_version_id
113
113
  rescue
114
114
  end
115
115
 
@@ -117,8 +117,8 @@ module Dor
117
117
  #only get steps that are part of accessioning and part of the current version. That can mean they were archived with the current version
118
118
  #number, or they might be active (no version number).
119
119
  milestones.each do |m|
120
- if STEPS.keys.include?(m[:milestone]) && (m[:version].nil? || m[:version] == current_version)
121
- current_milestones << m unless m[:milestone] == 'registered' && current_version.to_i > 1
120
+ if STEPS.keys.include?(m[:milestone]) and (m[:version].nil? or m[:version] == current_version)
121
+ current_milestones << m unless m[:milestone] == 'registered' and current_version.to_i > 1
122
122
  end
123
123
  end
124
124
 
@@ -136,25 +136,25 @@ module Dor
136
136
  end
137
137
  end
138
138
 
139
- {:current_version => current_version, :status_code => status_code, :status_time => status_time}
139
+ return {:current_version => current_version, :status_code => status_code, :status_time => status_time}
140
140
  end
141
141
 
142
- def status(include_time = false)
142
+ def status(include_time=false)
143
143
  status_info_hash = status_info
144
144
  current_version, status_code, status_time = status_info_hash[:current_version], status_info_hash[:status_code], status_info_hash[:status_time]
145
145
 
146
146
  #use the translation table to get the appropriate verbage for the latest step
147
147
  result = "v#{current_version} #{STATUS_CODE_DISP_TXT[status_code]}"
148
148
  result += " #{format_date(status_time)}" if include_time
149
- result
149
+ return result
150
150
  end
151
151
 
152
- def to_solr(solr_doc = Hash.new, *args)
152
+ def to_solr(solr_doc=Hash.new, *args)
153
153
  super(solr_doc, *args)
154
154
  sortable_milestones = {}
155
155
  current_version='1'
156
156
  begin
157
- current_version = versionMetadata.current_version_id
157
+ current_version = self.versionMetadata.current_version_id
158
158
  rescue
159
159
  end
160
160
  current_version_num=current_version.to_i
@@ -162,12 +162,12 @@ module Dor
162
162
  if self.respond_to?('versionMetadata')
163
163
  #add an entry with version id, tag and description for each version
164
164
  while current_version_num > 0
165
- add_solr_value(solr_doc, 'versions', current_version_num.to_s + ';' + versionMetadata.tag_for_version(current_version_num.to_s) + ';' + versionMetadata.description_for_version(current_version_num.to_s), :string, [:displayable])
165
+ add_solr_value(solr_doc, 'versions', current_version_num.to_s + ';' + self.versionMetadata.tag_for_version(current_version_num.to_s) + ';' + self.versionMetadata.description_for_version(current_version_num.to_s), :string, [:displayable])
166
166
  current_version_num -= 1
167
167
  end
168
168
  end
169
169
 
170
- milestones.each do |milestone|
170
+ self.milestones.each do |milestone|
171
171
  timestamp = milestone[:at].utc.xmlschema
172
172
  sortable_milestones[milestone[:milestone]] ||= []
173
173
  sortable_milestones[milestone[:milestone]] << timestamp
@@ -181,8 +181,8 @@ module Dor
181
181
  sortable_milestones.each do |milestone, unordered_dates|
182
182
  dates = unordered_dates.sort
183
183
  #create the published_dt and published_day fields and the like
184
- add_solr_value(solr_doc, milestone+'_day', DateTime.parse(dates.last).beginning_of_day.utc.xmlschema.split('T').first, :string, [:searchable, :facetable])
185
- add_solr_value(solr_doc, milestone, dates.first, :date, [:searchable, :facetable])
184
+ add_solr_value(solr_doc, milestone+'_day', DateTime.parse(dates.last).beginning_of_day.utc.xmlschema.split('T').first, :string, [:searchable, :stored_searchable, :facetable])
185
+ add_solr_value(solr_doc, milestone, dates.first, :date, [:searchable, :facetable, :stored_searchable])
186
186
 
187
187
  #fields for OAI havester to sort on
188
188
  add_solr_value(solr_doc, "#{milestone}_earliest_dt", dates.first, :date, [:sortable])
@@ -201,7 +201,7 @@ module Dor
201
201
  add_solr_value(solr_doc, "version_opened", DateTime.parse(opened_date).beginning_of_day.utc.xmlschema.split('T').first, :string, [ :searchable, :facetable])
202
202
  end
203
203
  add_solr_value(solr_doc, "current_version", current_version.to_s, :string, [ :displayable , :facetable])
204
- add_solr_value(solr_doc, "last_modified_day", modified_date.to_s.split('T').first, :string, [ :facetable ])
204
+ add_solr_value(solr_doc, "last_modified_day", self.modified_date.to_s.split('T').first, :string, [ :facetable ])
205
205
  add_solr_value(solr_doc, "rights", rights, :string, [:facetable]) if self.respond_to? :rights
206
206
  solr_doc
207
207
  end
@@ -210,24 +210,25 @@ module Dor
210
210
  # It will set the priorty of the new workflow to the current_priority if it is > 0
211
211
  # It will set lane_id from the item's APO default workflow lane
212
212
  # @param [String] name of the workflow to be initialized
213
+ # @param [String] repo name of the repository to create workflow for
213
214
  # @param [Boolean] create_ds create a 'workflows' datastream in Fedora for the object
214
- # @param [Integer] priority the workflow's priority level
215
- def initialize_workflow(name, create_ds = true, priority = 0)
215
+ def initialize_workflow(name, repo='dor', create_ds=true, priority=0)
216
216
  priority = workflows.current_priority if priority == 0
217
217
  opts = { :create_ds => create_ds }
218
218
  opts[:priority] = priority if(priority > 0)
219
219
  opts[:lane_id] = default_workflow_lane
220
- Dor::WorkflowService.create_workflow(Dor::WorkflowObject.initial_repo(name), pid, name, Dor::WorkflowObject.initial_workflow(name), opts)
220
+ Dor::WorkflowService.create_workflow(repo, self.pid, name, Dor::WorkflowObject.initial_workflow(name), opts)
221
221
  end
222
222
 
223
223
 
224
224
  private
225
225
  #handles formating utc date/time to human readable
226
+ # XXX: bad form to hardcode TZ here. Code smell abounds.
226
227
  def format_date datetime
227
228
  begin
228
- zone = ActiveSupport::TimeZone.new("Pacific Time (US & Canada)")
229
- d = datetime.is_a?(Time) ? datetime : DateTime.parse(datetime).in_time_zone(zone)
230
- I18n.l(d)
229
+ d = datetime.is_a?(Time) ? datetime :
230
+ DateTime.parse(datetime).in_time_zone(ActiveSupport::TimeZone.new("Pacific Time (US & Canada)"))
231
+ I18n.l(d).strftime('%Y-%m-%d %I:%M%p')
231
232
  rescue
232
233
  d = datetime.is_a?(Time) ? datetime : Time.parse(datetime.to_s)
233
234
  d.strftime('%Y-%m-%d %I:%M%p')
@@ -7,23 +7,24 @@ module Dor
7
7
  include Governable
8
8
  include Describable
9
9
  include Itemizable
10
+ include Presentable
10
11
 
11
12
  included do
12
- has_metadata name: 'rightsMetadata', type: ActiveFedora::OmDatastream, label: 'Rights Metadata'
13
+ has_metadata :name => "rightsMetadata", :type => ActiveFedora::OmDatastream, :label => 'Rights Metadata'
13
14
  end
14
15
 
15
16
  def build_rightsMetadata_datastream(ds)
16
- content_ds = admin_policy_object.datastreams['defaultObjectRights']
17
+ content_ds = self.admin_policy_object.datastreams['defaultObjectRights']
17
18
  ds.dsLabel = 'Rights Metadata'
18
19
  ds.ng_xml = content_ds.ng_xml.clone
19
20
  ds.content = ds.ng_xml.to_xml
20
21
  end
21
22
 
22
23
  def public_relationships
23
- include_elements = ['fedora:isMemberOf','fedora:isMemberOfCollection','fedora:isConstituentOf']
24
- rels_doc = Nokogiri::XML(datastreams['RELS-EXT'].content)
25
- rels_doc.xpath('/rdf:RDF/rdf:Description/*', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#').each do |rel|
26
- unless include_elements.include?([rel.namespace.prefix, rel.name].join(':'))
24
+ include_elements = ['fedora:isMemberOf','fedora:isMemberOfCollection']
25
+ rels_doc = Nokogiri::XML(self.datastreams['RELS-EXT'].content)
26
+ rels_doc.xpath('/rdf:RDF/rdf:Description/*', { 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' }).each do |rel|
27
+ unless include_elements.include?([rel.namespace.prefix,rel.name].join(':'))
27
28
  rel.next_sibling.remove if rel.next_sibling.content.strip.empty?
28
29
  rel.remove
29
30
  end
@@ -31,29 +32,29 @@ module Dor
31
32
  rels_doc
32
33
  end
33
34
 
34
- # Generate the public .xml for a PURL page.
35
- # @return [xml] The public xml for the item
35
+ #Generate the public .xml for a PURL page.
36
+ #@return [xml] The public xml for the item
37
+ #
36
38
  def public_xml
37
- pub = Nokogiri::XML('<publicObject/>').root
39
+ pub = Nokogiri::XML("<publicObject/>").root
38
40
  pub['id'] = pid
39
41
  pub['published'] = Time.now.xmlschema
40
- pub['publishVersion'] = 'dor-services/' + Dor::VERSION
41
- release_xml = Nokogiri(generate_release_xml).xpath('//release')
42
+ release_xml=Nokogiri(self.generate_release_xml).xpath('//release')
42
43
 
43
- im = datastreams['identityMetadata'].ng_xml.clone
44
- im.search('//release').each(&:remove) # remove any <release> tags from public xml which have full history
44
+ im=self.datastreams['identityMetadata'].ng_xml.clone
45
+ im.search('//release').each {|node| node.remove} # remove any <release> tags from public xml which have full history
46
+ im.root.add_child(release_xml) # now add in final <release> tag #TODO: Adding this breaks tests, rework these
45
47
 
46
48
  pub.add_child(im.root) # add in modified identityMetadata datastream
47
- pub.add_child(datastreams['contentMetadata'].public_xml.root.clone)
48
- pub.add_child(datastreams['rightsMetadata'].ng_xml.root.clone)
49
+ pub.add_child(self.datastreams['contentMetadata'].public_xml.root.clone)
50
+ pub.add_child(self.datastreams['rightsMetadata'].ng_xml.root.clone)
49
51
 
50
52
  rels = public_relationships.root
51
53
  pub.add_child(rels.clone) unless rels.nil? # TODO: Should never be nil in practice; working around an ActiveFedora quirk for testing
52
- pub.add_child(generate_dublin_core.root.clone)
53
- pub.add_child(Nokogiri(generate_release_xml).root.clone) unless release_xml.children.size == 0 # If there are no release_tags, this prevents an empty <releaseData/> from being added
54
- # Note we cannot base this on if an individual object has release tags or not, because the collection may cause one to be generated for an item,
55
- # so we need to calculate it and then look at the final result.s
56
- new_pub = Nokogiri::XML(pub.to_xml, &:noblanks)
54
+ pub.add_child(self.generate_dublin_core.root.clone)
55
+ @public_xml_doc = pub # save this for possible IIIF Presentation manifest
56
+ pub.add_child(Nokogiri(self.generate_release_xml).root.clone) #TODO: Adding this breaks tests, rework these
57
+ new_pub = Nokogiri::XML(pub.to_xml) { |x| x.noblanks }
57
58
  new_pub.encoding = 'UTF-8'
58
59
  new_pub.to_xml
59
60
  end
@@ -62,25 +63,30 @@ module Dor
62
63
  # otherwise, it prunes the object's metadata from the document cache
63
64
  def publish_metadata
64
65
  rights = datastreams['rightsMetadata'].ng_xml.clone.remove_namespaces!
65
- if rights.at_xpath("//rightsMetadata/access[@type='discover']/machine/world")
66
- dc_xml = generate_dublin_core.to_xml(&:no_declaration)
66
+ if(rights.at_xpath("//rightsMetadata/access[@type='discover']/machine/world"))
67
+ dc_xml = self.generate_dublin_core.to_xml {|config| config.no_declaration}
67
68
  DigitalStacksService.transfer_to_document_store(pid, dc_xml, 'dc')
68
- DigitalStacksService.transfer_to_document_store(pid, datastreams['identityMetadata'].to_xml, 'identityMetadata')
69
- DigitalStacksService.transfer_to_document_store(pid, datastreams['contentMetadata'].to_xml, 'contentMetadata')
70
- DigitalStacksService.transfer_to_document_store(pid, datastreams['rightsMetadata'].to_xml, 'rightsMetadata')
69
+ DigitalStacksService.transfer_to_document_store(pid, self.datastreams['identityMetadata'].to_xml, 'identityMetadata')
70
+ DigitalStacksService.transfer_to_document_store(pid, self.datastreams['contentMetadata'].to_xml, 'contentMetadata')
71
+ DigitalStacksService.transfer_to_document_store(pid, self.datastreams['rightsMetadata'].to_xml, 'rightsMetadata')
71
72
  DigitalStacksService.transfer_to_document_store(pid, public_xml, 'public')
72
- DigitalStacksService.transfer_to_document_store(pid, generate_public_desc_md, 'mods') if metadata_format == 'mods'
73
+ if self.metadata_format == 'mods'
74
+ DigitalStacksService.transfer_to_document_store(pid, self.generate_public_desc_md, 'mods')
75
+ end
76
+ if iiif_presentation_manifest_needed? @public_xml_doc
77
+ DigitalStacksService.transfer_to_document_store(pid, build_iiif_manifest(@public_xml_doc), 'manifest')
78
+ end
73
79
  else
74
80
  # Clear out the document cache for this item
75
81
  DigitalStacksService.prune_purl_dir pid
76
82
  end
77
83
  end
78
-
79
- # Call dor services app to have it publish the metadata
84
+ #call the dor services app to have it publish the metadata
80
85
  def publish_metadata_remotely
81
- dor_services = RestClient::Resource.new(Config.dor_services.url + "/v1/objects/#{pid}/publish")
86
+ dor_services = RestClient::Resource.new(Config.dor_services.url+"/v1/objects/#{pid}/publish")
82
87
  dor_services.post ''
83
88
  dor_services.url
84
89
  end
85
90
  end
86
- end
91
+
92
+ end
@@ -0,0 +1,310 @@
1
+ module Dor
2
+ module Releasable
3
+ extend ActiveSupport::Concern
4
+ include Itemizable
5
+
6
+ #Generate XML structure for inclusion to Purl
7
+ #
8
+ #@return [String] The XML release node as a string, with ReleaseDigest as the root document
9
+ def generate_release_xml
10
+ builder = Nokogiri::XML::Builder.new do |xml|
11
+ xml.ReleaseDigest {
12
+ self.released_for.each do |project,released_value|
13
+ xml.release(released_value["release"],:to=>project)
14
+ end
15
+ }
16
+ end
17
+ return builder.to_xml
18
+ end
19
+
20
+ #Determine which projects an item is released for
21
+ #
22
+ #@return [Hash] all namespaces in the form of {"Project" => Boolean}
23
+ def released_for
24
+ released_hash = {}
25
+
26
+ #Get release tags on the item itself
27
+ release_tags_on_this_item = self.release_nodes
28
+
29
+ #Get any self tags on this item
30
+ self_release_tags = self.get_self_release_tags(release_tags_on_this_item)
31
+
32
+ #Get the most recent self tag for all targets and save their result since most recent self always trumps any other non self tags
33
+ latest_self_tags = self.get_newest_release_tag(self_release_tags)
34
+ latest_self_tags.keys.each do |target|
35
+ released_hash[target] = self.clean_release_tag_for_purl(latest_self_tags[target])
36
+ end
37
+
38
+ #With Self Tags Resolved We Now need to deal with tags on all sets this object is part of
39
+
40
+ potential_applicable_release_tags = {} #This will be where we store all tags that apply, regardless of their timestamp
41
+
42
+ #Get all release tags on the item and strip out the what = self ones, we've already processed all the self tags on this item
43
+ potential_applicable_release_tags = get_tags_for_what_value(self.get_release_tags_for_item_and_all_governing_sets, 'collection')
44
+
45
+ administrative_tags = self.tags #Get them once here and pass them down
46
+
47
+ #We now have the keys for all potential releases, we need to check the tags and the most recent time stamp with an explicit true or false wins, in a nil case, the lack of an explicit false tag we do nothing
48
+ (potential_applicable_release_tags.keys-released_hash.keys).each do |key| #don't bother checking the ones already added to the release hash, they were added due to a self tag and that has won
49
+ latest_applicable_tag_for_key = latest_applicable_release_tag_in_array(potential_applicable_release_tags[key], administrative_tags)
50
+ if latest_applicable_tag_for_key != nil #We have a valid tag, record it
51
+ released_hash[key] = self.clean_release_tag_for_purl(latest_applicable_tag_for_key)
52
+ end
53
+
54
+ end
55
+
56
+ return released_hash
57
+ end
58
+
59
+ #Take a hash of tags as obtained via Dor::Item.release_tags and returns all self tags
60
+ #
61
+ #@param tags [Hash] a hash of tags obtained via Dor::Item.release_tags or matching format
62
+ #
63
+ #@return [Hash] a hash of self tags for each to value
64
+ def get_self_release_tags(tags)
65
+ return get_tags_for_what_value(tags, 'self')
66
+ end
67
+
68
+ #Take an item and get all of its release tags and all tags on collections it is a member of it
69
+ #
70
+ #
71
+ #@return [Hash] a hash of all tags
72
+ def get_release_tags_for_item_and_all_governing_sets
73
+ return_tags = self.release_nodes || {}
74
+ self.collections.each do |collection|
75
+ return_tags = combine_two_release_tag_hashes(return_tags, Dor::Item.find(collection.id).get_release_tags_for_item_and_all_governing_sets) #this will function recurvisely so parents of parents are found
76
+ end
77
+ return return_tags
78
+ end
79
+
80
+ #Take two hashes of tags and combine them, will not overwrite but will enforce uniqueness of the tags
81
+ #
82
+ #@param hash_one [Hash] a hash of tags obtained via Dor::Item.release_tags or matching format
83
+ #@param hash_two [Hash] a hash of tags obtained via Dor::Item.release_tags or matching format
84
+ #
85
+ #@return [Hash] the combined hash with uniquiness enforced
86
+ def combine_two_release_tag_hashes(hash_one, hash_two)
87
+ hash_two.keys.each do |key|
88
+ hash_one[key] = hash_two[key] if hash_one[key].nil?
89
+ hash_one[key] = (hash_one[key] + hash_two[key]).uniq if hash_one[key] != nil
90
+ end
91
+ return hash_one
92
+ end
93
+
94
+ #Take a hash of tags and return all tags with the matching what target
95
+ #
96
+ #@param tags [Hash] a hash of tags obtained via Dor::Item.release_tags or matching format
97
+ #@param what_target [String] the target for the 'what' key, self or collection
98
+ #
99
+ #@return [Hash] a hash of self tags for each to value
100
+ def get_tags_for_what_value(tags, what_target)
101
+ return_hash = {}
102
+ tags.keys.each do |key|
103
+ self_tags = tags[key].select{|tag| tag['what'] == what_target.downcase}
104
+ return_hash[key] = self_tags if self_tags.size > 0
105
+ end
106
+ return return_hash
107
+ end
108
+
109
+ #Take a hash of tags as obtained via Dor::Item.release_tags and returns the newest tag for each namespace
110
+ #
111
+ #@params tags [Hash] a hash of tags obtained via Dor::Item.release_tags or matching format
112
+ #
113
+ #@return [Hash] a hash of latest tags for each to value
114
+ def get_newest_release_tag(tags)
115
+ return_hash = {}
116
+ tags.keys.each do |key|
117
+ latest_for_key = newest_release_tag_in_an_array(tags[key])
118
+ return_hash[key] = latest_for_key
119
+ end
120
+ return return_hash
121
+ end
122
+
123
+ #Take a tag and return only the attributes we want to put into purl
124
+ #
125
+ #@param tag [Hash] a tag
126
+ #
127
+ #@return [Hash] a hash of the attributes we want for purl
128
+ def clean_release_tag_for_purl(tag)
129
+ for_purl = ['release']
130
+ return_hash = {}
131
+ for_purl.each do |attr|
132
+ return_hash[attr] = tag[attr]
133
+ end
134
+ return return_hash
135
+ end
136
+
137
+ #Takes an array of release tags and returns the most recent one
138
+ #
139
+ #@params tags [Array] an array of hashes, with the hashes being release tags
140
+ #
141
+ #@return [Hash] the most recent tag
142
+ def newest_release_tag_in_an_array(array_of_tags)
143
+ latest_tag_in_array = array_of_tags[0] || {}
144
+ array_of_tags.each do |tag|
145
+ latest_tag_in_array = tag if tag['when'] > latest_tag_in_array['when']
146
+ end
147
+ return latest_tag_in_array
148
+ end
149
+
150
+ #Takes a tag and returns true or false if it applies to the specific item
151
+ #
152
+ #@param release_tag [Hash] the tag in a hashed form
153
+ #@param Optional admin_tags [Array] the administrative tags on an item, if not supplied it will attempt to retrieve them
154
+ #
155
+ #@return [Boolean] true or false if it applies (not true or false if it is released, that is the release_tag data)
156
+ def does_release_tag_apply(release_tag, admin_tags=false)
157
+ #Is the tag global or restricted
158
+ return true if release_tag['tag'].nil? #there is no specific tag specificied, so that means this tag is global to all members of the collection, it applies, return true
159
+
160
+ admin_tags = self.tags unless admin_tags #We use false instead of [], since an item can have no admin_tags that which point we'd be passing down this variable as [] and would not an attempt to retrieve it
161
+ return admin_tags.include?(release_tag['tag'])
162
+ end
163
+
164
+ #Takes an array of release tags and returns the most recent one that applies to this item
165
+ #
166
+ #@param release_tags [Array] an array of release tags in hashed form
167
+ #param admin_tags [Array] the administrative tags on an on item
168
+ #
169
+ #@return [Hash] the tag
170
+ def latest_applicable_release_tag_in_array(release_tags, admin_tags)
171
+ newest_tag = newest_release_tag_in_an_array(release_tags)
172
+ return newest_tag if does_release_tag_apply(newest_tag, admin_tags) #Return true if we have it
173
+
174
+ #The latest tag wasn't applicable, slice it off and try again
175
+ #This could be optimized by reordering on the timestamp and just running down it instead of constantly resorting, at least if we end up getting numerous release tags on an item
176
+ release_tags.slice!(release_tags.index(newest_tag))
177
+
178
+ return latest_applicable_release_tag_in_array(release_tags, admin_tags) if release_tags.size > 0 #Try again after dropping the one that wasn't applicable
179
+
180
+ return nil #We're out of tags, no applicable ones
181
+ end
182
+
183
+ #helper method to get the release tags as a nodeset
184
+ #
185
+ #@return [Nokogiri::XML::NodeSet] of all release tags and their attributes
186
+ def release_tags
187
+ release_tags = self.identityMetadata.ng_xml.xpath('//release')
188
+ return_hash = {}
189
+ release_tags.each do |release_tag|
190
+ hashed_node = self.release_tag_node_to_hash(release_tag)
191
+ if return_hash[hashed_node[:to]] != nil
192
+ return_hash[hashed_node[:to]] << hashed_node[:attrs]
193
+ else
194
+ return_hash[hashed_node[:to]] = [hashed_node[:attrs]]
195
+ end
196
+ end
197
+ return return_hash
198
+ end
199
+
200
+ #method to convert one release element into an array
201
+ #
202
+ #@param rtag [Nokogiri::XML::Element] the release tag element
203
+ #
204
+ #return [Hash] in the form of {:to => String :attrs = Hash}
205
+ def release_tag_node_to_hash(rtag)
206
+ to = 'to'
207
+ release = 'release'
208
+ when_word = 'when' #TODO: Make to and when_word load from some config file instead of hardcoded here
209
+ attrs = rtag.attributes
210
+ return_hash = { :to => attrs[to].value }
211
+ attrs.tap { |a| a.delete(to)}
212
+ attrs[release] = rtag.text.downcase == "true" #save release as a boolean
213
+ return_hash[:attrs] = attrs
214
+
215
+ #convert all the attrs beside :to to strings, they are currently Nokogiri::XML::Attr
216
+ (return_hash[:attrs].keys-[to]).each do |a|
217
+ return_hash[:attrs][a] = return_hash[:attrs][a].to_s if a != release
218
+ end
219
+
220
+ return_hash[:attrs][when_word] = Time.parse(return_hash[:attrs][when_word]) #convert when to a datetime
221
+
222
+ return return_hash
223
+ end
224
+
225
+ #Determine if the supplied tag is a valid release tag that meets all requirements
226
+ #
227
+ #@raises [RuntimeError] Raises an error of the first fault in the release tag
228
+ #
229
+ #@return [Boolean] Returns true if no errors found
230
+ #
231
+ #@params attrs [hash] A hash of attributes for the tag, must contain: :when, a ISO 8601 timestamp; :who, to identify who or what added the tag; and :to, a string identifying the release target
232
+ def valid_release_attributes_and_tag(tag, attrs={})
233
+ raise ArgumentError, ":when is not iso8601" if attrs[:when].match('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z') == nil
234
+ [:who, :to, :what].each do |check_attr|
235
+ raise ArgumentError, "#{check_attr} not supplied as a String" if attrs[check_attr].class != String
236
+ end
237
+
238
+ what_correct = false
239
+ ['self', 'collection'].each do |allowed_what_value|
240
+ what_correct = true if attrs[:what] == allowed_what_value
241
+ end
242
+ raise ArgumentError, ":what must be self or collection" if ! what_correct
243
+ raise ArgumentError, "the value set for this tag is not a boolean" if !!tag != tag
244
+ validate_tag_format(attrs[:tag]) if attrs[:tag] != nil #Will Raise exception if invalid tag
245
+ return true
246
+ end
247
+
248
+ #Add a release node for the item
249
+ #Will use the current time to add in the timestamp if you do not supply a timestamp, you can supply a timestap for correcting history, etc if desired
250
+ #
251
+ #@return [Nokogiri::XML::Element] the tag added if successful
252
+ #
253
+ #@raise [RuntimeError] Raised if attributes are improperly supplied
254
+ #
255
+ #@params tag [Boolean] True or false for the release node
256
+ #@params attrs [hash] A hash of any attributes to be placed onto the tag
257
+ # release tag example:
258
+ # item.add_tag(true,:release,{:tag=>'Fitch : Batch2',:what=>'self',:to=>'Searchworks',:who=>'petucket'})
259
+ def add_release_node(release, attrs={})
260
+ identity_metadata_ds = self.identityMetadata
261
+ attrs[:when] = Time.now.utc.iso8601 if attrs[:when] == nil#add the timestamp
262
+ valid_release_attributes(release, attrs)
263
+
264
+ return identity_metadata_ds.add_value(:release, release.to_s, attrs)
265
+ end
266
+
267
+ #Determine if the supplied tag is a valid release node that meets all requirements
268
+ #
269
+ #@raises [ArgumentError] Raises an error of the first fault in the release tag
270
+ #
271
+ #@return [Boolean] Returns true if no errors found
272
+ #
273
+ #@params attrs [hash] A hash of attributes for the tag, must contain :when, a ISO 8601 timestamp and :who to identify who or what added the tag, :to,
274
+ def valid_release_attributes(tag, attrs={})
275
+ raise ArgumentError, ":when is not iso8601" if attrs[:when].match('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z') == nil
276
+ [:who, :to, :what].each do |check_attr|
277
+ raise ArgumentError, "#{check_attr} not supplied as a String" if attrs[check_attr].class != String
278
+ end
279
+
280
+ what_correct = false
281
+ ['self', 'collection'].each do |allowed_what_value|
282
+ what_correct = true if attrs[:what] == allowed_what_value
283
+ end
284
+ raise ArgumentError, ":what must be self or collection" if ! what_correct
285
+
286
+ raise ArgumentError, "the value set for this tag is not a boolean" if !!tag != tag
287
+ #identity_metadata_ds = self.identityMetadata
288
+ validate_tag_format(attrs[:tag]) if attrs[:tag] != nil #Will Raise exception if invalid tag
289
+ return true
290
+ end
291
+
292
+ #helper method to get the release nodes as a nodeset
293
+ #
294
+ #@return [Nokogiri::XML::NodeSet] of all release tags and their attributes
295
+ def release_nodes
296
+ release_tags = self.identityMetadata.ng_xml.xpath('//release')
297
+ return_hash = {}
298
+ release_tags.each do |release_tag|
299
+ hashed_node = self.release_tag_node_to_hash(release_tag)
300
+ if return_hash[hashed_node[:to]] != nil
301
+ return_hash[hashed_node[:to]] << hashed_node[:attrs]
302
+ else
303
+ return_hash[hashed_node[:to]] = [hashed_node[:attrs]]
304
+ end
305
+ end
306
+ return return_hash
307
+ end
308
+
309
+ end
310
+ end