I needed to export XFDF from an application. This code is kind of untested, but is based on the solution I came up with. I would appreciate responses/modifications. It's very straightforward mixin stuff, for the most part.
module FDF
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def exports_xfdf(options = {})
raise ArgumentError, "A :filename option must be specified." unless options[:filename]
options[:indentation] ||= 2
options[:include] = options[:include].is_a?(Array) ? options[:include] : [options[:include]].compact
unless included_modules.include? XFDFMethods
class_inheritable_accessor :xfdf_options
extend ClassMethods
include XFDFMethods
end
self.xfdf_options = options
end
def exports_fdf(options = {})
raise ArgumentError, "A :filename option must be specified." unless options[:filename]
options[:include] = options[:include].is_a?(Array) ? options[:include] : [options[:include]].compact
unless included_modules.include? FDFMethods
class_inheritable_accessor :fdf_options
extend ClassMethods
include FDFMethods
end
self.fdf_options = options
end
end
module XFDFMethods
def to_xfdf(options = {})
options.reverse_merge! self.class.xfdf_options
fields = Util.collect_values(self, self.class.content_columns.map(&:name), options)
filename = options[:filename]
xml = Builder::XmlMarkup.new :indentation => options[:indentation]
xml.instruct!
xml.xfdf("xmlns" => "http://ns.adobe.com/xfdf/", "xml:space" => "preserve") {
xml.f :href => filename
xml.fields {
fields.each do |field, value|
xml.field(:name => field) {
if value.is_a? Array
value.each {|item| xml.value(item.to_s) }
else
xml.value(value.to_s)
end
}
end
}
}
xml.target!
end
end
module FDFMethods
def to_fdf(options={})
options.reverse_merge! self.class.fdf_options
fields = Util.collect_values(self, self.class.content_columns.map(&:name), options)
filename = options[:filename]
data = "%FDF-1.2\n%âã�?Ó\n1 0 obj\n<< \n/FDF << /Fields [ "
fields.each do |field, value|
if value.is_a? Array
data << "<</T(#{field})/V["
value.each {|v| data << "(#{v.strip})"}
data << "]>>"
else
data << "<</T(#{field})/V(#{value.strip})>>"
end
end
end
data << "] \n/F (#{filename}) /ID [ <#{MD5.md5(Time.now).to_s}>\n ] >>" <<
" \n>> \nendobj\ntrailer\n" << "<<\n/Root 1 0 R \n\n>>\n%%EOF\n"
end
module Util
def self.collect_values(object, defaults, options = {})
attrs = []
if options[:attributes]
attrs = stringify_all(options[:attributes]) rescue []
else
[:include_attributes, :exclude_attributes].each do |opt|
options[opt] = stringify_all(options[opt]) rescue []
end
attrs = stringify_all(defaults) + options[:include_attributes] - options[:exclude_attributes]
end
fields = attrs.inject({}) do |hash, key|
value = object.send(key) rescue nil
hash.merge key => value
end
fields.merge collect_association_values(object, options)
end
def self.collect_association_values(object, options = {})
return {} if options[:include].blank?
values = {}
options[:include].each do |association|
unless object.send(association).blank?
models = object.send(association)
unless models.is_a? Array
columns = models.class.content_columns.map(&:name)
values.merge! association_dump(association, models, columns)
else
models.each_with_index do |model, index|
columns = model.class.content_columns.map(&:name)
values.merge! association_dump("#{association.singularize}_#{index+1}", model, columns)
end
end
end
end
values
end
def self.association_dump(prefix, object, attributes)
attributes.inject({}) do |hash, attr|
value = object.send(attr) rescue nil
hash.merge "#{prefix}_#{attr}" => value
end
end
def self.stringify_all(ary)
ary.compact.map(&:to_s).uniq
end
end
end
ActiveRecord::Base.send :include, FDF
Also available from: http://pastie.caboo.se/38835