├── .gitignore ├── README.md ├── README.old ├── TODO ├── lib ├── quickbooks.rb └── quickbooks │ ├── api.rb │ ├── config.rb │ ├── dtd_parser.rb │ ├── logger.rb │ ├── parser │ ├── class_builder.rb │ ├── qbxml_base.rb │ ├── xml_generation.rb │ └── xml_parsing.rb │ ├── qbxml_parser.rb │ └── support │ ├── inflection.rb │ └── monkey_patches.rb ├── quickbooks_api.gemspec ├── spec ├── quickbooks │ ├── api_spec.rb │ ├── class_builder_spec.rb │ ├── config_spec.rb │ ├── dtd_parser_spec.rb │ ├── inflection_spec.rb │ ├── logger_spec.rb │ ├── monkey_patches_spec.rb │ ├── qbxml_base_spec.rb │ ├── qbxml_parser_spec.rb │ ├── spec.opts │ ├── spec_helper.rb │ ├── xml_generation_spec.rb │ └── xml_parsing_spec.rb └── sample_data │ ├── customer_response_multiple.xml │ ├── customer_response_single.xml │ ├── inventory_response.xml │ ├── order_response.xml │ ├── receipt_response.xml │ └── vendor_response.xml └── xml_schema ├── qbposxmlops30.xml └── qbxmlops70.xml /.gitignore: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attention 2 | 3 | This gem is deprecated. Please use [qbxml](http://github.com/skryl/qbxml) instead. 4 | -------------------------------------------------------------------------------- /README.old: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | *quickbooks_api* is a quickbooks xml parser and API inspection tool. It gives 4 | you the ability to easilty go from qbxml to ruby and vice versa. 5 | 6 | ### Initialization 7 | ------------------------------------------------------------------------------ 8 | 9 | standard initialization 10 | 11 | api = Quickbooks::API.instance(:qbpos) 12 | 13 | shorthand initialization 14 | 15 | api = Quickbooks::API[:qbpos] 16 | 17 | 18 | :qb and :qbpos are the two supported init modes. 19 | 20 | ### API Introspection 21 | ------------------------------------------------------------------------------ 22 | 23 | return all available wrapper classes 24 | 25 | api.qbxml_classes 26 | 27 | return the top level wrapper class for the api 28 | 29 | api.container 30 | 31 | find specific qbxml class by name 32 | 33 | api.find('customer_mod_rq') 34 | 35 | find all qbxml classes that contain a pattern 36 | 37 | api.grep(/_mod_rq/) 38 | 39 | return a hash of all the data types for a wrapper class 40 | 41 | wrapper_class.template 42 | 43 | return the full teplate for a wrapper class (SLOOOW for top level classes) 44 | 45 | wrapper_class.template(true) 46 | 47 | return all the supported fields for the wrapper class 48 | 49 | wrapper_class.attribute_names 50 | 51 | return the qbxml template used to generate the wrapper class 52 | 53 | wrapper_class.xml_template 54 | 55 | 56 | ### QBXML To Ruby 57 | ------------------------------------------------------------------------------ 58 | 59 | wrap qbxml data in a qbxml object 60 | 61 | o = api.qbxml_to_obj(qbxml) 62 | 63 | convert qbxml object to hash 64 | 65 | o.inner_attributes 66 | 67 | same as above but includes the top level containers 68 | 69 | o.attributes 70 | 71 | retrieves attributes from nested objects 72 | 73 | o.attributes(true) 74 | 75 | directly convert raw qbxml to a hash 76 | 77 | h = api.qbxml_to_hash(qbxml) 78 | 79 | same as above but includes the top level containers 80 | 81 | h = api.qbxml_to_hash(qbxml, true) 82 | 83 | 84 | ### Ruby to QBXML 85 | ------------------------------------------------------------------------------ 86 | 87 | convert a hash to a qbxml object (automagically creates the top level containers) 88 | 89 | o = api.hash_to_obj(data_hash) 90 | 91 | convert a qbxml object to raw qbxml 92 | 93 | o.to_qbxml.to_s 94 | 95 | convert a hash directly to raw qbxml 96 | 97 | qbxml = api.hash_to_qbxml(data_hash) 98 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - tests 2 | - support for node type constraints and validation 3 | - hash to obj doesn't work when converting from an inner level hash ex. 'customer_mod_rq' 4 | - add processing instruction to node set 5 | - handle node attributes properly 6 | -------------------------------------------------------------------------------- /lib/quickbooks.rb: -------------------------------------------------------------------------------- 1 | # $LOAD_PATH.unshift "#{File.dirname(File.expand_path(__FILE__))}/../lib/" 2 | require 'rubygems' 3 | 4 | module Quickbooks; end 5 | module Quickbooks::QBXML; end 6 | module Quickbooks::QBPOSXML; end 7 | module Quickbooks::Support; end 8 | module Quickbooks::Parser; end 9 | 10 | require 'quickbooks/support/monkey_patches' 11 | require 'quickbooks/support/inflection' 12 | require 'quickbooks/logger' 13 | require 'quickbooks/config' 14 | require 'quickbooks/parser/xml_parsing' 15 | require 'quickbooks/parser/xml_generation' 16 | require 'quickbooks/parser/class_builder' 17 | require 'quickbooks/parser/qbxml_base' 18 | require 'quickbooks/qbxml_parser' 19 | require 'quickbooks/dtd_parser' 20 | require 'quickbooks/api' 21 | -------------------------------------------------------------------------------- /lib/quickbooks/api.rb: -------------------------------------------------------------------------------- 1 | class Quickbooks::API 2 | include Quickbooks::Logger 3 | include Quickbooks::Config 4 | include Quickbooks::Support::Inflection 5 | 6 | attr_reader :dtd_parser, :qbxml_parser, :schema_type 7 | private_class_method :new 8 | @@instances = {} 9 | 10 | def initialize(schema_type = nil, opts = {}) 11 | self.class.check_schema_type!(schema_type) 12 | @schema_type = schema_type 13 | 14 | @dtd_parser = Quickbooks::DtdParser.new(schema_type) 15 | @qbxml_parser = Quickbooks::QbxmlParser.new(schema_type) 16 | 17 | load_qb_classes 18 | @@instances[schema_type] = self 19 | end 20 | 21 | # simple singleton constructor without caching support 22 | # 23 | def self.[](schema_type) 24 | @@instances[schema_type] || new(schema_type) 25 | end 26 | 27 | # full singleton constructor 28 | # 29 | def self.instance(schema_type = nil, opts = {}) 30 | @@instances[schema_type] || new(schema_type, opts) 31 | end 32 | 33 | # user friendly api decorators. Not used anywhere else. 34 | # 35 | def container 36 | container_class 37 | end 38 | 39 | def qbxml_classes 40 | cached_classes 41 | end 42 | 43 | # api introspection 44 | # 45 | def find(class_name) 46 | cached_classes.find { |c| underscore(c) == class_name.to_s } 47 | end 48 | 49 | def grep(pattern) 50 | cached_classes.select { |c| underscore(c).match(/#{pattern}/) } 51 | end 52 | 53 | # QBXML 2 RUBY 54 | 55 | def qbxml_to_obj(qbxml) 56 | qbxml_parser.parse(qbxml) 57 | end 58 | 59 | def qbxml_to_hash(qbxml, include_container = false) 60 | if include_container 61 | qbxml_to_obj(qbxml).attributes 62 | else 63 | qbxml_to_obj(qbxml).inner_attributes 64 | end 65 | end 66 | 67 | # RUBY 2 QBXML 68 | 69 | def hash_to_obj(data) 70 | key, value = data.detect { |name, value| name != 'xml_attributes' && name != :xml_attributes } 71 | key_path = container_class.template(true).path_to_nested_key(key.to_s) 72 | raise(RuntimeError, "#{key} class not found in api template") unless key_path 73 | 74 | wrapped_data = Hash.nest(key_path, value) 75 | container_class.new(wrapped_data) 76 | end 77 | 78 | def hash_to_qbxml(data) 79 | hash_to_obj(data).to_qbxml 80 | end 81 | 82 | private 83 | 84 | def load_qb_classes 85 | rebuild_schema_cache(false) 86 | load_full_container_template 87 | container_class 88 | end 89 | 90 | # rebuilds schema cache in memory 91 | # 92 | def rebuild_schema_cache(force = false) 93 | dtd_parser.parse_file(dtd_file) if (cached_classes.empty? || force) 94 | end 95 | 96 | # load the recursive container class template into memory (significantly 97 | # speeds up wrapping of partial data hashes) 98 | # 99 | def load_full_container_template(use_disk_cache = false) 100 | container_class.template(true) 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /lib/quickbooks/config.rb: -------------------------------------------------------------------------------- 1 | module Quickbooks::Config 2 | 3 | API_ROOT = File.join(File.dirname(__FILE__), '..', '..').freeze 4 | XML_SCHEMA_PATH = File.join(API_ROOT, 'xml_schema').freeze 5 | RUBY_SCHEMA_PATH = File.join(API_ROOT, 'ruby_schema').freeze 6 | 7 | SCHEMA_MAP = { 8 | :qb => {:dtd_file => "qbxmlops70.xml", 9 | :namespace => Quickbooks::QBXML, 10 | :container_class => 'QBXML' 11 | }.freeze, 12 | :qbpos => {:dtd_file => "qbposxmlops30.xml", 13 | :namespace => Quickbooks::QBPOSXML, 14 | :container_class => 'QBPOSXML' 15 | }.freeze, 16 | }.freeze 17 | 18 | def self.included(klass) 19 | klass.extend ClassMethods 20 | end 21 | 22 | private 23 | 24 | def container_class 25 | schema_namespace.const_get(SCHEMA_MAP[schema_type][:container_class]) 26 | end 27 | 28 | def dtd_file 29 | "#{XML_SCHEMA_PATH}/#{SCHEMA_MAP[schema_type][:dtd_file]}" 30 | end 31 | 32 | def schema_namespace 33 | SCHEMA_MAP[schema_type][:namespace] 34 | end 35 | 36 | # introspection 37 | 38 | def cached_classes 39 | schema_namespace.constants.map { |const| schema_namespace.const_get(const) } 40 | end 41 | 42 | module ClassMethods 43 | 44 | def check_schema_type!(schema_type) 45 | unless SCHEMA_MAP.include?(schema_type) 46 | raise(ArgumentError, "valid schema type required: #{valid_schema_types.inspect}") 47 | end 48 | end 49 | 50 | private 51 | 52 | def valid_schema_types 53 | SCHEMA_MAP.keys 54 | end 55 | 56 | end 57 | 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/quickbooks/dtd_parser.rb: -------------------------------------------------------------------------------- 1 | class Quickbooks::DtdParser < Quickbooks::QbxmlParser 2 | include Quickbooks::Parser::ClassBuilder 3 | 4 | private 5 | 6 | def process_leaf_node(xml_obj, parent_class) 7 | attr_name, qb_type = parse_leaf_node_data(xml_obj) 8 | if parent_class 9 | log.debug [parent_class, attr_name, qb_type].inspect 10 | add_casting_attribute(parent_class, attr_name, qb_type) 11 | end 12 | end 13 | 14 | def process_non_leaf_node(xml_obj, parent_class) 15 | klass = build_qbxml_class(xml_obj) 16 | attr_name = underscore(xml_obj) 17 | if parent_class 18 | add_strict_attribute(parent_class, attr_name, klass) 19 | end 20 | klass 21 | end 22 | 23 | #TODO: stub 24 | def process_comment_node(xml_obj, parent_class) 25 | parent_class 26 | end 27 | 28 | # helpers 29 | 30 | def build_qbxml_class(xml_obj) 31 | obj_name = xml_obj.name 32 | unless schema_namespace.const_defined?(obj_name) 33 | klass = Class.new(Quickbooks::Parser::QbxmlBase) 34 | schema_namespace.const_set(obj_name, klass) 35 | klass.xml_attributes = parse_xml_attributes(xml_obj) 36 | add_xml_template(klass, xml_obj.to_xml) 37 | else 38 | klass = schema_namespace.const_get(obj_name) 39 | end 40 | klass 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/quickbooks/logger.rb: -------------------------------------------------------------------------------- 1 | require 'buffered_logger' 2 | 3 | module Quickbooks::Logger 4 | def log 5 | Quickbooks::Log.log 6 | end 7 | end 8 | 9 | class Quickbooks::Log 10 | private_class_method :new 11 | LOG_LEVEL = 6 12 | 13 | def self.init(log_level) 14 | @log = BufferedLogger.new(STDOUT, log_level || LOG_LEVEL) 15 | end 16 | 17 | def self.log 18 | @log ||= BufferedLogger.new(STDOUT, LOG_LEVEL) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/quickbooks/parser/class_builder.rb: -------------------------------------------------------------------------------- 1 | module Quickbooks::Parser::ClassBuilder 2 | 3 | private 4 | 5 | def add_strict_attribute(klass, attr_name, type) 6 | add_attribute_type(klass, attr_name, type) 7 | 8 | eval <<-class_body 9 | class #{klass} 10 | attr_accessor :#{attr_name} 11 | 12 | def #{attr_name}=(obj) 13 | if self.respond_to?("#{attr_name}_type") 14 | expected_type = self.class.#{attr_name}_type 15 | unless obj.is_a?(expected_type) || obj.is_a?(Array) 16 | raise(TypeError, "expecting an object of type \#{expected_type}") 17 | end 18 | end 19 | @#{attr_name} = obj 20 | end 21 | end 22 | class_body 23 | end 24 | 25 | def add_casting_attribute(klass, attr_name, type) 26 | type_casting_proc = klass::QB_TYPE_CONVERSION_MAP[type] 27 | ruby_type = type_casting_proc.call(nil).class 28 | add_attribute_type(klass, attr_name, ruby_type) 29 | 30 | eval <<-class_body 31 | class #{klass} 32 | attr_accessor :#{attr_name} 33 | 34 | def #{attr_name}=(obj) 35 | type_casting_proc = QB_TYPE_CONVERSION_MAP["#{type}"] 36 | @#{attr_name} = type_casting_proc ? type_casting_proc.call(obj) : obj 37 | end 38 | end 39 | class_body 40 | end 41 | 42 | def add_attribute_type(klass, attr_name, type) 43 | eval <<-class_body 44 | class #{klass} 45 | @@#{attr_name}_type = #{type} 46 | def self.#{attr_name}_type 47 | #{type} 48 | end 49 | end 50 | class_body 51 | end 52 | 53 | def add_xml_template(klass, xml_template) 54 | eval <<-class_body 55 | class #{klass} 56 | def self.xml_template 57 | #{xml_template.dump} 58 | end 59 | end 60 | class_body 61 | end 62 | 63 | 64 | end 65 | -------------------------------------------------------------------------------- /lib/quickbooks/parser/qbxml_base.rb: -------------------------------------------------------------------------------- 1 | # inheritance base for schema classes 2 | class Quickbooks::Parser::QbxmlBase 3 | include Quickbooks::Logger 4 | extend Quickbooks::Logger 5 | include Quickbooks::Parser::XMLGeneration 6 | 7 | QBXML_BASE = Quickbooks::Parser::QbxmlBase 8 | 9 | FLOAT_CAST = Proc.new {|d| d ? Float(d) : 0.0} 10 | BOOL_CAST = Proc.new {|d| d ? (d == 'True' ? true : false) : false } 11 | DATE_CAST = Proc.new {|d| d ? Date.parse(d).strftime("%Y-%m-%d") : Date.today.strftime("%Y-%m-%d") } 12 | TIME_CAST = Proc.new {|d| d ? Time.parse(d).xmlschema : Time.now.xmlschema } 13 | INT_CAST = Proc.new {|d| d ? Integer(d.to_i) : 0 } 14 | STR_CAST = Proc.new {|d| d ? String(d) : ''} 15 | 16 | QB_TYPE_CONVERSION_MAP= { 17 | "AMTTYPE" => FLOAT_CAST, 18 | "BOOLTYPE" => BOOL_CAST, 19 | "DATETIMETYPE" => TIME_CAST, 20 | "DATETYPE" => DATE_CAST, 21 | "ENUMTYPE" => STR_CAST, 22 | "FLOATTYPE" => FLOAT_CAST, 23 | "GUIDTYPE" => STR_CAST, 24 | "IDTYPE" => STR_CAST, 25 | "INTTYPE" => INT_CAST, 26 | "PERCENTTYPE" => FLOAT_CAST, 27 | "PRICETYPE" => FLOAT_CAST, 28 | "QUANTYPE" => INT_CAST, 29 | "STRTYPE" => STR_CAST, 30 | "TIMEINTERVALTYPE" => STR_CAST 31 | } 32 | 33 | attr_accessor :xml_attributes 34 | class << self 35 | attr_accessor :xml_attributes 36 | end 37 | 38 | def initialize(params = nil) 39 | return unless params.is_a?(Hash) 40 | @xml_attributes = params['xml_attributes'] || params[:xml_attributes] || {} 41 | params.delete('xml_attributes') 42 | params.delete(:xml_attributes) 43 | 44 | params.each do |attr, value| 45 | if self.respond_to?(attr) 46 | expected_attr_type = self.class.send("#{attr}_type") 47 | value = \ 48 | case value 49 | when Hash 50 | expected_attr_type.new(value) 51 | when Array 52 | value.inject([]) { |a,i| a << expected_attr_type.new(i) } 53 | else value 54 | end 55 | self.send("#{attr}=", value) 56 | else 57 | log.info "Warning: instance #{self} does not respond to attribute #{attr}" 58 | end 59 | end 60 | end 61 | 62 | def self.attribute_names 63 | # 1.9.2 changes instance_methods behavior to return symbols instead of strings 64 | instance_methods(false).reject { |m| m[-1..-1] == '=' || m.to_s =~ /xml_attributes/ || m =~ /_xml_class/}.map { |m| m.to_s } 65 | end 66 | 67 | # returns innermost attributes without outer layers of the hash 68 | # 69 | def inner_attributes(parent = self) 70 | attrs = attributes(false) 71 | attrs.delete('xml_attributes') 72 | values = attrs.values.compact 73 | 74 | if values.empty? 75 | attributes 76 | elsif values.first.is_a?(Array) 77 | attributes 78 | elsif values.size > 1 79 | parent.attributes 80 | else 81 | first_val = values.first 82 | if first_val.respond_to?(:inner_attributes) 83 | first_val.inner_attributes(self) 84 | else 85 | parent.attributes 86 | end 87 | end 88 | end 89 | 90 | def attributes(recursive = true) 91 | attrs = {} 92 | attrs['xml_attributes'] = xml_attributes 93 | self.class.attribute_names.inject(attrs) do |h, m| 94 | val = self.send(m) 95 | if !val.nil? 96 | if recursive 97 | h[m] = case val 98 | when QBXML_BASE 99 | val.attributes 100 | when Array 101 | val.inject([]) { |a, obj| obj.is_a?(QBXML_BASE) ? a << obj.attributes : a << obj } 102 | else val 103 | end 104 | else 105 | h[m] = val 106 | end 107 | end; h 108 | end 109 | end 110 | 111 | # returns a type map of the object's attributes 112 | # 113 | def self.template(recursive = false, reload = false) 114 | if recursive 115 | @template = (!reload && @template) || build_template(true) 116 | else build_template(false) 117 | end 118 | end 119 | 120 | private 121 | 122 | def self.build_template(recursive = false) 123 | attribute_names.inject({}) do |h, a| 124 | attr_type = self.send("#{a}_type") 125 | h[a] = ((attr_type < QBXML_BASE) && recursive) ? attr_type.build_template(true): attr_type.to_s; h 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /lib/quickbooks/parser/xml_generation.rb: -------------------------------------------------------------------------------- 1 | module Quickbooks::Parser::XMLGeneration 2 | include Quickbooks::Parser 3 | include Quickbooks::Parser::XMLParsing 4 | include Quickbooks::Support::Inflection 5 | 6 | def to_qbxml 7 | xml_doc = Nokogiri::XML(self.class.xml_template) 8 | root = xml_doc.root 9 | log.debug "to_qbxml#nodes_size: #{root.children.size}" 10 | 11 | # replace all children nodes of the template with populated data nodes 12 | xml_nodes = [] 13 | root.children.each do |xml_template| 14 | next unless xml_template.is_a? XML_ELEMENT 15 | attr_name = underscore(xml_template) 16 | log.debug "to_qbxml#attr_name: #{attr_name}" 17 | 18 | val = self.send(attr_name) 19 | next unless val && val.not_blank? 20 | 21 | xml_nodes += build_qbxml_nodes(xml_template, val) 22 | log.debug "to_qbxml#val: #{val}" 23 | end 24 | 25 | log.debug "to_qbxml#xml_nodes_size: #{xml_nodes.size}" 26 | root.children = xml_nodes.join('') 27 | set_xml_attributes!(root) 28 | root.to_s 29 | end 30 | 31 | private 32 | 33 | def build_qbxml_nodes(node, val) 34 | val = [val].flatten 35 | val.map do |v| 36 | case v 37 | when QbxmlBase 38 | v.to_qbxml 39 | else 40 | n = node.clone 41 | n.children = v.to_s 42 | n.to_s 43 | end 44 | end 45 | end 46 | 47 | def set_xml_attributes!(node) 48 | node.attributes.each { |name, value| node.remove_attribute(name) } 49 | self.xml_attributes.each { |a,v| node.set_attribute(a, v) } 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/quickbooks/parser/xml_parsing.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | module Quickbooks::Parser::XMLParsing 4 | 5 | XML_DOCUMENT = Nokogiri::XML::Document 6 | XML_NODE_SET = Nokogiri::XML::NodeSet 7 | XML_NODE = Nokogiri::XML::Node 8 | XML_ELEMENT = Nokogiri::XML::Element 9 | XML_COMMENT= Nokogiri::XML::Comment 10 | XML_TEXT = Nokogiri::XML::Text 11 | 12 | COMMENT_START = "" 14 | COMMENT_MATCHER = /\A#{COMMENT_START}.*#{COMMENT_END}\z/ 15 | 16 | # remove all comment lines and empty nodes 17 | def cleanup_qbxml(qbxml) 18 | qbxml = qbxml.split('\n') 19 | qbxml.map! { |l| l.strip } 20 | qbxml.reject! { |l| l =~ COMMENT_MATCHER } 21 | qbxml.join('') 22 | end 23 | 24 | def leaf_node?(xml_obj) 25 | (xml_obj.children.size == 0 || xml_obj.children.size == 1) && xml_obj.attributes.empty? 26 | end 27 | 28 | def parse_leaf_node_data(xml_obj) 29 | attr_name = underscore(xml_obj) 30 | text_node = xml_obj.children.first 31 | [attr_name, text_node && text_node.text] 32 | end 33 | 34 | def parse_xml_attributes(xml_obj) 35 | attrs = xml_obj.attributes 36 | attrs.inject({}) { |h, (n,v)| h[n] = v.value; h } 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/quickbooks/qbxml_parser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | class Quickbooks::QbxmlParser 4 | include Quickbooks::Config 5 | include Quickbooks::Logger 6 | include Quickbooks::Parser::XMLParsing 7 | include Quickbooks::Support::Inflection 8 | 9 | attr_accessor :schema_type 10 | 11 | def initialize(schema_type) 12 | @schema_type = schema_type 13 | end 14 | 15 | def parse_file(qbxml_file) 16 | parse( cleanup_qbxml( File.read_from_unknown(qbxml_file) ) ) 17 | end 18 | 19 | def parse(qbxml) 20 | xml_doc = Nokogiri::XML(qbxml) 21 | process_xml_obj(xml_doc, nil) 22 | end 23 | 24 | private 25 | 26 | def process_xml_obj(xml_obj, parent) 27 | case xml_obj 28 | when XML_DOCUMENT 29 | process_xml_obj(xml_obj.root, parent) 30 | when XML_NODE_SET 31 | if !xml_obj.empty? 32 | process_xml_obj(xml_obj.shift, parent) 33 | process_xml_obj(xml_obj, parent) 34 | end 35 | when XML_ELEMENT 36 | if leaf_node?(xml_obj) 37 | process_leaf_node(xml_obj, parent) 38 | else 39 | obj = process_non_leaf_node(xml_obj, parent) 40 | process_xml_obj(xml_obj.children, obj) 41 | obj 42 | end 43 | when XML_COMMENT 44 | process_comment_node(xml_obj, parent) 45 | end 46 | end 47 | 48 | def process_leaf_node(xml_obj, parent_instance) 49 | attr_name, data = parse_leaf_node_data(xml_obj) 50 | if parent_instance 51 | set_attribute_value(parent_instance, attr_name, data) 52 | end 53 | parent_instance 54 | end 55 | 56 | def process_non_leaf_node(xml_obj, parent_instance) 57 | instance = fetch_qbxml_class_instance(xml_obj) 58 | attr_name = underscore(instance.class) 59 | if parent_instance 60 | set_attribute_value(parent_instance, attr_name, instance) 61 | end 62 | instance 63 | end 64 | 65 | #TODO: stub 66 | def process_comment_node(xml_obj, parent_instance) 67 | parent_instance 68 | end 69 | 70 | # helpers 71 | 72 | def fetch_qbxml_class_instance(xml_obj) 73 | obj = schema_namespace.const_get(xml_obj.name).new 74 | obj.xml_attributes = parse_xml_attributes(xml_obj) 75 | obj 76 | end 77 | 78 | def set_attribute_value(instance, attr_name, data) 79 | if instance.respond_to?(attr_name) 80 | cur_val = instance.send(attr_name) 81 | case cur_val 82 | when nil 83 | instance.send("#{attr_name}=", data) 84 | when Array 85 | cur_val << data 86 | else 87 | instance.send("#{attr_name}=", [cur_val, data]) 88 | end 89 | else 90 | log.info "Warning: instance #{instance} does not respond to attribute #{attr_name}" 91 | end 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /lib/quickbooks/support/inflection.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext' 2 | 3 | module Quickbooks::Support::Inflection 4 | 5 | def underscore(obj) 6 | name = \ 7 | case obj 8 | when Class 9 | obj.simple_name 10 | when Nokogiri::XML::Element 11 | obj.name 12 | else 13 | obj.to_s 14 | end 15 | name.underscore 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/quickbooks/support/monkey_patches.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | 3 | def path_to_nested_key(key) 4 | each do |k,v| 5 | path = [k] 6 | if k == key 7 | return path 8 | elsif v.is_a? Hash 9 | nested_path = v.path_to_nested_key(key) 10 | return (path + nested_path) if nested_path 11 | end 12 | end 13 | return nil 14 | end 15 | 16 | def self.nest(path, value) 17 | hash_constructor = Proc.new { |h, k| h[k] = Hash.new(&hash_constructor) } 18 | nested_hash = Hash.new(&hash_constructor) 19 | 20 | last_key = path.last 21 | path.inject(nested_hash) { |h, k| (k == last_key) ? h[k] = value : h[k] } 22 | nested_hash 23 | end 24 | 25 | end 26 | 27 | class Class 28 | def simple_name 29 | self.to_s.split("::").last 30 | end 31 | end 32 | 33 | class Object 34 | def not_blank? 35 | !self.blank? 36 | end 37 | end 38 | 39 | class File 40 | def self.read_from_unknown(file) 41 | case file 42 | when String 43 | File.read(file) 44 | when IO 45 | file.read 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /quickbooks_api.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'quickbooks_api' 3 | s.version = '0.1.7' 4 | 5 | s.summary = "QuickBooks XML API" 6 | s.description = %{A QuickBooks QBXML wrapper for Ruby} 7 | 8 | s.author = "Alex Skryl" 9 | s.email = "rut216@gmail.com" 10 | s.homepage = "http://github.com/skryl" 11 | s.files = `git ls-files`.split($/) 12 | s.require_paths = ["lib"] 13 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 14 | s.require_paths = ["lib"] 15 | 16 | 17 | s.add_dependency(%q, [">= 0"]) 18 | s.add_dependency(%q, [">= 0"]) 19 | s.add_dependency(%q, [">= 0.1.3"]) 20 | end 21 | -------------------------------------------------------------------------------- /spec/quickbooks/api_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::API do 4 | 5 | describe "interface" do 6 | 7 | it "should initialize api instance" do 8 | end 9 | 10 | it "should return cached instance for consecutive api requests" do 11 | end 12 | 13 | it "should create qbxml object from qbxml input" do 14 | end 15 | 16 | it "should create qbxml object from hash input" do 17 | end 18 | 19 | it "should convert qbxml input to hash output" do 20 | end 21 | 22 | it "should convert hash input to qbxml output" do 23 | end 24 | 25 | end 26 | 27 | describe "helpers" do 28 | 29 | it "should load the schema into memory during initialization" do 30 | end 31 | 32 | it "should load the container template into memory before initialization" do 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /spec/quickbooks/class_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Parser::ClassBuilder do 4 | 5 | it "should add a type checked attribute accessor to a class" do 6 | end 7 | 8 | it "should add a casting attribute accessor to a class" do 9 | end 10 | 11 | it "should add a type accessor for an attribute" do 12 | end 13 | 14 | it "should add the appropriate xml template to each ruby wrapper object" do 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/quickbooks/config_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Config do 4 | 5 | #describe "configuration helpers" do 6 | 7 | #it "should return the supported schema types" do 8 | #valid_schema_types.should be_an_instance_of(Array) 9 | #valid_schema_types.include?(:qb).should be_true 10 | #valid_schema_types.include?(:qbpos).should be_true 11 | #end 12 | 13 | #it "should check if a particular schema type is supported" do 14 | #self.stub!(:schema_type => :qb) 15 | #valid_schema_type?.should be_true 16 | #self.stub!(:schema_type => :qbpos) 17 | #valid_schema_type?.should be_true 18 | #self.stub!(:schema_type => :doesntexist) 19 | #valid_schema_type?.should == false 20 | #end 21 | 22 | #it "should determine the dtd file path for any supported schema type" do 23 | #self.stub!(:schema_type => :qb) 24 | #File.exists?(dtd_file).should be_true 25 | #self.stub!(:schema_type => :qbpos) 26 | #File.exists?(dtd_file).should be_true 27 | #end 28 | 29 | #it "should determine the namespace for any supported schema type" do 30 | #self.stub!(:schema_type => :qb) 31 | #schema_namespace.should be_a_kind_of(Module) 32 | #self.stub!(:schema_type => :qbpos) 33 | #schema_namespace.should be_a_kind_of(Module) 34 | #end 35 | 36 | #it "should determine the container class for any supported schema type" do 37 | #self.stub!(:schema_type => :qb) 38 | #container_class.should == 'QBXML' 39 | #self.stub!(:schema_type => :qbpos) 40 | #schema_namespace.should == 'QBPOSXML' 41 | #end 42 | 43 | #end 44 | 45 | #describe "other helpers" do 46 | 47 | #before :all do 48 | #@qb_api = Quickbooks::API.new(:qb) 49 | #@qbpos_api = Quickbooks::API.new(:qbpos) 50 | #end 51 | 52 | #it "should return all the cached classes" do 53 | #self.stub!(:schema_type => :qb) 54 | #cached_classes.should be_an_instance_of(Array) 55 | #cached_classes.empty?.should be_false 56 | #cached_classes.include?(container_class).should be_true 57 | #self.stub!(:schema_type => :qbpos) 58 | #cached_classes.should be_an_instance_of(Array) 59 | #cached_classes.empty?.should be_false 60 | #cached_classes.include?(container_class).should be_true 61 | #end 62 | 63 | #it "should check if a particular class is cached" do 64 | #self.stub!(:schema_type => :qb) 65 | #is_cached_class?(Quickbooks::QBXML::QBXML).should be_true 66 | #self.stub!(:schema_type => :qbpos) 67 | #is_cached_class?(Quickbooks::QBPOSXML::QBPOSXML).should be_true 68 | #end 69 | 70 | #end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /spec/quickbooks/dtd_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::DtdParser do 4 | 5 | it "should parse a dtd file and build wrapper classes" do 6 | end 7 | 8 | it "should add a casting attribute to a leaf node class" do 9 | end 10 | 11 | it "should add a strict attribute to a non leaf node class" do 12 | end 13 | 14 | it "should should convert a commment into a data validator for the appropriate attribute" do 15 | end 16 | 17 | it "should create a new wrapper class for every xml node" do 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/quickbooks/inflection_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Support::Inflection do 4 | 5 | #before :each do 6 | #@klass = Nokogiri::XML::NodeSet 7 | #stub_child = stub(:class => XML_TEXT) 8 | #@stub_xml_node = stub(:name => "SomeNodeName", :children => [stub_child]) 9 | #@stub_xml_node.stub!(:is_a?).with(anything).and_return(false) 10 | #@stub_xml_node.stub!(:is_a?).with(XML_ELEMENT).and_return(true) 11 | #end 12 | 13 | #it "should convert a class or xml element name to underscored" do 14 | #underscore(@klass).should == 'node_set' 15 | #underscore(@stub_xml_node).should == 'some_node_name' 16 | #end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/quickbooks/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Logger do 4 | 5 | it "should initialize an ActiveSupport::BufferedLogger" do 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/quickbooks/monkey_patches_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe "Monkey Patches" do 4 | 5 | before :each do 6 | end 7 | 8 | #it "should convert a full class name to a simple class name" do 9 | #@klass.simple_name.should == 'NodeSet' 10 | #end 11 | end 12 | -------------------------------------------------------------------------------- /spec/quickbooks/qbxml_base_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Parser::QbxmlBase do 4 | 5 | it "should have a type conversion map with all qb types" do 6 | end 7 | 8 | it "should initialize a qbxml object given an attribute hash" do 9 | end 10 | 11 | it "should generate qbxml from a qbxml object" do 12 | end 13 | 14 | it "should a hash of attributes for the qbxml object" do 15 | end 16 | 17 | it "should return the attribute names of the qbxml class" do 18 | end 19 | 20 | it "should return the type map of the qbxml class" do 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/quickbooks/qbxml_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::QbxmlParser do 4 | 5 | it "should set the schema type during initialization" do 6 | end 7 | 8 | it "should parse a qbxml file and build wrapper classes" do 9 | end 10 | 11 | it "should parse a qbxml string and build wrapper classes" do 12 | end 13 | 14 | it "should process an xml document and build wrapper classes" do 15 | end 16 | 17 | it "should set the attribute value for a leaf node to a literal" do 18 | end 19 | 20 | it "should set the attribute value for a non leaf node to a nested class" do 21 | end 22 | 23 | it "should extract data from a leaf node" do 24 | end 25 | 26 | it "should fetch the wrapper instance for any qbxml node" do 27 | end 28 | 29 | it "should set an objects attribute" do 30 | end 31 | 32 | end 33 | 34 | -------------------------------------------------------------------------------- /spec/quickbooks/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format s 3 | --loadby mtime 4 | --reverse 5 | -------------------------------------------------------------------------------- /spec/quickbooks/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), "../lib") 2 | require 'rspec' 3 | require 'quickbooks' 4 | -------------------------------------------------------------------------------- /spec/quickbooks/xml_generation_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Parser::XMLGeneration do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/quickbooks/xml_parsing_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "spec_helper.rb") 2 | 3 | describe Quickbooks::Parser::XMLParsing do 4 | 5 | #it "should set useful parsing constants" do 6 | #XML_DOCUMENT.should == Nokogiri::XML::Document 7 | #XML_NODE_SET.should == Nokogiri::XML::NodeSet 8 | #XML_NODE.should == Nokogiri::XML::Node 9 | #XML_ELEMENT.should == Nokogiri::XML::Element 10 | #XML_COMMENT= Nokogiri::XML::Comment 11 | #XML_TEXT.should == Nokogiri::XML::Text 12 | #end 13 | 14 | #describe "xml parsing helpers" do 15 | 16 | #before :each do 17 | #@klass = Nokogiri::XML::NodeSet 18 | #stub_child = stub(:class => XML_TEXT) 19 | #@stub_xml_node = stub(:name => "SomeNodeName", :children => [stub_child]) 20 | #@stub_xml_node.stub!(:is_a?).with(anything).and_return(false) 21 | #@stub_xml_node.stub!(:is_a?).with(XML_ELEMENT).and_return(true) 22 | #end 23 | 24 | #it "should check if a node is a leaf node" do 25 | #is_leaf_node?(@stub_xml_node).should be_true 26 | #end 27 | 28 | #end 29 | 30 | #it "should check if a class is already defined in a particular namespace" do 31 | #end 32 | 33 | #it "should cleanup qbxml" do 34 | #end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/sample_data/customer_response_multiple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -7153013893932089081 6 | 2011-06-16T11:03:18-05:00 7 | 0.00 8 | 0.00 9 | 0.00 10 | None 11 | alex@gmail.com 12 | Alex 13 | Alex Skryl 14 | True 15 | False 16 | Skryl 17 | 8001112345 18 | 1 19 | Modified 20 | 21 | 22 | -7153013893932089082 23 | 2011-06-16T11:03:18-05:00 24 | 0.00 25 | 0.00 26 | 0.00 27 | None 28 | alex@gmail.com 29 | Alex 30 | Alex Skryl 31 | True 32 | False 33 | Skryl 34 | 8001112345 35 | 1 36 | Modified 37 | 38 | 39 | -7153013893932089083 40 | 2011-06-16T11:03:18-05:00 41 | 0.00 42 | 0.00 43 | 0.00 44 | None 45 | alex@gmail.com 46 | Alex 47 | Alex Skryl 48 | True 49 | False 50 | Skryl 51 | 8001112345 52 | 1 53 | Modified 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/sample_data/customer_response_single.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -7153013893932089081 6 | 2011-06-16T11:03:18-05:00 7 | 0.00 8 | 0.00 9 | 0.00 10 | None 11 | alex@gmail.com 12 | Alex 13 | Alex Skryl 14 | True 15 | False 16 | Skryl 17 | 8001112345 18 | 1 19 | Modified 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spec/sample_data/inventory_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Red 6 | 51.00 7 | -7153010144316587775 8 | Yellow Cup 9 | A Yellow Cup 10 | False 11 | True 12 | True 13 | False 14 | 123 15 | Inventory 16 | -7153010535175388927 17 | 0.00 18 | 100 19 | 9 20 | 0.00 21 | 53.00 22 | 10.00 23 | 9.00 24 | 9.00 25 | 9.00 26 | 21.00 27 | 22.00 28 | 555.00 29 | Optional 30 | Larger 31 | Modified 32 | Tax 33 | 2011-06-16T11:04:02-05:00 34 | 2011-06-16T11:04:02-05:00 35 | 1111111111111169 36 | -7153004882964872959 37 | 38 | 0.00 39 | 40 | 41 | 0.00 42 | 43 | 44 | 0.00 45 | 46 | 47 | 0.00 48 | 49 | 50 | 51 | 0.00 52 | -7153003340987727615 53 | ViewSonic Monitor 54 | False 55 | True 56 | True 57 | False 58 | 2 59 | Inventory 60 | -7153004736919207679 61 | 0.00 62 | 100 63 | 0 64 | 0.00 65 | 0.00 66 | 100.00 67 | 90.00 68 | 90.00 69 | 90.00 70 | 0.00 71 | 0.00 72 | 0.00 73 | Optional 74 | Modified 75 | Tax 76 | 2011-06-16T11:07:43-05:00 77 | 1111111111111112 78 | -7153003877908971263 79 | 80 | 0.00 81 | 82 | 83 | 0.00 84 | 85 | 86 | 0.00 87 | 88 | 89 | 0.00 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /spec/sample_data/order_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | John 6 | -7153012137256910591 7 | -7134405948113780479 8 | 2011-06-16T11:04:02-05:00 9 | 2011-06-16T11:04:02-05:00 10 | Alex 11 | 110.00 12 | 0.00 13 | 5.00 14 | 3.00 15 | 2 16 | 1 17 | 2.00 18 | 123 19 | Open 20 | Modified 21 | 110.00 22 | 15.00 23 | Local Sales Tax 24 | 16.00 25 | 110.00 26 | 2011-06-21 27 | 100.00 28 | 29 | Alex 30 | Skryl 31 | 32 | 33 | 0.00 34 | 35 | 36 | -7153010535175388927 37 | 1 38 | Alex 39 | 0.00 40 | Yellow Cup 41 | A Yellow Cup 42 | 0.00 43 | 0.00 44 | 11.00 45 | 7.00 46 | 1111 47 | 10.00 48 | 1 49 | Larger 50 | 1.00 51 | 0.00 52 | Tax 53 | 0.00 54 | 1111111111111111 55 | 56 | 57 | -7153004736919207679 58 | 2 59 | Alex 60 | 0.00 61 | ViewSonic Monitor 62 | 0.00 63 | 0.00 64 | 100.00 65 | 0.00 66 | 2 67 | 100.00 68 | 1 69 | 1.00 70 | 0.00 71 | Tax 72 | 0.00 73 | 1111111111111112 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /spec/sample_data/receipt_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | John 6 | -7134401610112925439 7 | 2011-06-16T11:04:02-05:00 8 | 2011-06-16T11:04:02-05:00 9 | -7153013893932089087 10 | 4.00 11 | 5.00 12 | Regular 13 | 1 14 | 1 15 | NotPosted 16 | 18 17 | Sales 18 | Modified 19 | 1 20 | 50.00 21 | 19.00 22 | Local Sales Tax 23 | 21.00 24 | Cash 25 | 50.00 26 | 2011-06-21 27 | 1 28 | 29 | Alex 30 | Skryl 31 | 8472932591 32 | 33 | 34 | 0.00 35 | 36 | 37 | -7134403723295555327 38 | 0.00 39 | 0.00 40 | Aquafina Waterbottle 41 | 0.00 42 | 0.00 43 | 50.00 44 | 0.00 45 | 3 46 | 50.00 47 | 1 48 | 1.00 49 | 0.00 50 | Tax 51 | 0.00 52 | 3333333333333333 53 | 54 | 55 | 50.00 56 | 57 | 58 | 59 | -7121131527051771647 60 | 2011-06-25T01:15:29-05:00 61 | -7153013893932089087 62 | 0.00 63 | 0.00 64 | Regular 65 | 3 66 | 1 67 | NotPosted 68 | 2 69 | Sales 70 | Modified 71 | 1 72 | 270.00 73 | 0.00 74 | Local Sales Tax 75 | 0.00 76 | Cash 77 | 270.00 78 | 2011-06-25 79 | 1 80 | 81 | 0.00 82 | 83 | 84 | -7153004736919207679 85 | 0.00 86 | 0.00 87 | ViewSonic Monitor 88 | 0.00 89 | 0.00 90 | 100.00 91 | 0.00 92 | 2 93 | 100.00 94 | 1 95 | 1.00 96 | 0.00 97 | Tax 98 | 0.00 99 | 1111111111111112 100 | 101 | 102 | -7153010535175388927 103 | 0.00 104 | 0.00 105 | Yellow Cup 106 | A Yellow Cup 107 | 0.00 108 | 0.00 109 | 20.00 110 | 0.00 111 | 1 112 | 10.00 113 | 1 114 | 2.00 115 | 0.00 116 | Tax 117 | 0.00 118 | 1111111111111111 119 | 120 | 121 | -7134403723295555327 122 | 0.00 123 | 0.00 124 | Aquafina Waterbottle 125 | 0.00 126 | 0.00 127 | 150.00 128 | 0.00 129 | 3 130 | 50.00 131 | 1 132 | 3.00 133 | 0.00 134 | Tax 135 | 0.00 136 | 3333333333333333 137 | 138 | 139 | 270.00 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /spec/sample_data/vendor_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -7153004882964872959 6 | Yellow Cups Ltd 7 | False 8 | Modified 9 | 0.00 10 | 0 11 | 0 12 | vendor@gmail.com 13 | James 14 | 123-123-1255 15 | AB123 16 | 2011-06-16T11:04:02-05:00 17 | 2011-06-16T11:04:02-05:00 18 | 19 | 20 | -7153003877908971263 21 | 2011-06-16T11:06:49-05:00 22 | ViewSonic 23 | False 24 | Modified 25 | 0.00 26 | 0 27 | 0 28 | 29 | 30 | -7134402220023447295 31 | 2011-06-21T11:25:13-05:00 32 | Aquafina 33 | False 34 | Modified 35 | 0.00 36 | 0 37 | 0 38 | 39 | 40 | 1000000002 41 | 2011-06-13T12:08:58-05:00 42 | System 43 | False 44 | Modified 45 | 0.00 46 | 0 47 | 0 48 | SYS 49 | 50 | 51 | 52 | 53 | --------------------------------------------------------------------------------