rbook-onix 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +3 -1
- data/examples/header.xml +55 -0
- data/examples/message.rb +8 -0
- data/examples/stream_reader.rb +2 -1
- data/lib/rbook/onix.rb +1 -0
- data/lib/rbook/onix/contributor.rb +36 -38
- data/lib/rbook/onix/message.rb +68 -67
- data/lib/rbook/onix/product.rb +112 -108
- data/lib/rbook/onix/sales_restriction.rb +26 -31
- data/lib/rbook/onix/stream_reader.rb +32 -10
- data/lib/rbook/onix/supply_detail.rb +47 -51
- data/specs/contributor_with_data_spec.rb +13 -5
- data/specs/data/2_ids.xml +55 -0
- data/specs/data/leading_garbage.xml +70 -0
- data/specs/data/truncated.xml +80 -0
- data/specs/data/with_ampersands.xml +111 -0
- data/specs/message_class_spec.rb +10 -3
- data/specs/message_with_data_spec.rb +25 -4
- data/specs/product_class_spec.rb +10 -0
- data/specs/product_with_data_spec.rb +30 -9
- data/specs/sales_restriction_with_data_spec.rb +12 -5
- data/specs/stream_reader_spec.rb +57 -0
- data/specs/supply_detail_with_data_spec.rb +18 -9
- metadata +51 -27
@@ -6,50 +6,45 @@ module Onix
|
|
6
6
|
class SalesRestriction
|
7
7
|
attr_accessor :type, :detail
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
raise ArgumentError, 'load_from_element expects a REXML element object' unless element.class == REXML::Element
|
12
|
-
|
13
|
-
if REXML::XPath.first(element, '//SalesRestriction').nil?
|
14
|
-
raise LoadError, 'supplied REXML document object does not appear contain a valid SalesRestriction fragment'
|
15
|
-
end
|
16
|
-
|
9
|
+
def self.load_from_string(str)
|
10
|
+
doc = Hpricot::XML(str)
|
17
11
|
restriction = SalesRestriction.new
|
18
12
|
|
19
|
-
tmp =
|
20
|
-
restriction.type = tmp.
|
13
|
+
tmp = doc.search('//SalesRestriction/SalesRestrictionType')
|
14
|
+
restriction.type = tmp.inner_html unless tmp.inner_html.blank?
|
21
15
|
|
22
|
-
tmp =
|
23
|
-
restriction.detail = tmp.
|
16
|
+
tmp = doc.search('//SalesRestriction/SalesRestrictionDetail')
|
17
|
+
restriction.detail = tmp.inner_html unless tmp.inner_html.blank?
|
24
18
|
|
25
19
|
return restriction
|
26
|
-
|
27
20
|
end
|
28
|
-
|
29
|
-
#
|
30
|
-
def
|
31
|
-
raise '
|
32
|
-
raise 'Contributor must have a detail to create an element' if self.detail.nil?
|
21
|
+
|
22
|
+
# Attempts to create a contributor object using the supplied xml element
|
23
|
+
def SalesRestriction.load_from_element(element)
|
24
|
+
raise ArgumentError, 'load_from_element expects a REXML element object' unless element.class == REXML::Element
|
33
25
|
|
34
|
-
|
35
|
-
|
36
|
-
|
26
|
+
if REXML::XPath.first(element, '//SalesRestriction').nil?
|
27
|
+
raise LoadError, 'supplied REXML document object does not appear contain a valid SalesRestriction fragment'
|
28
|
+
end
|
37
29
|
|
38
|
-
|
30
|
+
self.load_from_string(element.to_s)
|
39
31
|
end
|
40
32
|
|
41
33
|
# Return an XML string representing this contributor
|
42
34
|
def to_s
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
35
|
+
raise 'SalesRestriction must have a type to create an element' if self.type.nil?
|
36
|
+
raise 'Contributor must have a detail to create an element' if self.detail.nil?
|
37
|
+
|
38
|
+
builder = Builder::XmlMarkup.new(:indent => 2)
|
39
|
+
|
40
|
+
builder.SalesRestriction do |sr|
|
41
|
+
sr.SalesRestrictionType self.type
|
42
|
+
sr.SalesRestrictionDetail self.detail
|
51
43
|
end
|
52
|
-
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_element
|
47
|
+
REXML::Document.new(to_s).root
|
53
48
|
end
|
54
49
|
end
|
55
50
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rexml/document'
|
2
2
|
require 'thread'
|
3
|
+
require 'timeout'
|
3
4
|
|
4
5
|
module RBook
|
5
6
|
module Onix
|
@@ -14,6 +15,11 @@ module RBook
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def doctype(name, pub_sys, long_name, uri)
|
18
|
+
# do nothing
|
19
|
+
end
|
20
|
+
|
21
|
+
def instruction(name, instruction)
|
22
|
+
# do nothing
|
17
23
|
end
|
18
24
|
|
19
25
|
def tag_start(name, attrs)
|
@@ -29,7 +35,7 @@ module RBook
|
|
29
35
|
end
|
30
36
|
|
31
37
|
def text(text)
|
32
|
-
@product_fragment << text if @in_product
|
38
|
+
@product_fragment << text.to_xs if @in_product
|
33
39
|
end
|
34
40
|
|
35
41
|
def tag_end(name)
|
@@ -43,13 +49,13 @@ module RBook
|
|
43
49
|
# A product tag is finished, so add it to the queue
|
44
50
|
@product_fragment << "</Product>"
|
45
51
|
begin
|
46
|
-
|
47
|
-
unless
|
48
|
-
|
49
|
-
@queue.push(product) unless product.nil?
|
50
|
-
end
|
51
|
-
rescue
|
52
|
+
product = RBook::Onix::Product.load_from_string(@product_fragment)
|
53
|
+
@queue.push(product) unless product.nil?
|
54
|
+
rescue Exception => e
|
52
55
|
# error occurred while building the product from an XML fragment
|
56
|
+
# pop the error on the queue so it can be raised by the thread
|
57
|
+
# reading items off the queue
|
58
|
+
@queue.push(e)
|
53
59
|
end
|
54
60
|
@in_product = false
|
55
61
|
else
|
@@ -61,7 +67,14 @@ module RBook
|
|
61
67
|
# do nothing
|
62
68
|
end
|
63
69
|
|
64
|
-
def
|
70
|
+
def end_document
|
71
|
+
# signal to the thread reading products off the queue that there are none left.
|
72
|
+
# this event curently isn't supported by rexml, so if an ONIX file is truncated
|
73
|
+
# and is missing </ONIXMessage>, it will hang. Patch submitted upstream
|
74
|
+
@queue.push(nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
def method_missing(*args)
|
65
78
|
# do nothing
|
66
79
|
end
|
67
80
|
end
|
@@ -91,6 +104,7 @@ module RBook
|
|
91
104
|
else
|
92
105
|
throw "Unable to read from path or file"
|
93
106
|
end
|
107
|
+
|
94
108
|
# create a sized queue to store each product read from the file
|
95
109
|
@queue = SizedQueue.new(100)
|
96
110
|
|
@@ -109,11 +123,19 @@ module RBook
|
|
109
123
|
# puts product.inspect
|
110
124
|
# end
|
111
125
|
def each
|
112
|
-
|
126
|
+
# if the ONIX file we're processing has been truncated (no </ONIXMessage>), then
|
127
|
+
# we will block on the next @queue.pop indefinitely, so give it a time limit
|
128
|
+
obj = nil
|
129
|
+
Timeout::timeout(5) { obj = @queue.pop }
|
113
130
|
while !obj.nil?
|
131
|
+
raise obj if obj.kind_of?(Exception)
|
114
132
|
yield obj
|
115
|
-
|
133
|
+
|
134
|
+
Timeout::timeout(5) { obj = @queue.pop }
|
116
135
|
end
|
136
|
+
rescue Timeout::Error
|
137
|
+
# do nothing, no more items on the queue - possibly the source
|
138
|
+
# file wasn't an XML file?
|
117
139
|
end
|
118
140
|
end
|
119
141
|
end
|
@@ -7,74 +7,70 @@ module Onix
|
|
7
7
|
class SupplyDetail
|
8
8
|
attr_accessor :supplier_name, :availability_code, :intermediary_availability_code, :price, :price_type_code
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
raise ArgumentError, 'load_from_element expects a REXML element object' unless element.class == REXML::Element
|
13
|
-
|
14
|
-
if REXML::XPath.first(element, '//SupplyDetail').nil?
|
15
|
-
raise LoadError, 'supplied REXML document object does not appear contain a valid SupplyDetail fragment'
|
16
|
-
end
|
17
|
-
|
10
|
+
def self.load_from_string(str)
|
11
|
+
doc = Hpricot::XML(str)
|
18
12
|
supply_detail = SupplyDetail.new
|
19
13
|
|
20
|
-
tmp =
|
21
|
-
supply_detail.supplier_name = tmp.
|
14
|
+
tmp = doc.search('//SupplyDetail/SupplierName')
|
15
|
+
supply_detail.supplier_name = tmp.inner_html unless tmp.inner_html.blank?
|
22
16
|
|
23
|
-
tmp =
|
24
|
-
supply_detail.availability_code = tmp.
|
17
|
+
tmp = doc.search('//SupplyDetail/AvailabilityCode')
|
18
|
+
supply_detail.availability_code = tmp.inner_html unless tmp.inner_html.blank?
|
25
19
|
|
26
|
-
tmp =
|
27
|
-
supply_detail.intermediary_availability_code = tmp.
|
20
|
+
tmp = doc.search('//SupplyDetail/IntermediaryAvailabilityCode')
|
21
|
+
supply_detail.intermediary_availability_code = tmp.inner_html unless tmp.inner_html.blank?
|
28
22
|
|
29
|
-
tmp =
|
30
|
-
supply_detail.price = BigDecimal(tmp.
|
23
|
+
tmp = doc.search('//SupplyDetail/Price/PriceAmount')
|
24
|
+
supply_detail.price = BigDecimal(tmp.inner_html) unless tmp.inner_html.blank?
|
31
25
|
|
32
|
-
tmp =
|
33
|
-
supply_detail.price_type_code = tmp.
|
26
|
+
tmp = doc.search('//SupplyDetail/Price/PriceTypeCode')
|
27
|
+
supply_detail.price_type_code = tmp.inner_html.to_i unless tmp.inner_html.blank?
|
34
28
|
|
35
29
|
return supply_detail
|
36
|
-
|
37
30
|
end
|
38
|
-
|
39
|
-
#
|
40
|
-
def
|
41
|
-
raise '
|
42
|
-
raise 'SupplyDetail must have a availability code to create an element' if self.availability_code.nil?
|
31
|
+
|
32
|
+
# Attempts to create a contributor object using the supplied xml element
|
33
|
+
def self.load_from_element(element)
|
34
|
+
raise ArgumentError, 'load_from_element expects a REXML element object' unless element.class == REXML::Element
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
supply_detail.add_element('AvailabilityCode').text = self.availability_code.to_xs unless self.availability_code.nil?
|
47
|
-
supply_detail.add_element('IntermediaryAvailabilityCode').text = self.intermediary_availability_code.to_xs unless self.intermediary_availability_code.nil?
|
48
|
-
unless self.price.nil?
|
49
|
-
tmp = REXML::Element.new('Price')
|
50
|
-
if self.price.kind_of?(BigDecimal)
|
51
|
-
tmp.add_element('PriceAmount').text = self.price.to_s("F")
|
52
|
-
else
|
53
|
-
tmp.add_element('PriceAmount').text = self.price.to_s
|
54
|
-
end
|
55
|
-
supply_detail.add_element(tmp)
|
56
|
-
if self.price_type_code.nil?
|
57
|
-
tmp.add_element('PriceTypeCode').text = "02"
|
58
|
-
else
|
59
|
-
tmp.add_element('PriceTypeCode').text = self.price_type_code
|
60
|
-
end
|
36
|
+
if REXML::XPath.first(element, '//SupplyDetail').nil?
|
37
|
+
raise LoadError, 'supplied REXML document object does not appear contain a valid SupplyDetail fragment'
|
61
38
|
end
|
62
39
|
|
63
|
-
|
40
|
+
self.load_from_string(element.to_s)
|
64
41
|
end
|
65
42
|
|
66
|
-
|
43
|
+
def to_element
|
44
|
+
REXML::Document.new(to_s).root
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return an XML string representing this supply detail
|
67
48
|
def to_s
|
68
|
-
|
49
|
+
raise 'SupplyDetail must have a supplier name to create an element' if self.supplier_name.nil?
|
50
|
+
raise 'SupplyDetail must have a availability code to create an element' if self.availability_code.nil?
|
51
|
+
|
52
|
+
builder = Builder::XmlMarkup.new(:indent => 2)
|
69
53
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
54
|
+
builder.SupplyDetail do |supply|
|
55
|
+
supply.SupplierName self.supplier_name
|
56
|
+
supply.AvailabilityCode self.availability_code unless self.availability_code.nil?
|
57
|
+
supply.IntermediaryAvailabilityCode self.intermediary_availability_code unless self.intermediary_availability_code.nil?
|
58
|
+
unless self.price.nil?
|
59
|
+
tmp = REXML::Element.new('Price')
|
60
|
+
supply.Price do |p|
|
61
|
+
if self.price.kind_of?(BigDecimal)
|
62
|
+
p.PriceAmount self.price.to_s("F")
|
63
|
+
else
|
64
|
+
p.PriceAmount self.price.to_s
|
65
|
+
end
|
66
|
+
if self.price_type_code.nil?
|
67
|
+
p.PriceTypeCode "02"
|
68
|
+
else
|
69
|
+
p.PriceTypeCode self.price_type_code
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
76
73
|
end
|
77
|
-
return output
|
78
74
|
end
|
79
75
|
end
|
80
76
|
end
|
@@ -16,12 +16,12 @@ context "A product object with valid data" do
|
|
16
16
|
contributor.sequence_number = '01'
|
17
17
|
contributor.role = 'A01'
|
18
18
|
|
19
|
-
doc = contributor.
|
19
|
+
doc = REXML::Document.new(contributor.to_s)
|
20
20
|
|
21
|
-
REXML::XPath.first(doc, '
|
22
|
-
REXML::XPath.first(doc, '
|
23
|
-
REXML::XPath.first(doc, '
|
24
|
-
REXML::XPath.first(doc, '
|
21
|
+
REXML::XPath.first(doc, '//PersonNameInverted').should_not be_nil
|
22
|
+
REXML::XPath.first(doc, '//PersonNameInverted').text.should eql("Healy, James")
|
23
|
+
REXML::XPath.first(doc, '//ContributorRole').should_not be_nil
|
24
|
+
REXML::XPath.first(doc, '//ContributorRole').text.should eql("A01")
|
25
25
|
end
|
26
26
|
|
27
27
|
specify "should output a valid string" do
|
@@ -31,6 +31,14 @@ context "A product object with valid data" do
|
|
31
31
|
contributor.role = 'A01'
|
32
32
|
|
33
33
|
contributor.to_s.should be_a_kind_of(String)
|
34
|
+
end
|
34
35
|
|
36
|
+
specify "should output a valid REXML::Element" do
|
37
|
+
contributor = RBook::Onix::Contributor.new
|
38
|
+
contributor.name_inverted = 'Healy, James'
|
39
|
+
contributor.sequence_number = '01'
|
40
|
+
contributor.role = 'A01'
|
41
|
+
|
42
|
+
contributor.to_element.should be_a_kind_of(REXML::Element)
|
35
43
|
end
|
36
44
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE ONIXMessage SYSTEM "http://www.editeur.org/onix/2.1/02/reference/onix-international.dtd">
|
3
|
+
<ONIXMessage>
|
4
|
+
<Header>
|
5
|
+
<FromCompany>Walker Books Australia</FromCompany>
|
6
|
+
<SentDate>20070613</SentDate>
|
7
|
+
</Header>
|
8
|
+
<Product>
|
9
|
+
<RecordReference>9789900036708</RecordReference>
|
10
|
+
<NotificationType>03</NotificationType>
|
11
|
+
<ProductIdentifier>
|
12
|
+
<ProductIDType>03</ProductIDType>
|
13
|
+
<IDValue>9789900036708</IDValue>
|
14
|
+
</ProductIdentifier>
|
15
|
+
<ProductIdentifier>
|
16
|
+
<ProductIDType>15</ProductIDType>
|
17
|
+
<IDValue>9789900036708</IDValue>
|
18
|
+
</ProductIdentifier>
|
19
|
+
<ProductForm>WX</ProductForm>
|
20
|
+
<NoContributor/>
|
21
|
+
<AudienceCode>02</AudienceCode>
|
22
|
+
<Imprint>
|
23
|
+
<ImprintName>Walker Books</ImprintName>
|
24
|
+
</Imprint>
|
25
|
+
<Publisher>
|
26
|
+
<PublishingRole>01</PublishingRole>
|
27
|
+
<PublisherName>Walker Books</PublisherName>
|
28
|
+
</Publisher>
|
29
|
+
<PublishingStatus>02</PublishingStatus>
|
30
|
+
<PublicationDate>20070901</PublicationDate>
|
31
|
+
<SupplyDetail>
|
32
|
+
<SupplierName>TL Distribution</SupplierName>
|
33
|
+
<SupplierRole>02</SupplierRole>
|
34
|
+
<ProductAvailability>10</ProductAvailability>
|
35
|
+
<Stock>
|
36
|
+
<OnHand>NYP</OnHand>
|
37
|
+
<OnOrder>No</OnOrder>
|
38
|
+
</Stock>
|
39
|
+
<Price>
|
40
|
+
<PriceTypeCode>02</PriceTypeCode>
|
41
|
+
<PriceAmount>508.50</PriceAmount>
|
42
|
+
</Price>
|
43
|
+
</SupplyDetail>
|
44
|
+
<MarketRepresentation>
|
45
|
+
<AgentName>Walker Books Australia</AgentName>
|
46
|
+
<AgentRole>07</AgentRole>
|
47
|
+
<MarketCountry>AU</MarketCountry>
|
48
|
+
<MarketPublishingStatus>02</MarketPublishingStatus>
|
49
|
+
<MarketDate>
|
50
|
+
<MarketDateRole>01</MarketDateRole>
|
51
|
+
<Date>20070901</Date>
|
52
|
+
</MarketDate>
|
53
|
+
</MarketRepresentation>
|
54
|
+
</Product>
|
55
|
+
</ONIXMessage>
|
@@ -0,0 +1,70 @@
|
|
1
|
+
<b>ERROR!!!!</b> ORA-00942: table or view does not exist
|
2
|
+
<br><b>ERROR!!!!</b> ORA-00942: table or view does not exist
|
3
|
+
<br><b>ERROR!!!!</b> ORA-00942: table or view does not exist
|
4
|
+
<br><b>ERROR!!!!</b> ORA-00942: table or view does not exist
|
5
|
+
<br><b>ERROR!!!!</b> ORA-00942: table or view does not exist
|
6
|
+
<br><?xml version="1.0" encoding="utf-8"?>
|
7
|
+
<!DOCTYPE ONIXMessage SYSTEM "http://www.editeur.org/onix/2.1/reference/onix-international.dtd">
|
8
|
+
<ONIXMessage>
|
9
|
+
<Header>
|
10
|
+
<FromCompany>TitlePage</FromCompany>
|
11
|
+
<FromPerson>TitlePage 02 92819788</FromPerson>
|
12
|
+
<FromEmail>titlepage@publishers.asn.au</FromEmail>
|
13
|
+
<SentDate>20071104</SentDate>
|
14
|
+
<MessageNote>This data is copyright to TitlePage. TitlePage makes no guarantee of the accuracy or the timeliness of production. It is supplied for your exclusive use as our customer and is only to be used for advising on the pricing and availability of publications (Authorised Purpose). You must not charge a fee for this service. TitlePage and its content may not be used for any other purpose. While it may be copied once for the authorised purpose, written permission from TitlePage must be obtained for any other use. If you were not an intended recipient, you must notify the sender and delete all copies.</MessageNote>
|
15
|
+
</Header>
|
16
|
+
<Product>
|
17
|
+
<RecordReference>77-9780140441185</RecordReference>
|
18
|
+
<NotificationType>03</NotificationType>
|
19
|
+
<ProductForm>BC</ProductForm>
|
20
|
+
<ProductFormDetail>B305</ProductFormDetail>
|
21
|
+
<Title>
|
22
|
+
<TitleType>01</TitleType>
|
23
|
+
<TitleText>Thus Spoke Zarathustra</TitleText>
|
24
|
+
</Title>
|
25
|
+
<Website>
|
26
|
+
<WebsiteLink>http://www.penguin.com.au/default.cfm?SBN=9780140441185</WebsiteLink>
|
27
|
+
</Website>
|
28
|
+
<EditionNumber>1</EditionNumber>
|
29
|
+
<NumberOfPages>352</NumberOfPages>
|
30
|
+
<BICMainSubject>HPC</BICMainSubject>
|
31
|
+
<AudienceCode>01</AudienceCode>
|
32
|
+
<MediaFile>
|
33
|
+
<MediaFileTypeCode>04</MediaFileTypeCode>
|
34
|
+
<MediaFileLinkTypeCode>01</MediaFileLinkTypeCode>
|
35
|
+
<MediaFileLink>http://coverimages.titlepage.com.au/77/9780140441185.gif</MediaFileLink>
|
36
|
+
</MediaFile>
|
37
|
+
<Imprint>
|
38
|
+
<ImprintName>Penguin</ImprintName>
|
39
|
+
</Imprint>
|
40
|
+
<Publisher>
|
41
|
+
<PublishingRole>01</PublishingRole>
|
42
|
+
<PublisherName>Penguin UK</PublisherName>
|
43
|
+
</Publisher>
|
44
|
+
<PublishingStatus>04</PublishingStatus>
|
45
|
+
<PublicationDate>19640101</PublicationDate>
|
46
|
+
<YearFirstPublished>1964</YearFirstPublished>
|
47
|
+
<SupplyDetail>
|
48
|
+
<SupplierName>United Book Distributors</SupplierName>
|
49
|
+
<ProductAvailability>20</ProductAvailability>
|
50
|
+
<Stock>
|
51
|
+
<OnHand>In Stock</OnHand>
|
52
|
+
<OnOrder>No</OnOrder>
|
53
|
+
</Stock>
|
54
|
+
<PackQuantity>48</PackQuantity>
|
55
|
+
<Price>
|
56
|
+
<PriceTypeCode>02</PriceTypeCode>
|
57
|
+
<PriceAmount>12.95</PriceAmount>
|
58
|
+
</Price>
|
59
|
+
</SupplyDetail>
|
60
|
+
<MarketRepresentation>
|
61
|
+
<AgentName>Penguin Group Australia</AgentName>
|
62
|
+
<MarketCountry>AU</MarketCountry>
|
63
|
+
<MarketPublishingStatus>04</MarketPublishingStatus>
|
64
|
+
<MarketDate>
|
65
|
+
<MarketDateRole>01</MarketDateRole>
|
66
|
+
<Date>19640101</Date>
|
67
|
+
</MarketDate>
|
68
|
+
</MarketRepresentation>
|
69
|
+
</Product>
|
70
|
+
</ONIXMessage>
|