openurl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/openurl.rb ADDED
@@ -0,0 +1,10 @@
1
+ # A library to create and parse NISO Z39.88 OpenURLs
2
+ # Can also work with OpenURL 0.1, but your YMMV
3
+ # See: http://alcme.oclc.org/openurl/docs/implementation_guidelines/
4
+ # for more information on implementing NISO Z39.88
5
+ require 'date'
6
+ require 'rexml/document'
7
+ require 'cgi'
8
+ require 'openurl/context_object'
9
+ require 'openurl/context_object_entity'
10
+ require 'openurl/transport'
@@ -0,0 +1,530 @@
1
+ module OpenURL
2
+
3
+ # The ContextObject class is intended to both create new OpenURL 1.0 context
4
+ # objects or parse existing ones, either from Key-Encoded Values (KEVs) or XML.
5
+ # Usage:
6
+ # require 'openurl/context_object'
7
+ # include OpenURL
8
+ # ctx = ContextObject.new
9
+ # ctx.referent.set_format('journal')
10
+ # ctx.referent.add_identifier('info:doi/10.1016/j.ipm.2005.03.024')
11
+ # ctx.referent.set_metadata('issn', '0306-4573')
12
+ # ctx.referent.set_metadata('aulast', 'Bollen')
13
+ # ctx.referrer.add_identifier('info:sid/google')
14
+ # puts ctx.kev
15
+ # # url_ver=Z39.88-2004&ctx_tim=2007-10-29T12%3A18%3A53-0400&ctx_ver=Z39.88-2004&ctx_enc=info%3Aofi%2Fenc%3AUTF-8&ctx_id=&rft.issn=0306-4573&rft.aulast=Bollen&rft_val_fmt=info%3Aofi%2Ffmt%3Axml%3Axsd%3Ajournal&rft_id=info%3Adoi%2F10.1016%2Fj.ipm.2005.03.024&rfr_id=info%3Asid%2Fgoogle
16
+
17
+ class ContextObject
18
+
19
+ attr_accessor(:referent, :referringEntity, :requestor, :referrer, :serviceType, :resolver, :custom)
20
+ attr_reader(:admin)
21
+ @@defined_entities = {"rft"=>"referent", "rfr"=>"referrer", "rfe"=>"referring-entity", "req"=>"requestor", "svc"=>"service-type", "res"=>"resolver"}
22
+
23
+ # Creates a new ContextObject object and initializes the ContextObjectEntities.
24
+
25
+ def initialize()
26
+ @referent = ReferentEntity.new()
27
+ @referringEntity = ReferringEntity.new()
28
+ @requestor = RequestorEntity.new()
29
+ @referrer = ReferrerEntity.new()
30
+ @serviceType = [ServiceTypeEntity.new()]
31
+ @resolver = [ResolverEntity.new()]
32
+ @custom = []
33
+ @admin = {"ctx_ver"=>{"label"=>"version", "value"=>"Z39.88-2004"}, "ctx_tim"=>{"label"=>"timestamp", "value"=>DateTime.now().to_s}, "ctx_id"=>{"label"=>"identifier", "value"=>""}, "ctx_enc"=>{"label"=>"encoding", "value"=>"info:ofi/enc:UTF-8"}}
34
+ end
35
+
36
+ def deep_copy
37
+ cloned = ContextObject.new
38
+ cloned.import_context_object( self )
39
+ return cloned
40
+ end
41
+
42
+ # Serialize the ContextObject to XML.
43
+
44
+ def xml
45
+ doc = REXML::Document.new()
46
+ coContainer = doc.add_element "ctx:context-objects"
47
+ coContainer.add_namespace("ctx","info:ofi/fmt:xml:xsd:ctx")
48
+ coContainer.add_namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
49
+ coContainer.add_attribute("xsi:schemaLocation", "info:ofi/fmt:xml:xsd:ctx http://www.openurl.info/registry/docs/info:ofi/fmt:xml:xsd:ctx")
50
+ co = coContainer.add_element "ctx:context-object"
51
+ @admin.each_key do |k|
52
+ co.add_attribute(@admin[k]["label"], @admin[k]["value"])
53
+ end
54
+
55
+ [@referent, @referringEntity, @requestor, @referrer].each do | ent |
56
+ ent.xml(co) unless ent.empty?
57
+ end
58
+
59
+ [@serviceType, @resolver, @custom].each do |entCont|
60
+ entCont.each do |ent|
61
+ ent.xml(co) unless ent.empty?
62
+ end
63
+ end
64
+
65
+ return doc.to_s
66
+ end
67
+
68
+ # Alias for .xml
69
+
70
+ def sap2
71
+ return xml
72
+ end
73
+
74
+ # Output the ContextObject as a Key-encoded value string. Pass a boolean
75
+ # true argument if you do not want the ctx_tim key included.
76
+
77
+ def kev(no_date=false)
78
+ require 'cgi'
79
+ kevs = ["url_ver=Z39.88-2004"]
80
+
81
+ # Loop through the administrative metadata
82
+ @admin.each_key do |k|
83
+ next if k == "ctx_tim" && no_date
84
+ kevs.push(k+"="+CGI.escape(@admin[k]["value"].to_s)) if @admin[k]["value"]
85
+ end
86
+
87
+ [@referent, @referringEntity, @requestor, @referrer].each do | ent |
88
+ kevs.push(ent.kev) unless ent.empty?
89
+ end
90
+
91
+ [@serviceType, @resolver, @custom].each do |entCont|
92
+ entCont.each do |ent|
93
+ kevs.push(ent.kev) unless ent.empty?
94
+ end
95
+ end
96
+ return kevs.join("&")
97
+ end
98
+
99
+ # Outputs the ContextObject as a ruby hash.
100
+
101
+ def to_hash
102
+ co_hash = {"url_ver"=>"Z39.88-2004"}
103
+
104
+ @admin.each_key do |k|
105
+ co_hash[k]=@admin[k]["value"] if @admin[k]["value"]
106
+ end
107
+
108
+ [@referent, @referringEntity, @requestor, @referrer].each do | ent |
109
+ co_hash.merge!(ent.to_hash) unless ent.empty?
110
+ end
111
+
112
+ [@serviceType, @resolver, @custom].each do |entCont|
113
+ entCont.each do |ent|
114
+ co_hash.merge!(ent.to_hash) unless ent.empty?
115
+ end
116
+ end
117
+ return co_hash
118
+ end
119
+
120
+ # Alias for .kev
121
+
122
+ def sap1
123
+ return kev
124
+ end
125
+
126
+ # Outputs a COinS (ContextObject in SPANS) span tag for the ContextObject.
127
+ # Arguments are any other CSS classes you want included and the innerHTML
128
+ # content.
129
+
130
+ def coins (classnames=nil, innerHTML=nil)
131
+ return "<span class='Z3988 #{classnames}' title='"+CGI.escapeHTML(self.kev(true))+"'>#{innerHTML}</span>"
132
+ end
133
+
134
+ # Adds another ServiceType entity to the context object and returns the
135
+ # array index of the new object.
136
+
137
+ def add_service_type_entity
138
+ @serviceType << ServiceTypeEntity.new
139
+ return @serviceType.index(@serviceType.last)
140
+ end
141
+
142
+ # Adds another Resolver entity to the context object and returns the
143
+ # array index of the new object.
144
+
145
+ def add_resolver_entity
146
+ @resolver << ResolverEntity.new
147
+ return @resolver.index(@resolver.last)
148
+ end
149
+
150
+ # Adds a custom entity to the ContextObject and returns array index of the
151
+ # new object. Expects an abbreviation and label for KEV and XML output.
152
+
153
+ def add_custom_entity(abbr=nil, label=nil)
154
+ @custom << CustomEntity.new(abbr, label)
155
+ return @custom.index(@custom.last)
156
+ end
157
+
158
+ # Returns the appropriate CustomEntity for the given entity abbreviation.
159
+
160
+ def custom_entity(abbr)
161
+ return @custom.find { |c| c.abbr == abbr }
162
+ end
163
+
164
+ # Sets a ContextObject administration field.
165
+
166
+ def set_administration_key(key, val)
167
+ raise ArgumentException, "#{key} is not a valid admin key!" unless @admin.keys.index(key)
168
+ @admin[key]["value"] = val
169
+ end
170
+
171
+ # Imports an existing Key-encoded value string and sets the appropriate
172
+ # entities.
173
+
174
+ def import_kev(kev)
175
+ co = CGI::parse(kev)
176
+ co2 = {}
177
+ co.each_key do |k|
178
+ # Only take the first value from the value array
179
+ co2[k] = co[k][0]
180
+ end
181
+ self.import_hash(co2)
182
+ end
183
+
184
+ # Initialize a new ContextObject object from an existing KEV
185
+
186
+ def self.new_from_kev(kev)
187
+ co = self.new
188
+ co.import_kev(kev)
189
+ return co
190
+ end
191
+
192
+ # Imports an existing XML encoded context object and sets the appropriate
193
+ # entities
194
+
195
+ def import_xml(xml)
196
+ doc = REXML::Document.new xml
197
+ # Cut to the context object
198
+ ctx = REXML::XPath.first(doc, ".//ctx:context-object", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
199
+ ctx.attributes.each do |attr, val|
200
+ @admin.each do |adm, vals|
201
+ self.set_administration_key(adm, val) if vals["label"] == attr
202
+ end
203
+ end
204
+ ctx.to_a.each do | ent |
205
+ if @@defined_entities.value?(ent.name())
206
+ var = @@defined_entities.keys[@@defined_entities.values.index(ent.name())]
207
+ meth = "import_#{var}_node"
208
+ self.send(meth, ent)
209
+ else
210
+ self.import_custom_node(ent)
211
+ end
212
+ end
213
+ end
214
+
215
+ # Initialize a new ContextObject object from an existing XML ContextObject
216
+
217
+ def self.new_from_xml(xml)
218
+ co = self.new
219
+ co.import_xml(xml)
220
+ return co
221
+ end
222
+
223
+ # Searches the Custom Entities for the key/value pair and returns an array
224
+ # of the @custom array keys of any matches.
225
+
226
+ def search_custom_entities(key, val)
227
+ matches = []
228
+ @custom.each do |cus|
229
+ begin
230
+ matches << @custom.index(cus) if cus.instance_variable_get('@'+key) == val
231
+ rescue NameError
232
+ next
233
+ end
234
+ end
235
+ return matches
236
+ end
237
+
238
+ # Imports an existing hash of ContextObject values and sets the appropriate
239
+ # entities.
240
+
241
+ def import_hash(hash)
242
+ ref = {}
243
+ openurl_keys = ["url_ver", "url_tim", "url_ctx_fmt"]
244
+ hash.each do |key, val|
245
+ if openurl_keys.include?(key)
246
+ next # None of these matter much for our purposes
247
+ elsif @admin.has_key?(key)
248
+ self.set_administration_key(key, val)
249
+ elsif key.downcase.match(/^[a-z]{3}_val_fmt$/)
250
+ # Realistically should only be rft or rfe: get the format
251
+ (entity, v, fmt) = key.split("_")
252
+ ent = self.translate_abbr(entity)
253
+ eval("@"+ent).set_format(val)
254
+ elsif key.match(/^[a-z]{3}_ref/)
255
+ # determines if we have a by-reference context object
256
+ (entity, v, fmt) = key.split("_")
257
+ ent = self.translate_abbr(entity)
258
+ # by-reference requires two fields, format and location, if this is
259
+ # the first field we've run across, set a place holder until we get
260
+ # the other value
261
+ unless ref[entity]
262
+ if fmt
263
+ ref_key = "format"
264
+ else
265
+ ref_key = "location"
266
+ end
267
+ ref[entity] = [ref_key, val]
268
+ else
269
+ if ref[entity][0] == "format"
270
+ eval("@"+ent).set_reference(val, ref[entity][1])
271
+ else
272
+ eval("@"+ent).set_reference(ref[entity][1], val)
273
+ end
274
+ end
275
+ elsif key.match(/^[a-z]{3}_id$/)
276
+ # Get the entity identifier
277
+ (entity, v) = key.split("_")
278
+ ent = self.translate_abbr(entity)
279
+ eval("@"+ent).set_identifier(val)
280
+ elsif key.match(/^[a-z]{3}_dat$/)
281
+ # Get any private data
282
+ (entity, v) = key.split("_")
283
+ ent = self.translate_abbr(entity)
284
+ eval("@"+ent).set_private_data(val)
285
+ else
286
+ # collect the entity metadata
287
+ keyparts = key.split(".")
288
+ if keyparts.length > 1
289
+ # This is 1.0 OpenURL
290
+ ent = self.translate_abbr(keyparts[0])
291
+ eval("@"+ent).set_metadata(keyparts[1], val)
292
+ else
293
+ # This is a 0.1 OpenURL. Your mileage may vary on how accurately
294
+ # this maps.
295
+ if key == 'id'
296
+ @referent.set_identifier(val)
297
+ elsif key == 'sid'
298
+ @referrer.set_identifier("info:sid/"+val.to_s)
299
+ else
300
+ @referent.set_metadata(key, val)
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ # Initialize a new ContextObject object from an existing key/value hash
307
+
308
+ def self.new_from_hash(hash)
309
+ co = self.new
310
+ co.import_hash(hash)
311
+ return co
312
+ end
313
+
314
+ # if we don't have a referent format (most likely because we have a 0.1
315
+ # OpenURL), try to determine something from the genre. If that doesn't
316
+ # exist, just call it a journal since most 0.1 OpenURLs would be one,
317
+ # anyway.
318
+ unless @referent.format
319
+ fmt = case @referent.metadata['genre']
320
+ when /article|journal|issue|proceeding|conference|preprint/ then 'journal'
321
+ when /book|bookitem|report|document/ then 'book'
322
+ else 'journal'
323
+ end
324
+ @referent.set_format(fmt)
325
+ end
326
+ end
327
+
328
+ # Translates the abbreviated entity (rft, rfr, etc.) to the associated class
329
+ # name. For repeatable entities, uses the first object in the array. Returns
330
+ # a string of the object name which would then be eval'ed to call a method
331
+ # upon.
332
+
333
+ def translate_abbr(abbr)
334
+ if @@defined_entities.has_key?abbr
335
+ ent = @@defined_entities[abbr]
336
+ if ent == "service-type"
337
+ ent = "serviceType[0]"
338
+ elsif ent == "resolver"
339
+ ent = "resolver[0]"
340
+ elsif ent == "referring-entity"
341
+ ent = "referringEntity"
342
+ end
343
+ else
344
+ idx = self.search_custom_entities("abbr", abbr)
345
+ if idx.length == 0
346
+ self.add_custom_entity(abbr)
347
+ idx = self.search_custom_entities("abbr", abbr)
348
+ end
349
+ ent = "custom["+idx[0].to_s+"]"
350
+ end
351
+ return ent
352
+ end
353
+
354
+ # Imports an existing OpenURL::ContextObject object and sets the appropriate
355
+ # entity values.
356
+
357
+ def import_context_object(context_object)
358
+ @admin.each_key { |k|
359
+ self.set_administration_key(k, context_object.admin[k]["value"])
360
+ }
361
+ [context_object.referent, context_object.referringEntity, context_object.requestor, context_object.referrer].each {| ent |
362
+ unless ent.empty?
363
+ ['identifier', 'format', 'private_data'].each { |var|
364
+ unless ent.send(var).nil?
365
+ unless ent.kind_of?(OpenURL::ReferringEntity)
366
+ eval("@"+ent.label.downcase).send('set_'+var,ent.send(var))
367
+ else
368
+ @referringEntity.send('set_'+var,ent.send(var))
369
+ end
370
+ end
371
+ }
372
+ unless ent.reference["format"].nil? or ent.reference["format"].nil?
373
+ unless ent.kind_of?(OpenURL::ReferringEntity)
374
+ eval("@"+ent.label.downcase).set_reference(ent.reference["location"], ent.reference["format"])
375
+ else
376
+ @referringEntity.set_referent(ent.reference["location"], ent.reference["format"])
377
+ end
378
+ end
379
+ ent.metadata.each_key { |k|
380
+ unless ent.metadata[k].nil?
381
+ unless ent.kind_of?(OpenURL::ReferringEntity)
382
+ eval("@"+ent.label.downcase).set_metadata(k, ent.metadata[k])
383
+ else
384
+ @referringEntity.set_metadata(k, ent.metadata[k])
385
+ end
386
+ end
387
+ }
388
+ end
389
+ }
390
+ context_object.serviceType.each { |svc|
391
+ if @serviceType[0].empty?
392
+ @serviceType[0] = svc
393
+ else
394
+ idx = self.add_service_type_entity
395
+ @serviceType[idx] = svc
396
+ end
397
+
398
+ }
399
+ context_object.resolver.each { |res|
400
+ if @resolver[0].empty?
401
+ @resolver[0] = res
402
+ else
403
+ idx = self.add_resolver_entity
404
+ @resolver[idx] = res
405
+ end
406
+
407
+ }
408
+ context_object.custom.each { |cus|
409
+ idx = self.add_custom_entity(cus.abbr, cus.label)
410
+ @custom[idx] = cus
411
+ }
412
+ end
413
+
414
+ # Initialize a new ContextObject object from an existing
415
+ # OpenURL::ContextObject
416
+
417
+ def self.new_from_context_object(context_object)
418
+ co = self.new
419
+ co.import_context_object(context_object)
420
+ return co
421
+ end
422
+
423
+ protected
424
+
425
+ def import_rft_node(node)
426
+ self.import_xml_common(@referent, node)
427
+ self.import_xml_mbv_ref(@referent, node)
428
+ end
429
+
430
+ def import_rfe_node(node)
431
+ self.import_xml_common(@referringEntity, node)
432
+ self.import_xml_mbv_ref(@referringEntity, node)
433
+ end
434
+
435
+ def import_rfr_node(node)
436
+ self.import_xml_common(@referrer, node)
437
+ self.import_xml_mbv(@referrer, node)
438
+ end
439
+
440
+ def import_req_node(node)
441
+ self.import_xml_common(@requestor, node)
442
+ self.import_xml_mbv(@requestor, node)
443
+ end
444
+
445
+ def import_svc_node(node)
446
+ if @serviceType[0].empty?
447
+ key = 0
448
+ else
449
+ key = self.add_service_type_entity
450
+ end
451
+ self.import_xml_common(@serviceType[key], node)
452
+ self.import_xml_mbv(@serviceType[key], node)
453
+ end
454
+
455
+ def import_custom_node(node)
456
+ key = self.add_custom_entity(node.name())
457
+ self.import_xml_commom(@custom[key], node)
458
+ self.import_xml_mbv(@custom[key], node)
459
+ end
460
+
461
+ def import_res_node(node)
462
+ if @resolver[0].empty?
463
+ key = 0
464
+ else
465
+ key = self.add_resolver_entity
466
+ end
467
+ self.import_xml_common(@resolver[key], node)
468
+ self.import_xml_mbv(@resolver[key], node)
469
+ end
470
+
471
+ # Parses the data that should apply to all XML context objects
472
+
473
+ def import_xml_common(ent, node)
474
+ fmt = REXML::XPath.first(node, ".//ctx:format", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
475
+ ent.set_format(fmt.get_text.value) if fmt and fmt.has_text
476
+
477
+ id = REXML::XPath.first(node, ".//ctx:identifier", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
478
+ ent.set_identifier(id.get_text.value) if id and id.has_text?
479
+
480
+ priv = REXML::XPath.first(node, ".//ctx:private-data", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
481
+ ent.set_private_data(priv.get_text.value) if priv and priv.has_text?
482
+
483
+ ref = REXML::XPath.first(node, ".//ctx:metadata-by-ref", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
484
+ if ref
485
+ ref.to_a.each do |r|
486
+ if r.name() == "format"
487
+ format = r.get_text.value
488
+ else
489
+ location = r.get_text.value
490
+ end
491
+ ent.set_reference(location, format)
492
+ end
493
+ end
494
+ end
495
+
496
+ # Parses metadata-by-val data
497
+
498
+ def import_xml_mbv(ent, node)
499
+ mbv = REXML::XPath.first(node, ".//ctx:metadata-by-val", {"ctx"=>"info:ofi/fmt:xml:xsd:ctx"})
500
+
501
+ if mbv
502
+ mbv.to_a.each { |m|
503
+ ent.set_metadata(m.name(), m.get_text.value)
504
+ }
505
+ end
506
+ end
507
+
508
+ # Referent and ReferringEntities place their metadata-by-val inside
509
+ # the format element
510
+
511
+ def import_xml_mbv_ref(ent, node)
512
+ ns = "info:ofi/fmt:xml:xsd:"+ent.format
513
+ mbv = REXML::XPath.first(node, ".//fmt:"+ent.format, {"fmt"=>ns})
514
+ if mbv
515
+ mbv.to_a.each { |m|
516
+ if m.has_text?
517
+ ent.set_metadata(m.name(), m.get_text.value)
518
+ end
519
+ if m.has_elements?
520
+ m.to_a.each { | md |
521
+ if md.has_text?
522
+ ent.set_metadata(md.name(), md.get_text.value)
523
+ end
524
+ }
525
+ end
526
+ }
527
+ end
528
+ end
529
+ end
530
+ end
@@ -0,0 +1,303 @@
1
+ module OpenURL
2
+
3
+ # The ContextObjectEntity is a generic class to define an entity. It should
4
+ # not be initialized directly, only through one of its children:
5
+ # ReferentEntity, ReferrerEntity, ReferringEntity, ResolverEntity,
6
+ # ServiceTypeEntity, or CustomEntity
7
+
8
+ class ContextObjectEntity
9
+ # identifiers should always be an array, but it might be an empty one.
10
+ attr_reader(:identifiers, :reference, :format, :metadata, :private_data, :abbr, :label)
11
+
12
+ def initialize
13
+ @identifiers = []
14
+ @reference = {"format"=>nil, "location"=>nil}
15
+ @format = nil
16
+ @metadata = {}
17
+ @private_data = nil
18
+ end
19
+
20
+ # Sets the location and format of a by-reference context object entity
21
+
22
+ def set_reference(loc, fmt)
23
+ @reference["location"] = loc
24
+ @reference["format"] = fmt
25
+ end
26
+
27
+ # Should really be called "add identifier", since we can have more
28
+ # than one. But for legacy, it's "set_identifier".
29
+ def add_identifier(val)
30
+ @identifiers.push( self.class.normalize_id(val) )
31
+ end
32
+ alias :set_identifier :add_identifier
33
+
34
+ # We can actually have more than one, but certain code calls this
35
+ # method as if there's only one. We return the first.
36
+ def identifier
37
+ return @identifiers[0]
38
+ end
39
+
40
+
41
+ def set_private_data(val)
42
+ @private_data = val
43
+ end
44
+
45
+ def set_metadata(key, val)
46
+ @metadata[key] = val
47
+ end
48
+
49
+ def get_metadata(key)
50
+ return @metadata[key]
51
+ end
52
+
53
+ def set_format(format)
54
+ @format = format
55
+ end
56
+
57
+ # Serializes the entity to XML and attaches it to the supplied REXML element.
58
+
59
+ def xml(co_elem)
60
+ meta = {"container"=>co_elem.add_element("ctx:"+@label)}
61
+
62
+ if @metadata.length > 0 or @format
63
+ meta["metadata-by-val"] = meta["container"].add_element("ctx:metadata-by-val")
64
+ if @format
65
+ meta["format"] = meta["container"].add_element("ctx:format")
66
+ meta["format"].text = "info:ofi/fmt:xml:xsd:"+@format
67
+ end
68
+ if @metadata.length > 0
69
+ meta["metadata"] = meta["metadata-by-val"].add_element("ctx:metadata")
70
+ @metadata.each do |k,v|
71
+ meta[k] = meta["metadata"].add_element("ctx:"+k)
72
+ meta[k].text = v
73
+ end
74
+ end
75
+ end
76
+ if @reference["format"]
77
+ meta["metadata-by-ref"] = meta["container"].add_element("ctx:metadata-by-ref")
78
+ meta["ref_format"] = meta["metadata-by-ref"].add_element("ctx:format")
79
+ meta["ref_format"].text = @reference["format"]
80
+ meta["ref_loc"] = meta["metadata-by-ref"].add_element("ctx:location")
81
+ meta["ref_loc"].text = @reference["location"]
82
+ end
83
+
84
+ @identifiers.each do |id|
85
+ # Yes, meta["identifier"] will get over-written if there's more than
86
+ # one identifier. But I dont' think this meta hash is used for much
87
+ # I don't think it's a problem. -JR
88
+ meta["identifier"] = meta["container"].add_element("ctx:identifier")
89
+ meta["identifier"].text = id
90
+ end
91
+ if @private_data
92
+ meta["private-data"] = meta["container"].add_element("ctx:private-data")
93
+ meta["private-data"].text = @private_data
94
+ end
95
+ return co_elem
96
+ end
97
+
98
+ # Outputs the entity as a KEV array
99
+
100
+ def kev
101
+ kevs = []
102
+
103
+ @metadata.each do |k,v|
104
+ kevs << "#{@abbr}.#{k}="+CGI.escape(v) if v
105
+ end
106
+
107
+ kevs << "#{@abbr}_val_fmt="+CGI.escape("info:ofi/fmt:xml:xsd:#{@format}") if @format
108
+
109
+ if @reference["format"]
110
+ kevs << "#{@abbr}_ref_fmt="+CGI.escape(@reference["format"])
111
+ kevs << "#{@abbr}_ref="+CGI.escape(@reference["location"])
112
+ end
113
+
114
+ @identifiers.each do |id|
115
+ kevs << "#{@abbr}_id="+CGI.escape(id)
116
+ end
117
+
118
+ kevs << "#{@abbr}_dat="+CGI.escape(@private_data) if @private_data
119
+
120
+ return kevs
121
+ end
122
+
123
+ # Outputs the entity as a hash
124
+
125
+ def to_hash
126
+ co_hash = {}
127
+
128
+ @metadata.each do |k,v|
129
+ co_hash["#{@abbr}.#{k}"]=v if v
130
+ end
131
+
132
+ co_hash["#{@abbr}_val_fmt"]="info:ofi/fmt:xml:xsd:#{@format}" if @format
133
+
134
+ if @reference["format"]
135
+ co_hash["#{@abbr}_ref_fmt"]=@reference["format"]
136
+ co_hash["#{@abbr}_ref"]=@reference["location"]
137
+ end
138
+
139
+ @identifiers.each do |id|
140
+ # Put em in a list.
141
+ co_hash["#{@abbr}_id"] ||= Array.new
142
+ co_hash["#{@abbr}_id"].push( id )
143
+ end
144
+ co_hash["#{@abbr}_dat"]=@private_data if @private_data
145
+
146
+ return co_hash
147
+ end
148
+
149
+ # Checks to see if the entity has any metadata set.
150
+
151
+ def empty?
152
+ return false if (@identifiers.length > 0 ) or @reference["format"] or @reference["location"] or @metadata.length > 0 or @format or @private_data
153
+ return true
154
+ end
155
+
156
+ # Serializes the metadata values for Referent and ReferringEntity entities
157
+ # since their schema is a little different.
158
+
159
+ def xml_for_ref_entity(co_elem)
160
+ meta = {"container"=>co_elem.add_element("ctx:"+@label)}
161
+
162
+ if @metadata.length > 0 or @format
163
+ meta["metadata-by-val"] = meta["container"].add_element("ctx:metadata-by-val")
164
+ if @format
165
+ meta["format"] = meta["metadata-by-val"].add_element("ctx:format")
166
+ meta["format"].text = "info:ofi/fmt:xml:xsd:"+@format
167
+
168
+ if @metadata.length > 0
169
+ meta["metadata"] = meta["metadata-by-val"].add_element("ctx:metadata")
170
+ meta["format_container"] = meta["metadata"].add_element(@format)
171
+ meta["format_container"].add_namespace(@abbr, meta["format"].text)
172
+ meta["format_container"].add_attribute("xsi:schemaLocation", meta["format"].text+" http://www.openurl.info/registry/docs/info:ofi/fmt:xml:xsd:"+@format)
173
+ @metadata.each do |k,v|
174
+ meta[k] = meta["format_container"].add_element(@abbr+":"+k)
175
+ meta[k].text = v
176
+ end
177
+ end
178
+ end
179
+ end
180
+ if @reference["format"]
181
+ meta["metadata-by-ref"] = meta["container"].add_element("ctx:metadata-by-ref")
182
+ meta["ref_format"] = meta["metadata-by-ref"].add_element("ctx:format")
183
+ meta["ref_format"].text = @reference["format"]
184
+ meta["ref_loc"] = meta["metadata-by-ref"].add_element("ctx:location")
185
+ meta["ref_loc"].text = @reference["location"]
186
+ end
187
+
188
+ @identifiers.each do |id|
189
+ # Yes, if there's more than one, meta["identifier"] will get
190
+ # overwritten with last. I don't think this is a problem, cause
191
+ # meta["identifier"] isn't used anywhere.
192
+ meta["identifier"] = meta["container"].add_element("ctx:identifier")
193
+ meta["identifier"].text = id
194
+ end
195
+ if @private_data
196
+ meta["private-data"] = meta["container"].add_element("ctx:private-data")
197
+ meta["private-data"].text = @private_data
198
+ end
199
+ return co_elem
200
+ end
201
+
202
+ # Switch old 0.1 style ids to new 1.0 style ids.
203
+ # Eg, turn << doi:[x] >> into << info:doi/[x] >>
204
+ def self.normalize_id(value)
205
+ # info, urn, and http are all good new style 1.0 ids.
206
+ # we assume anything else is not. Is this a valid assumption?
207
+ unless ( (value.slice(0,5) == 'info:') ||
208
+ (value.slice(0,4) == 'urn:') ||
209
+ (value.slice(0,5) == 'http:') )
210
+ value = value.sub(/^([a-z,A-Z]+)\:/, 'info:\1/')
211
+ end
212
+
213
+ return value
214
+ end
215
+
216
+ end
217
+
218
+ class ReferentEntity < ContextObjectEntity
219
+ def initialize
220
+ super()
221
+ @abbr = "rft"
222
+ @label = "referent"
223
+ end
224
+ def xml(co_elem)
225
+ return self.xml_for_ref_entity(co_elem)
226
+ end
227
+ def set_format(fmt)
228
+ if fmt.split(":").length > 1
229
+ @format = fmt.split(":").last
230
+ else
231
+ @format = fmt
232
+ end
233
+ end
234
+ end
235
+
236
+ class ReferringEntity < ContextObjectEntity
237
+ def initialize
238
+ super()
239
+ @abbr = "rfe"
240
+ @label = "referring-entity"
241
+ end
242
+ def xml(co_elem)
243
+ return self.xml_for_ref_entity(co_elem)
244
+ end
245
+ def set_format(fmt)
246
+ if fmt.split(":").length > 1
247
+ @format = fmt.split(":").last
248
+ else
249
+ @format = fmt
250
+ end
251
+ end
252
+ end
253
+
254
+ class ReferrerEntity < ContextObjectEntity
255
+ def initialize
256
+ super()
257
+ @abbr = "rfr"
258
+ @label = "referrer"
259
+ end
260
+ end
261
+
262
+ class RequestorEntity < ContextObjectEntity
263
+ def initialize
264
+ super()
265
+ @abbr = "req"
266
+ @label = "requestor"
267
+ end
268
+ end
269
+
270
+ class ServiceTypeEntity < ContextObjectEntity
271
+ def initialize
272
+ super()
273
+ @abbr = "svc"
274
+ @label = "service-type"
275
+ end
276
+ end
277
+ class ResolverEntity < ContextObjectEntity
278
+ def initialize
279
+ super()
280
+ @abbr = "res"
281
+ @label = "resolver"
282
+ end
283
+ end
284
+
285
+ class CustomEntity < ContextObjectEntity
286
+ attr_accessor :abbr, :label
287
+ def initialize(abbr=nil, label=nil)
288
+ super()
289
+ unless abbr
290
+ @abbr = "cus"
291
+ else
292
+ @abbr = abbr
293
+ end
294
+ unless label
295
+ @label = @abbr
296
+ else
297
+ @abbr = label
298
+ end
299
+
300
+ end
301
+ end
302
+
303
+ end
@@ -0,0 +1,172 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module OpenURL
5
+ # The Transport class is intended to be used to deliver ContextObject objects
6
+ # to an OpenURL enabled host. Currently only HTTP is supported.
7
+ # Usage:
8
+ # require 'openurl'
9
+ # include OpenURL
10
+ # context_object = ContextObject.new_from_kev('ctx_enc=info%3Aofi%2Fenc%3AUTF-8&ctx_ver=Z39.88-2004&rft.genre=article&rft_id=info%3Adoi%2F10.1016%2Fj.ipm.2005.03.024&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Aarticle&url_ctx_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Actx&url_ver=Z39.88-2004')
11
+ # transport = Transport.new('http://demo.exlibrisgroup.com:9003/lr_3', context_object)
12
+ # transport.get
13
+ # puts tranport.response
14
+
15
+ class Transport
16
+ attr_accessor(:extra_args, :ctx_id)
17
+ attr_reader(:response, :request_string, :context_objects, :code, :message)
18
+
19
+ # Creates the transport object which can be used to initiate
20
+ # subsequent requests. The contextobject argument can be an OpenURL
21
+ # ContextObject object, and array of ContextObjects or nil. http_arguments
22
+ # set the Net::HTTP attributes: {:open_timeout=>3, :read_timeout=>5}, etc.
23
+
24
+ def initialize(target_base_url, contextobject=nil, http_arguments={})
25
+ @uri = URI.parse(target_base_url)
26
+ @context_objects = []
27
+ self.add_context_object(contextobject) if contextobject
28
+ @url_ver = "Z39.88-2004"
29
+ @extra_args = {}
30
+ @client = Net::HTTP.new(@uri.host, @uri.port)
31
+ @client.open_timeout = (http_arguments[:open_timeout]||3)
32
+ @client.read_timeout = (http_arguments[:read_timeout]||5)
33
+ end
34
+
35
+ # Can take either an OpenURL::ContextObject or an array of ContextObjects
36
+ # to send to the Transport target
37
+
38
+ def add_context_object(contextobject)
39
+
40
+ if contextobject.is_a?(OpenURL::ContextObject)
41
+ @context_objects << contextobject
42
+ elsif contextobject.is_a?(Array)
43
+ contextobject.each do | co |
44
+ raise ArgumentError, "Each element in array much be an OpenURL::ContextObject!" unless co.is_a?(OpenURL::ContextObject)
45
+ @context_objects << co
46
+ end
47
+ else
48
+ raise ArgumentError, "Argument must be a ContextObject or array of ContextObjects!, #{contextobject.class} sent."
49
+ end
50
+ end
51
+
52
+ # Accepts either a ContextObject or array index to remove from array being
53
+ # sent to the Transport target
54
+
55
+ def remove_context_object(element)
56
+ idx = case element.class
57
+ when Fixnum then element
58
+ when OpenURL::ContextObject then @context_objects.index(element)
59
+ else raise ArgumentError, "Invalid argument for element"
60
+ end
61
+ @context_objects.delete_at(idx)
62
+ end
63
+
64
+ # Perform an inline HTTP GET request. Only one context object can be sent
65
+ # via GET, so pass the index of the desired context object (defaults to the
66
+ # first)
67
+
68
+ def get(idx=0)
69
+ extra = ""
70
+ @extra_args.each do | key, val |
71
+ extra << "&#{key}=#{val}"
72
+ end
73
+ self.parse_response(@client.get("#{@uri.path}?#{@context_objects[idx].kev}#{extra}"))
74
+ end
75
+
76
+ # Sends an inline transport request. YOu can specify which HTTP method
77
+ # to use. Since you can only send one context object per inline request,
78
+ # the second argument is the index of the desired context object.
79
+
80
+ def transport_inline(method="GET", idx=0)
81
+ return(self.get(idx)) if method=="GET"
82
+ return(self.post({:inline=>true, :index=>idx})) if method=="POST"
83
+ end
84
+
85
+ # Sends an by-value transport request. YOu can specify which HTTP method
86
+ # to use. Since a GET request is effectively the same as an inline request,
87
+ # the index of which context object must be specified (defaults to 0).
88
+
89
+ def transport_by_val(method="POST", idx=0)
90
+ return(self.get(idx)) if method=="GET"
91
+ return(self.post) if method=="POST"
92
+ end
93
+
94
+ # POSTs an HTTP request to the transport target. To send an inline request,
95
+ # include a hash that looks like: {:inline=>true, :index=>n} (:index defaults
96
+ # to 0. Transport.post must be used to send multiple context objects to a
97
+ # target.
98
+
99
+ def post(args={})
100
+ # Inline requests send the context object as a hash
101
+ if args[:inline]
102
+ self.parse_response(self.post_http(@context_objects[(args[:index]||0)].to_hash.merge(@extra_args.merge({"url_ctx_fmt"=>"info:ofi/fmt:kev:mtx:ctx"}))))
103
+ return
104
+ end
105
+ ctx_hash = {"url_ctx_fmt" => "info:ofi/fmt:xml:xsd:ctx"}
106
+ # If we're only sending one context object, use that, otherwise concatenate
107
+ # them.
108
+ if @context_objects.length == 1
109
+ ctx_hash["url_ctx_val"] = @context_objects[0].xml
110
+ else
111
+ ctx_hash["url_ctx_val"] = self.merge_context_objects
112
+ end
113
+ @context_objects[0].admin.each do | key, hsh |
114
+ ctx_hash[key] = hsh["value"]
115
+ end
116
+
117
+ self.parse_response(self.post_http(ctx_hash.merge(@extra_args)))
118
+ end
119
+
120
+ # For a multiple context object request, takes the first context object in
121
+ # the context_objects attribute, and adds the other context objects to it,
122
+ # under /ctx:context-objects/ctx:context-object and serializes it all as XML.
123
+ # Returns a string of the XML document
124
+
125
+ def merge_context_objects
126
+ ctx_doc = REXML::Document.new(@context_objects[0].xml)
127
+ root = ctx_doc.root
128
+ @context_objects.each do | ctx |
129
+ next if @context_objects.index(ctx) == 0
130
+ c_doc = REXML::Document.new(ctx.xml)
131
+ c_elm = c_doc.elements['ctx:context-objects/ctx:context-object']
132
+ root.add_element(c_elm)
133
+ end
134
+ return ctx_doc.to_s
135
+ end
136
+
137
+ # Deprecated. Set by-reference in OpenURL::ContextObject and use .get or
138
+ # .post
139
+
140
+ def transport_by_ref(fmt, ref, method="GET")
141
+ md = "url_ver=Z39.88-2004&url_ctx_fmt="+CGI.escape(fmt)+"&url_tim="+CGI.escape(DateTime.now().to_s)
142
+ if method == "GET"
143
+ parse.response(@client.get("#{@uri.path}?#{md}&url_ctx_ref="+CGI.escape(ref)))
144
+ else
145
+ args = {"url_ver"=>"Z39.88-2004",
146
+ "url_ctx_fmt"=>fmt,
147
+ "url_tim"=>DateTime.now().to_s,
148
+ "url_ctx_ref" => ref}
149
+ args = args.merge(@extra_args) unless @extra_args.empty?
150
+
151
+ self.parse_response(self.post_http(args))
152
+ end
153
+ end
154
+
155
+ protected
156
+
157
+ # Reads the HTTP::Response object and sets the response, code and message
158
+ # attributes
159
+
160
+ def parse_response(response)
161
+ @response = response.body
162
+ @code = response.code
163
+ @message = response.message
164
+ end
165
+
166
+ # Sends the actual POST request.
167
+
168
+ def post_http(args)
169
+ return(Net::HTTP.post_form @uri, args)
170
+ end
171
+ end
172
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: openurl
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2007-10-29 00:00:00 -04:00
8
+ summary: a Ruby library to create, parse and use NISO Z39.88 OpenURLs
9
+ require_paths:
10
+ - lib
11
+ email: rossfsinger@gmail.com
12
+ homepage: http://openurl.rubyforge.org/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: openurl
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Ross Singer
31
+ files:
32
+ - lib/openurl
33
+ - lib/openurl/context_object.rb
34
+ - lib/openurl/context_object_entity.rb
35
+ - lib/openurl/transport.rb
36
+ - lib/openurl.rb
37
+ test_files: []
38
+
39
+ rdoc_options: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ executables: []
44
+
45
+ extensions: []
46
+
47
+ requirements: []
48
+
49
+ dependencies: []
50
+