traject_horizon 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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in traject_horizon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jonathan Rochkind
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # Traject::Horizon
2
+
3
+ Export MARC records directly from a Horizon ILS rdbms, either as serialized MARC,
4
+ or to then index to Solr.
5
+
6
+ traject-horizon is a plugin for [traject](http://github.com/jrochkind/traject), and
7
+ requires jruby to be installed.
8
+
9
+ Supports embedding copy/item holdings information in exported MARC.
10
+
11
+ Fairly high-performance, should have higher throughput than most existing
12
+ Horizon MARC export options, including the vendor-supplied Windows-only
13
+ 'marcout'. There are probably opportunities for increasing performance
14
+ yet further with more development of multi-threaded processing.
15
+
16
+ ## Installation
17
+
18
+ traject_horizon is a plugin for [traject](http://github.com/jrochkind/traject), install
19
+ them both:
20
+
21
+ $ gem install traject traject_horizon
22
+
23
+ ### Or, if using a Gemfile with your traject project
24
+
25
+ Add this line to your [traject project's Gemfile](https://github.com/jrochkind/traject/blob/master/doc/extending.md#or-with-bundler):
26
+
27
+ gem 'traject_horizon'
28
+
29
+ And then execute:
30
+
31
+ $ bundle install
32
+
33
+ ## Usage
34
+
35
+ I recommend creating a seperate traject configuration file just with
36
+ settings for the Horizon export.
37
+
38
+ ~~~ruby
39
+ # horizon_conf.rb
40
+
41
+ # Require traject/horizon to load the gem, including
42
+ # the Traject::HorizonReader we'll subsequently
43
+ # configure to be used
44
+ require 'traject/horizon'
45
+
46
+ settings do
47
+ store "reader_class_name", "Traject::HorizonReader"
48
+
49
+ # JDBC URL starting with "jdbc:jtds", and either "sybase:"
50
+ # or "sqlserver:", including username on the end but not password:
51
+ provide "horizon.jdbc_url", "jdbc:jtds:sybase://horizonserver.university.edu:2025/horizon_db;user=esys"
52
+
53
+ # DB password in seperate setting
54
+ provide "horizon.jdbc_password", "drilg53"
55
+
56
+ # Do you want to include copy/item holdings information?
57
+ # this setting says to include "top-level" holdings,
58
+ # copy or item but not both. Holdings will be included
59
+ # in tags 991 and 937, although the tags and nature
60
+ # of included holdings is configurable.
61
+ provide "horizon.include_holdings", "direct"
62
+
63
+ # Would you like to exclude certain tags from
64
+ # your Horizon db? If you are including holdings,
65
+ # then it's recommended to exclude 991 and 937 to
66
+ # avoid any collision with the tags we add to represent holdings.
67
+ provide "horizon.exclude_tags", "991,937"
68
+ end
69
+ ~~~
70
+
71
+ There are a variety of additional settings that apply to the HorizonReader,
72
+ especially settings for customizing the item/copy holdings information
73
+ included. See [HorizonReader] inline comment docs.
74
+
75
+ Note by default `staff-only` records are _not_ included in the export,
76
+ but this can be changed in settings.
77
+
78
+ As with all traject settings, string-valued settings can also be supplied
79
+ on the traject command line with `-s setting=value`.
80
+
81
+ ### Export MARC records
82
+
83
+ $ traject -x marcout -c horizon_conf.rb -o marc_files.marc
84
+
85
+ That will export your entire horizon database,,
86
+ using the connection details and configuration from horizon_conf.rb, exporting
87
+ in ISO 2709 binary format to `marc_files.marc`.
88
+
89
+ You can also specify specific ranges of bib#'s to export:
90
+
91
+ $ traject -x marcout -c horizon_conf.rb -o marc_files.marc -s horizon.first_bib=10000 -s horizon.last_bib=10100
92
+ $ traject -x marcout -c horizon_conf.rb -o marc_files.marc -s horizon.only_bib=12345
93
+
94
+ You can export in MarcXML, or in a human readable format for debuging,
95
+ using standard traject `-x marcout` functionality:
96
+
97
+ $ traject -x marcout -c horizon_conf.rb -s marcout.type=xml -o marc_files.xml
98
+
99
+ # leave off the `-o` argument to write to stdout, and view bib# 12345 in
100
+ # human-readable format:
101
+ $ traject -x marcout -c horizon_conf.rb -s marcout.type=human -s horizon.only_bib=12345
102
+
103
+ ### Indexing records to solr
104
+
105
+ Traject is primarily a tool for indexing to solr. You can use `traject-horizon` to
106
+ export from Horizon and send directly through the indexing pipeline, without
107
+ having to serialize MARC to disk first.
108
+
109
+ You would have one or more additional traject configuration files specifying
110
+ your indexing mapping rules, and Solr connection details. See traject
111
+ documentation.
112
+
113
+ Then, simply:
114
+
115
+ $ traject -c horizon_conf.rb -c other_traject_conf.rb
116
+
117
+ ## Note on character encodings
118
+
119
+ By default, traject-horizon assumes the data in your Horizon database is stored
120
+ in the Marc8 encoding. (I think this is true of all Horizon databases?). And by
121
+ default, traject-horizon will transcode it to UTF-8, marking leader byte 9 in any
122
+ exported MARC appropriately (Using the Marc4J AnselConverter class).
123
+
124
+ If you'd like traject to avoid this transcode, you can set the traject
125
+ setting `horizon.destination_encoding` to nil or the empty string, either
126
+ on the command line:
127
+
128
+ traject -x marcout -s horizon.destination_encoding= -c horizon_conf.rb
129
+
130
+ Or in your traject configuration file:
131
+
132
+ settings do
133
+ #...
134
+ provide "horizon.destination_encoding", nil
135
+ end
136
+
137
+ You might want to do this with `marcout` use, perhaps for diagnostics, but
138
+ it shouldn't ever be appropriate for indexing-to-solr use, as there are limited
139
+ facilities for dealing with Marc8 encoding in ruby.
140
+
141
+ Currently, item/copy information may not be treated entirely consistent here,
142
+ there may be edge-case encoding bugs related to non-ascii item/copy notes etc,
143
+ and it may not be possible to output them in Marc8. Sorry.
144
+
145
+ ## Challenges
146
+
147
+ I had to reverse engineer the Horizon database to figure out how to turn it into
148
+ MARC records. I believe I have been succesful, and traject-horizon seems to produce
149
+ the same output as Horizon's own marcout.
150
+
151
+ Hopefully this will remain true in future Horizon versions, I don't think relevant
152
+ aspects of Horizon architecture change very much, but it's always a risk.
153
+
154
+ The two biggest challenges were dealing with character encoding, and dealing
155
+ with merging information from the Horizon bib and auth tables.
156
+
157
+ The translation from Marc8 to UTF8 appears to work properly, _except_
158
+ some known issues with item/copy holding information. item/copy holding
159
+ information may occasionally not transcode properly, and it may not
160
+ be possible to keep item/copy holding info in Marc8. If these become
161
+ an actual problem in practice for anyone, further development can
162
+ probably resolve these issues.
163
+
164
+
165
+ ## Development
166
+
167
+ There is only limited test coverage at the moment, sorry. I couldn't
168
+ quite figure out how to easily provide test coverage when so much
169
+ functionality interacts with a Horizon database.
170
+
171
+ There is some test coverage of the bib/auth merging routines.
172
+
173
+ Test are provided with minitest, and can be run with `rake test`.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+
6
+ task :default => [:test]
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.libs.push 'test', 'test_support'
11
+ end
@@ -0,0 +1,124 @@
1
+ module Traject
2
+
3
+ # Merges 'bib text' and 'auth text' lines from Horizon, using bib text as
4
+ # template when neccesary.
5
+ #
6
+ # merged_str = HorizonBibAuthMerge.new(tag, bib_text_str, auth_text_str).merge!
7
+ #
8
+ # Strings passed in may be mutated for efficiency. So you can only call merge! once, it's just
9
+ # utility.
10
+ class HorizonBibAuthMerge
11
+ attr_reader :bibtext, :authtext, :tag
12
+
13
+ # Pass in bibtext and authtext as String -- you probably need to get
14
+ # column values from JDBC as bytes and then use String.from_java_bytes
15
+ # to avoid messing up possible Marc8 encoding.
16
+ #
17
+ # bibtext is either text or longtext column from fullbib, preferring
18
+ # longtext. authtext is either xref_text or xref_longtext from fullbib,
19
+ # preferring xref_longtext.
20
+ def initialize(tag, bibtext, authtext)
21
+ @merged = false
22
+
23
+ @tag = tag
24
+ @bibtext = bibtext
25
+ @authtext = authtext
26
+
27
+ # remove terminal MARC Field Terminator if present.
28
+ @bibtext.chomp!("\x1E") if @bibtext
29
+ @authtext.chomp!("\x1E") if @authtext
30
+ end
31
+
32
+ # Returns merged string, composed of a marc 'field', with subfields
33
+ # seperated by seperator control chars. Does not include terminal
34
+ # MARC Field Seperator.
35
+ #
36
+ # Will mutate bibtext and authtext for efficiency.
37
+ def merge!
38
+ raise Exception.new("Can only call `merge!` once, already called.") if @merged
39
+ @merged = true
40
+
41
+ # just one? (Or neither?) Just return it.
42
+ return authtext if bibtext.nil?
43
+ return bibtext if authtext.nil?
44
+
45
+
46
+
47
+ # We need to do a crazy combination of template in text with values in authtext.
48
+ # horizon, you so crazy. text template is like:
49
+ #"\x1Fa.\x1Fp ;\x1Fv81."
50
+ # which means each subfield after the \x1F, merge in
51
+ # the subfield value from the auth record if it's present,
52
+ # otherwise don't.
53
+ #
54
+ # plus some weird as hell stuff with punctuation and spaces, I can't
55
+ # even explain it, just trial and error'd it comparing to marcout.
56
+ bibtext.gsub!(/\x1F([^\x1F\x1E])( ?)([[:punct:] ]*)/) do
57
+
58
+ subfield = $1
59
+ space = $2
60
+ maybe_punct = $3
61
+
62
+
63
+ # okay this is crazy hacky reverse engineering, I don't really
64
+ # know what's going on but for 240 and 243, 'a' in template
65
+ # is filled by 't' in auth tag.
66
+ auth_subfield = if subfield == "a" && (tag == "240" || tag == "243")
67
+ "t"
68
+ else
69
+ subfield
70
+ end
71
+
72
+ # Find substitute fill-in value from authtext, if it can
73
+ # be found -- first subfield indicated. Then we REMOVE
74
+ # it from authtext, so next time this subfield is asked for,
75
+ # subsequent subfield with that code will be used.
76
+ substitute = nil
77
+ authtext.sub!(/\x1F#{auth_subfield}([^\x1F\x1E]*)/) do
78
+ substitute = $1
79
+ ''
80
+ end
81
+
82
+ if substitute
83
+
84
+
85
+ # Dealing with punctuation is REALLY CONFUSING -- reverse engineering
86
+ # HIP/Horizon, which does WEIRD THINGS.
87
+ # But we seem to have arrived at something that appears to match all cases
88
+ # we can find of what HIP/Horizon does.
89
+ #
90
+ # If the auth value already ends up with the same punctuation from the template,
91
+ # _leave it alone_ -- including preserving all spaces near the punct in the auth
92
+ # value.
93
+ #
94
+ # Otherwise, remove all punct from the auth value, then add in the punct from the template,
95
+ # along with any spaces before the punct in the template.
96
+ if maybe_punct && maybe_punct.length > 0
97
+ # remove all punctuation from end of auth value? to use punct from template instead?
98
+ # But preserve initial spaces from template? Unless it already ends
99
+ # with the punctuation, in which case don't touch it, to avoid
100
+ # messing up spaces? WEIRD, yeah.
101
+ unless substitute.end_with? maybe_punct
102
+ substitute.gsub!(/[[:punct:]]+\Z/, "")
103
+ # This adding the #{space} back in, is consistent with what HIP does.
104
+ # I have no idea if it's right or a bug in HIP, but being consistent.
105
+ # neither leaving it in nor taking it out is exactly consistent with HznExportMarc,
106
+ # which seems to have bugs.
107
+ substitute << "#{space}#{maybe_punct}"
108
+ end
109
+ end
110
+
111
+ "\x1F#{subfield}#{substitute}"
112
+ else # just keep original, which has no maybe_punct
113
+ "\x1F#{subfield}"
114
+ end
115
+ end
116
+
117
+ # We mutated bibtext to fill in template, now just return it.
118
+ return bibtext
119
+ end
120
+
121
+
122
+
123
+ end
124
+ end
@@ -0,0 +1,641 @@
1
+ require 'traject'
2
+ require 'traject/util'
3
+ require 'traject/indexer/settings'
4
+
5
+ require 'traject/horizon_bib_auth_merge'
6
+
7
+ require 'marc'
8
+
9
+ module Traject
10
+ #
11
+ # = Settings
12
+ #
13
+ # == Connection
14
+ #
15
+ # [horizon.jdbc_url] JDBC connection URL using jtds. Should include username, but not password.
16
+ # See `horizon.jdbc_password` setting, kept seperate so we can try to suppress
17
+ # it from logging. Eg: "jdbc:jtds:sybase://horizon.lib.univ.edu:2025/dbname;user=dbuser"
18
+ # * In command line, you'll have to use quotes: -s 'horizon.jdbc_url=jdbc:jtds:sybase://horizon.lib.univ.edu:2025/dbname;user=dbuser'
19
+ #
20
+ # [horizon.jdbc_password] Password to use for JDBC connection. We'll try to suppress it from being logged.
21
+ #
22
+ # == What to export
23
+ #
24
+ # Normally exports the entire horizon database, for diagnostic or batch purposes you
25
+ # can export just one bib, or a range of bibs instead.
26
+ #
27
+ # [horizon.first_bib] Greater than equal to this bib number. Can be combined with horizon.last_bib
28
+ # [horizon.last_bib] Less than or equal to this bib number. Can be combined with horizon.first_bib
29
+ # [horizon.only_bib] Only this single bib number.
30
+ #
31
+ # You can also control whether to export staff-only bibs, copies, and items.
32
+ #
33
+ # [horizon.public_only] Default true. If set to true, only includes bibs that are NOT staff_only,
34
+ # also only include copy/item that are not staff-only if including copy/item.
35
+ #
36
+ # You can also exclude certain tags:
37
+ #
38
+ # [horizon.exclude_tags] Default nil. A comma-seperated string (so easy to supply on command line)
39
+ # of tag names to exclude from export. You probably want to at least include the tags
40
+ # you are using for horizon.item_tag and horizon.copy_tag, to avoid collision
41
+ # from tags already in record.
42
+ #
43
+ # == Item/Copy Inclusion
44
+ #
45
+ # The HorizonReader can export MARC with holdings information (horizon items and copies) included
46
+ # in the MARC. Each item or copy record will be represented as one marc field -- the tags
47
+ # used are configurable. You can configure how individual columns from item or copy tables
48
+ # map to MARC subfields in that field -- and also include columns from other tables joined
49
+ # to item or copy.
50
+ #
51
+ # [horizon.include_holdings] * false, nil, or empty string: Do not include holdings. (DEFAULT)
52
+ # * all: include copies and items
53
+ # * items: only include items
54
+ # * copies: only include copies
55
+ # * direct: only include copies OR items, but not both; if bib has
56
+ # include copies, otherwise include items if present.
57
+ #
58
+ # Each item or copy will be one marc field, you can configure what tags these fields
59
+ # will have.
60
+ #
61
+ # [horizon.item_tag] Default "991".
62
+ # [horizon.copy_tag] Default "937"
63
+ #
64
+ # Which columns from item or copy tables will be mapped to which subfields in those
65
+ # fields is controlled by hashes in settings, hash from column name (with table prefix)
66
+ # to subfield code. There are defaults, see HorizonReader.default_settings. Example for
67
+ # item_map default:
68
+ #
69
+ # "horizon.item_map" => {
70
+ # "item.call_reconstructed" => "a",
71
+ # "collection.call_type" => "b",
72
+ # "item.copy_reconstructed" => "c",
73
+ # "call_type.processor" => "f",
74
+ # "item.item#" => "i",
75
+ # "item.collection" => "l",
76
+ # "item.location" => "m",
77
+ # "item.notes" => "n",
78
+ # "item.staff_only" => "q"
79
+ # }
80
+ #
81
+ # [horizon.item_map]
82
+ # [horizon.copy_map]
83
+ #
84
+ # The column-to-subfield maps can include columns from other tables
85
+ # joined in, with a join clause configured in settings too.
86
+ # By default both item and copy join to: collection, and call_type --
87
+ # using some clever SQL to join to call_type on the item/copy fk, OR the
88
+ # associated collection fk if no specific item/copy one is defined.
89
+ #
90
+ # [horizon.item_join_clause]
91
+ # [horizon.copy_join_clause]
92
+ #
93
+ # == Character Encoding
94
+ #
95
+ # The HorizonReader can convert from Marc8 to UTF8. By default `horizon.source_encoding` is set to "MARC8"
96
+ # and `horizon.destination_encoding` is set to "UTF8", which will make it do that conversion, as well
97
+ # as set the leader byte for char encoding properly.
98
+ #
99
+ # Any other configuration of those settings, and no transcoding will take place, HorizonReader
100
+ # is not currently capable of doing any other transcoding. Set
101
+ # or `horizon.destination_encoding` to nil if you don't want any transcoding to happen --
102
+ # you'd only want this for diagnostic purposes, or if your horizon db is already utf8 (is
103
+ # that possible? We don't know.)
104
+ #
105
+ # [horizon.codepoint_translate] translates from Horizon's weird <U+nnnn> unicode
106
+ # codepoint escaping to actual UTF-8 bytes. Defaults to true. Will be ignored
107
+ # unless horizon.destination_encoding is UTF8 though.
108
+ #
109
+ # == Misc
110
+ #
111
+ # [horizon.batch_size] Batch size to use for fetching item/copy info on each bib. Default 400.
112
+ # [debug_ascii_progress] if true, will output a "<" and a ">" to stderr around every copy/item
113
+ # subsidiary fetch. See description of this setting in docs/settings.md
114
+ #
115
+ # [jtds.jar_path] Normally we'll use a distribution of jtds bundled with this gem.
116
+ # But specify a filepath of a directory containing jtds jar(s),
117
+ # and all jars in that dir will be loaded instead of our bundled jtds.
118
+ #
119
+ #
120
+ # Note: Could probably make this even faster by using a thread pool -- the bottleneck
121
+ # is probably processing into MARC, not the database query and streaming. But it's a
122
+ # bit tricky to refactor for concurrency there. Perhaps pull all the raw
123
+ # row values out and batch them in groups by bib#, then feed those lists
124
+ # to a threadpool. And then we'd just be fighting for CPU time with the
125
+ # threadpool for mapping, not sure if overall throughput increase would happen, would
126
+ # depend on exact environment.
127
+ class HorizonReader
128
+ attr_reader :settings
129
+ attr_reader :things_to_close
130
+
131
+ # We ignore the iostream even though we get one, we're gonna
132
+ # read from a Horizon DB!
133
+ def initialize(iostream, settings)
134
+ # we ignore the iostream, we're fetching from Horizon db
135
+
136
+ @settings = Traject::Indexer::Settings.new( self.class.default_settings).merge(settings)
137
+
138
+ require_jars!
139
+ end
140
+
141
+ # Requires marc4j and jtds, and java_import's some classes.
142
+ def require_jars!
143
+ Traject::Util.jruby_ensure_init!("Traject::HorizonReader")
144
+
145
+ Traject::Util.require_marc4j_jars(settings)
146
+
147
+ # For some reason we seem to need to java_import it, and use
148
+ # a string like this. can't just refer to it by full
149
+ # qualified name, not sure why, but this seems to work.
150
+ java_import "org.marc4j.converter.impl.AnselToUnicode"
151
+
152
+ unless defined? Java::net.sourceforge.jtds.jdbc.Driver
153
+ jtds_jar_dir = settings["jtds.jar_path"] || File.expand_path("../../vendor/jtds", File.dirname(__FILE__))
154
+
155
+ Dir.glob("#{jtds_jar_dir}/*.jar") do |x|
156
+ require x
157
+ end
158
+
159
+ # For confusing reasons, in normal Java need to
160
+ # Class.forName("net.sourceforge.jtds.jdbc.Driver")
161
+ # to get the jtds driver to actually be recognized by JDBC.
162
+ #
163
+ # In Jruby, Class.forName doesn't work, but this seems
164
+ # to do the same thing:
165
+ Java::net.sourceforge.jtds.jdbc.Driver
166
+ end
167
+
168
+ # So we can refer to these classes as just ResultSet, etc.
169
+ java_import java.sql.ResultSet, java.sql.PreparedStatement, java.sql.Driver
170
+ end
171
+
172
+ def fetch_result_set!(conn)
173
+ #fullbib is a view in Horizon, I think it was an SD default view, that pulls
174
+ #in stuff from multiple tables, including authority tables, to get actual
175
+ # text.
176
+ # You might think need an ORDER BY, but doing so makes it incredibly slow
177
+ # to retrieve results, can't do it. We just count on the view returning
178
+ # the rows properly. (ORDER BY bib#, tagord)
179
+ #
180
+ # We start with the fullbib view defined out of the box in Horizon, but
181
+ # need to join in bib_control to have access to the staff_only column.
182
+ #
183
+ sql = <<-EOS
184
+ SELECT b.bib#, b.tagord, b.tag,
185
+ indicators = substring(b.indicators+' ',1,2)+a.indicators,
186
+ b.text, b.cat_link_type#, b.cat_link_xref#, b.link_type,
187
+ bl.longtext, xref_text = a.text, xref_longtext = al.longtext,
188
+ b.timestamp, auth_timestamp = a.timestamp,
189
+ bc.staff_only
190
+ FROM bib b
191
+ left join bib_control bc on b.bib# = bc.bib#
192
+ left join bib_longtext bl on b.bib# = bl.bib# and b.tag = bl.tag and b.tagord = bl.tagord
193
+ left join auth a on b.cat_link_xref# = a.auth# and a.tag like '1[0-9][0-9]'
194
+ left join auth_longtext al on b.cat_link_xref# = al.auth# and al.tag like '1[0-9][0-9]'
195
+ WHERE 1 = 1
196
+ EOS
197
+
198
+ sql = <<-EOS
199
+ SELECT b.*, bc.staff_only
200
+ FROM fullbib b
201
+ JOIN bib_control bc on b.bib# = bc.bib#
202
+ WHERE 1 = 1
203
+ EOS
204
+
205
+ if settings["horizon.public_only"].to_s == "true"
206
+ sql += " AND staff_only != 1"
207
+ end
208
+
209
+ # settings should not be coming from untrusted user input not going
210
+ # to bother worrying about sql injection.
211
+ if settings.has_key? "horizon.only_bib"
212
+ sql += " AND b.bib# = #{settings['horizon.only_bib']} "
213
+ elsif settings.has_key?("horizon.first_bib") || settings.has_key?("horizon.last_bib")
214
+ clauses = []
215
+ clauses << " b.bib# >= #{settings['horizon.first_bib']}" if settings['horizon.first_bib']
216
+ clauses << " b.bib# <= #{settings['horizon.last_bib']}" if settings['horizon.last_bib']
217
+ sql += " AND " + clauses.join(" AND ") + " "
218
+ end
219
+
220
+ pstmt = conn.prepareStatement(sql);
221
+
222
+ # this may be what's neccesary to keep the driver from fetching
223
+ # entire result set into memory.
224
+ pstmt.setFetchSize(10000)
225
+
226
+
227
+ logger.debug("HorizonReader: Executing query: #{sql}")
228
+ rs = pstmt.executeQuery
229
+ logger.debug("HorizonReader: Executed!")
230
+ return rs
231
+ end
232
+
233
+ # Converts from Marc8 to UTF8 if neccesary.
234
+ # Also replaces horizon <U+nnnn> codes if needed.
235
+ def convert_text!(text, error_handler)
236
+ text = AnselToUnicode.new(error_handler, true).convert(text) if convert_marc8_to_utf8?
237
+
238
+ # Turn Horizon's weird escaping into UTF8: <U+nnnn> where nnnn is a hex unicode
239
+ # codepoint, turn it UTF8 for that codepoint
240
+ if settings["horizon.codepoint_translate"].to_s == "true" && settings["horizon.destination_encoding"] == "UTF8"
241
+ text.gsub!(/\<U\+([0-9A-F]{4})\>/) do
242
+ [$1.hex].pack("U")
243
+ end
244
+ end
245
+
246
+ return text
247
+ end
248
+
249
+ # Read rows from horizon database, assemble them into MARC::Record's, and yield each
250
+ # MARC::Record to caller.
251
+ def each
252
+ # Need to close the connection, teh result_set, AND the result_set.getStatement when
253
+ # we're done.
254
+ connection = open_connection!
255
+
256
+ # We're going to need to ask for item/copy info while in the
257
+ # middle of streaming our results. JDBC is happier and more performant
258
+ # if we use a seperate connection for this.
259
+ extra_connection = open_connection! if include_some_holdings?
260
+
261
+ # We're going to make our marc records in batches, and only yield
262
+ # them to caller in batches, so we can fetch copy/item info in batches
263
+ # for efficiency.
264
+ batch_size = settings["horizon.batch_size"]
265
+ record_batch = []
266
+
267
+ exclude_tags = (settings["horizon.exclude_tags"] || "").split(",")
268
+
269
+
270
+ rs = self.fetch_result_set!(connection)
271
+
272
+ current_bib_id = nil
273
+ record = nil
274
+ record_count = 0
275
+
276
+ error_handler = org.marc4j.ErrorHandler.new
277
+
278
+ while(rs.next)
279
+ bib_id = rs.getInt("bib#");
280
+
281
+ if bib_id != current_bib_id
282
+ record_count += 1
283
+
284
+ if settings["debug_ascii_progress"] && (record_count % settings["solrj_writer.batch_size"] == 0)
285
+ $stderr.write ","
286
+ end
287
+
288
+ # new record! Put old one on batch queue.
289
+ record_batch << record if record
290
+
291
+ # prepare and yield batch?
292
+ if (record_count % batch_size == 0)
293
+ enhance_batch!(extra_connection, record_batch)
294
+ record_batch.each do |r|
295
+ # set current_bib_id for error logging
296
+ current_bib_id = r['001'].value
297
+ yield r
298
+ end
299
+ record_batch.clear
300
+ end
301
+
302
+ # And start new record we've encountered.
303
+ error_handler = org.marc4j.ErrorHandler.new
304
+ current_bib_id = bib_id
305
+ record = MARC::Record.new
306
+ record.append MARC::ControlField.new("001", bib_id.to_s)
307
+ end
308
+
309
+
310
+ tagord = rs.getInt("tagord");
311
+ tag = rs.getString("tag")
312
+
313
+ # just silently skip it, some weird row in the horizon db, it happens.
314
+ # plus any of our exclude_tags.
315
+ next if tag.nil? || tag == "" || exclude_tags.include?(tag)
316
+
317
+ numeric_tag = tag.to_i if tag =~ /\A\d+\Z/
318
+
319
+ indicators = rs.getString("indicators")
320
+
321
+ # a packed byte array could be in various columns, in order of preference...
322
+ # the xref stuff is joined in from the auth table
323
+ # Have to get it as bytes and then convert it to String to avoid JDBC messing
324
+ # up the encoding marc8 grr
325
+ authtext = rs.getBytes("xref_longtext") || rs.getBytes("xref_text")
326
+ if authtext
327
+ authtext = String.from_java_bytes(authtext)
328
+ authtext.force_encoding("binary")
329
+ end
330
+
331
+ text = rs.getBytes("longtext") || rs.getBytes("text")
332
+ if text
333
+ text = String.from_java_bytes(text)
334
+ text.force_encoding("binary")
335
+ end
336
+
337
+ text = Traject::HorizonBibAuthMerge.new(tag, text, authtext).merge!
338
+
339
+ next if text.nil? # sometimes there's nothing there, skip it.
340
+
341
+ # convert from MARC8 to UTF8 if needed
342
+ text = convert_text!(text, error_handler)
343
+
344
+ if numeric_tag && numeric_tag == 0
345
+ record.leader = text
346
+ fix_leader!(record.leader)
347
+ elsif numeric_tag && numeric_tag == 1
348
+ # nothing, we add the 001 ourselves first
349
+ elsif numeric_tag && numeric_tag < 10
350
+ # control field
351
+ record.append MARC::ControlField.new(tag, text )
352
+ else
353
+ # data field
354
+ indicator1 = indicators.slice(0)
355
+ indicator2 = indicators.slice(1)
356
+
357
+ data_field = MARC::DataField.new( tag, indicator1, indicator2 )
358
+ record.append data_field
359
+
360
+ subfields = text.split("\x1F")
361
+
362
+ subfields.each do |subfield|
363
+ next if subfield.empty?
364
+
365
+ subfield_code = subfield.slice(0)
366
+ subfield_text = subfield.slice(1, subfield.length)
367
+
368
+ data_field.append MARC::Subfield.new(subfield_code, subfield_text)
369
+ end
370
+ end
371
+ end
372
+ # last one
373
+ record_batch << record if record
374
+
375
+ # yield last batch
376
+ enhance_batch!(extra_connection, record_batch)
377
+ record_batch.each do |r|
378
+ # reset bib_id for error message logging
379
+ current_bib_id = (f = r['001']) && f.value
380
+ yield r
381
+ end
382
+ record_batch.clear
383
+
384
+ rescue Exception => e
385
+ logger.fatal "HorizonReader, unexpected exception at bib id:#{current_bib_id}: #{Traject::Util.exception_to_log_message(e)}"
386
+ raise e
387
+ ensure
388
+ logger.info("HorizonReader: Closing all JDBC objects...")
389
+
390
+ # have to cancel the statement to keep us from waiting on entire
391
+ # result set when exception is raised in the middle of stream.
392
+ statement = rs && rs.getStatement
393
+ if statement
394
+ statement.cancel
395
+ statement.close
396
+ end
397
+
398
+ rs.close if rs
399
+
400
+ # shouldn't actually need to close the resultset and statement if we cancel, I think.
401
+ connection.close if connection
402
+
403
+ extra_connection.close if extra_connection
404
+
405
+ logger.info("HorizonReader: Closed JDBC objects")
406
+ end
407
+
408
+ def process_batch(batch)
409
+
410
+ end
411
+
412
+ # Pass in an array of MARC::Records', adds fields for copy and item
413
+ # info if so configured. Returns record_batch so you can chain if you want.
414
+ def enhance_batch!(conn, record_batch)
415
+ return record_batch if record_batch.nil? || record_batch.empty?
416
+
417
+ copy_info = get_joined_table(
418
+ conn, record_batch,
419
+ :table_name => "copy",
420
+ :column_map => settings['horizon.copy_map'],
421
+ :join_clause => settings['horizon.copy_join_clause'],
422
+ :public_only => (settings['horizon.public_only'].to_s == "true")
423
+ ) if %w{all copies direct}.include? settings['horizon.include_holdings'].to_s
424
+
425
+
426
+
427
+ item_info = get_joined_table(
428
+ conn, record_batch,
429
+ :table_name => "item",
430
+ :column_map => settings['horizon.item_map'],
431
+ :join_clause => settings['horizon.item_join_clause'],
432
+ :public_only => (settings['horizon.public_only'].to_s == "true")
433
+ ) if %w{all items direct}.include? settings['horizon.include_holdings'].to_s
434
+
435
+
436
+
437
+ if item_info || copy_info
438
+ record_batch.each do |record|
439
+ id = record['001'].value.to_s
440
+ record_copy_info = copy_info && copy_info[id]
441
+ record_item_info = item_info && item_info[id]
442
+
443
+ record_copy_info.each do |copy_row|
444
+ field = MARC::DataField.new( settings["horizon.copy_tag"] )
445
+ copy_row.each_pair do |subfield, value|
446
+ field.append MARC::Subfield.new(subfield, value)
447
+ end
448
+ record.append field
449
+ end if record_copy_info
450
+
451
+ record_item_info.each do |item_row|
452
+ field = MARC::DataField.new( settings["horizon.item_tag"] )
453
+ item_row.each_pair do |subfield, value|
454
+ field.append MARC::Subfield.new(subfield, value)
455
+ end
456
+ record.append field
457
+ end if record_item_info && ((settings['horizon.include_holdings'].to_s != "direct") || record_copy_info.empty?)
458
+ end
459
+ end
460
+
461
+ return record_batch
462
+ end
463
+
464
+ # Can be used to fetch a batch of subsidiary info from other tables:
465
+ # Used to fetch item or copy information. Can fetch with joins too.
466
+ # Usually called by passing in settings, but a literal call might look something
467
+ # like this for items:
468
+ #
469
+ # get_joined_table(jdbc_conn, array_of_marc_records,
470
+ # :table_name => "item",
471
+ # :column_map => {"item.item#" => "i", "call_type.processor" => "k"},
472
+ # :join_clause => "JOIN call_type ON item.call_type = call_type.call_type"
473
+ # )
474
+ #
475
+ # Returns a hash keyed by bibID, value is an array of hashes of subfield->value, eg:
476
+ #
477
+ # {'343434' => [
478
+ # {
479
+ # 'i' => "012124" # item.item#
480
+ # 'k' => 'lccn' # call_type.processor
481
+ # }
482
+ # ]
483
+ # }
484
+ #
485
+ # Can also pass in a `:public_only => true` option, will add on a staff_only != 1
486
+ # where clause, assumes primary table has a staff_only column.
487
+ def get_joined_table(conn, batch, options = {})
488
+ table_name = options[:table_name] or raise ArgumentError.new("Need a :table_name option")
489
+ column_map = options[:column_map] or raise ArgumentError.new("Need a :column_map option")
490
+ join_clause = options[:join_clause] || ""
491
+ public_only = options[:public_only]
492
+
493
+
494
+ results = Hash.new {|h, k| h[k] = [] }
495
+
496
+ bib_ids_joined = batch.collect do |record|
497
+ record['001'].value.to_s
498
+ end.join(",")
499
+
500
+ # We include the column name with prefix as an "AS", so we can fetch it out
501
+ # of the result set later just like that.
502
+ columns_clause = column_map.keys.collect {|c| "#{c} AS '#{c}'"}.join(",")
503
+ sql = <<-EOS
504
+ SELECT bib#, #{columns_clause}
505
+ FROM #{table_name}
506
+ #{join_clause}
507
+ WHERE bib# IN (#{bib_ids_joined})
508
+ EOS
509
+
510
+ if public_only
511
+ sql += " AND staff_only != 1"
512
+ end
513
+
514
+ $stderr.write "<" if settings["debug_ascii_progress"]
515
+
516
+ # It might be higher performance to refactor to re-use the same prepared statement
517
+ # for each item/copy fetch... but appears to be no great way to do that in JDBC3
518
+ # where you need to parameterize "IN" values. JDBC4 has got it, but jTDS is just JDBC3.
519
+ pstmt = conn.prepareStatement(sql);
520
+ rs = pstmt.executeQuery
521
+
522
+
523
+ while (rs.next)
524
+ bib_id = rs.getString("bib#")
525
+ row_hash = {}
526
+
527
+ column_map.each_pair do |column, subfield|
528
+ value = rs.getString( column )
529
+
530
+ if value
531
+ # Okay, total hack to deal with the fact that holding notes
532
+ # seem to be in UTF8 even though records are in MARC... which
533
+ # ends up causing problems for exporting as marc8, which is
534
+ # handled kind of not very well anyway.
535
+ # I don't even totally understand what I'm doing, after 6 hours working on it,
536
+ # sorry, just a hack.
537
+ value.force_encoding("BINARY") unless settings["horizon.destination_encoding"] == "UTF8"
538
+
539
+ row_hash[subfield] = value
540
+ end
541
+ end
542
+
543
+ results[bib_id] << row_hash
544
+ end
545
+
546
+ return results
547
+ ensure
548
+ pstmt.cancel if pstmt
549
+ pstmt.close if pstmt
550
+ rs.close if rs
551
+ $stderr.write ">" if settings["debug_ascii_progress"]
552
+ end
553
+
554
+ # Mutate string passed in to fix leader bytes for marc21
555
+ def fix_leader!(leader)
556
+ if leader.length < 24
557
+ # pad it to 24 bytes, leader is supposed to be 24 bytes
558
+ leader.replace( leader.ljust(24, ' ') )
559
+ end
560
+ # http://www.loc.gov/marc/bibliographic/ecbdldrd.html
561
+ leader[10..11] = '22'
562
+ leader[20..23] = '4500'
563
+
564
+ if settings['horizon.destination_encoding'] == "UTF8"
565
+ leader[9] = 'a'
566
+ end
567
+ end
568
+
569
+ def include_some_holdings?
570
+ ! [false, nil, ""].include?(settings['horizon.include_holdings'])
571
+ end
572
+
573
+ def convert_marc8_to_utf8?
574
+ settings['horizon.source_encoding'] == "MARC8" && settings['horizon.destination_encoding'] == "UTF8"
575
+ end
576
+
577
+
578
+ def open_connection!
579
+ logger.debug("HorizonReader: Opening JDBC Connection at #{settings["horizon.jdbc_url"]} ...")
580
+
581
+ url = settings["horizon.jdbc_url"]
582
+ if settings["horizon.jdbc_password"]
583
+ url += ";password=#{settings['horizon.jdbc_password']}"
584
+ end
585
+
586
+ conn = java.sql.DriverManager.getConnection( url )
587
+ # If autocommit on, fetchSize later has no effect, and JDBC slurps
588
+ # the whole result set into memory, which we can not handle.
589
+ conn.setAutoCommit false
590
+ logger.debug("HorizonReader: Opened JDBC Connection.")
591
+ return conn
592
+ end
593
+
594
+ def logger
595
+ settings["logger"] || Yell::Logger.new(STDERR, :level => "gt.fatal") # null logger
596
+ end
597
+
598
+ def self.default_settings
599
+ {
600
+ "horizon.batch_size" => 400,
601
+
602
+ "horizon.public_only" => true,
603
+
604
+ "horizon.source_encoding" => "MARC8",
605
+ "horizon.destination_encoding" => "UTF8",
606
+ "horizon.codepoint_translate" => true,
607
+
608
+ "horizon.item_tag" => "991",
609
+ # Crazy isnull() in the call_type join to join to call_type directly on item
610
+ # if specified otherwise calltype on colleciton. Phew!
611
+ "horizon.item_join_clause" => "LEFT OUTER JOIN collection ON item.collection = collection.collection LEFT OUTER JOIN call_type ON isnull(item.call_type, collection.call_type) = call_type.call_type",
612
+ "horizon.item_map" => {
613
+ "item.call_reconstructed" => "a",
614
+ "call_type.processor" => "f",
615
+ "call_type.call_type" => "b",
616
+ "item.copy_reconstructed" => "c",
617
+ "item.staff_only" => "q",
618
+ "item.item#" => "i",
619
+ "item.collection" => "l",
620
+ "item.notes" => "n",
621
+ "item.location" => "m"
622
+ },
623
+
624
+ "horizon.copy_tag" => "937",
625
+ # Crazy isnull() in the call_type join to join to call_type directly on item
626
+ # if specified otherwise calltype on colleciton. Phew!
627
+ "horizon.copy_join_clause" => "LEFT OUTER JOIN collection ON copy.collection = collection.collection LEFT OUTER JOIN call_type ON isnull(copy.call_type, collection.call_type) = call_type.call_type",
628
+ "horizon.copy_map" => {
629
+ "copy.copy#" => "8",
630
+ "copy.call" => "a",
631
+ "copy.copy_number" => "c",
632
+ "call_type.processor" => "f",
633
+ "copy.staff_only" => "q",
634
+ "copy.location" => "m",
635
+ "copy.collection" => "l",
636
+ "copy.pac_note" => "n"
637
+ }
638
+ }
639
+ end
640
+ end
641
+ end
@@ -0,0 +1,6 @@
1
+ require "traject_horizon/version"
2
+
3
+ require 'traject/horizon_reader'
4
+
5
+ module TrajectHorizon
6
+ end
@@ -0,0 +1,3 @@
1
+ module TrajectHorizon
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,58 @@
1
+ # Encoding: ASCII-8BIT
2
+
3
+ require 'test_helper'
4
+
5
+ require 'traject'
6
+ require 'traject/horizon_bib_auth_merge'
7
+
8
+ describe "HorizonBibAuthMerge" do
9
+ HzMerge = Traject::HorizonBibAuthMerge # shortcut
10
+
11
+ it "does simple example" do
12
+ assert_equal "aOsmoregulationvCongresses.", HzMerge.new("650", "a v.", "aOsmoregulationvCongresses.").merge!
13
+ end
14
+
15
+ it "adds on simple trailing punctuation" do
16
+ assert_equal "aHomeostasisvCongresses.", HzMerge.new("650", "a v.", "aHomeostasisvCongresses").merge!
17
+ end
18
+
19
+ it "handles weirder punctuation" do
20
+ assert_equal "aEastaugh, Steven R.,d1952-", HzMerge.new("100", "a.,d-", "aEastaugh, Steven R.,d1952-").merge!
21
+ end
22
+
23
+ it "merges non-controlled values" do
24
+ assert_equal "aNational League for Nursing publication ;vno. 52-1870.", HzMerge.new("830", "a ;vno. 52-1870.", "aNational League for Nursing publication ;").merge!
25
+ end
26
+
27
+ it "handles multiple templated subfield with same code" do
28
+ assert_equal "aMedical carexUtilizationzMarylandzBaltimore.", HzMerge.new("650", "a x z z.", "aMedical carexUtilizationzMarylandzBaltimore.").merge!
29
+ end
30
+
31
+ it "handles tag 240 weirdness" do
32
+ assert_equal "aProblemy radiaÙtýsionnoµi genetiki.lEnglish", HzMerge.new("240", "a.l ", "aDubinin, Nikolaµi Petrovich,d1907-1998.tProblemy radiaÙtýsionnoµi genetiki.lEnglish").merge!
33
+ end
34
+
35
+ it "preserves space before semi-colon in 830" do
36
+ # this is actually something Alpha-G's HznExportMarc does differently
37
+ # than HIP/Horizon -- we try to stick with HIP/Horizon, not entirely
38
+ # sure if this is a bug in HIP we're reproducing, maybe there shouldn't
39
+ # be space before the semi-colon?
40
+ assert_equal "aActa ophthalmologica.pSupplementum ;v81.", HzMerge.new("830", "a.p ;v81.", "aActa ophthalmologica.pSupplementum").merge!
41
+ end
42
+
43
+ it "handles non-matching ending punct" do
44
+ # Yes, current HIP behavior, as well as marcout and HznMarcOut, ends in
45
+ # period. I don't know if it's really right, but we'll match current behavior.
46
+ assert_equal "aWessel, Rosa,d1897.", HzMerge.new("100", "a,d.", "aWessel, Rosa,d1897-").merge!
47
+ end
48
+
49
+ it "a weird non-matching ending punct" do
50
+ # in this one, HIP and Alpha-G HznMarcOut actually didn't match! We go with HIP.
51
+ assert_equal "aGreat Britain.bParliament.tPapers by Command ;vCd. 4671.", HzMerge.new("810", "a.b.t ;vCd. 4671.", "aGreat Britain.bParliament.tPapers by Command.").merge!
52
+ end
53
+
54
+ it "handles weird internal multi punct with spaces" do
55
+ assert_equal "aMiscellaneous publications (Pan American Sanitary Bureau) ;vno. 79.", HzMerge.new("830", "a) ;vno. 79.", "aMiscellaneous publications (Pan American Sanitary Bureau) ;").merge!
56
+ end
57
+
58
+ end
@@ -0,0 +1,16 @@
1
+ gem 'minitest' # I feel like this messes with bundler, but only way to get minitest to shut up
2
+ require 'minitest/autorun'
3
+ require 'minitest/spec'
4
+
5
+ require 'traject'
6
+ require 'marc'
7
+
8
+ # keeps things from complaining about "yell-1.4.0/lib/yell/adapters/io.rb:66 warning: syswrite for buffered IO"
9
+ # for reasons I don't entirely understand, involving yell using syswrite and tests sometimes
10
+ # using $stderr.puts. https://github.com/TwP/logging/issues/31
11
+ STDERR.sync = true
12
+
13
+ # Hacky way to turn off Indexer logging by default, say only
14
+ # log things higher than fatal, which is nothing.
15
+ require 'traject/indexer/settings'
16
+ Traject::Indexer::Settings.defaults["log.level"] = "gt.fatal"
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'traject_horizon/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "traject_horizon"
8
+ spec.version = TrajectHorizon::VERSION
9
+ spec.authors = ["Jonathan Rochkind"]
10
+ spec.email = ["jonathan@dnil.net"]
11
+ spec.summary = %q{Horizon ILS MARC Exporter, a plugin for the traject tool}
12
+ spec.homepage = "http://github.com/jrochkind/traject_horizon"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "traject"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traject_horizon
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Jonathan Rochkind
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: traject
16
+ version_requirements: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ none: false
22
+ requirement: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ none: false
28
+ prerelease: false
29
+ type: :runtime
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ version_requirements: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ~>
35
+ - !ruby/object:Gem::Version
36
+ version: '1.3'
37
+ none: false
38
+ requirement: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ version: '1.3'
43
+ none: false
44
+ prerelease: false
45
+ type: :development
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ none: false
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ none: false
60
+ prerelease: false
61
+ type: :development
62
+ description:
63
+ email:
64
+ - jonathan@dnil.net
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - lib/traject/horizon_bib_auth_merge.rb
75
+ - lib/traject/horizon_reader.rb
76
+ - lib/traject_horizon.rb
77
+ - lib/traject_horizon/version.rb
78
+ - test/horizon_bib_auth_merge_test.rb
79
+ - test/test_helper.rb
80
+ - traject_horizon.gemspec
81
+ - vendor/jtds/.DS_Store
82
+ - vendor/jtds/jtds-1.2.8.jar
83
+ homepage: http://github.com/jrochkind/traject_horizon
84
+ licenses:
85
+ - MIT
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ none: false
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ none: false
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.24
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Horizon ILS MARC Exporter, a plugin for the traject tool
108
+ test_files:
109
+ - test/horizon_bib_auth_merge_test.rb
110
+ - test/test_helper.rb