openurl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+