lardawge-rfm 1.4.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rfm/record.rb ADDED
@@ -0,0 +1,219 @@
1
+ module Rfm
2
+
3
+ # The Record object represents a single FileMaker record. You typically get them from ResultSet objects.
4
+ # For example, you might use a Layout object to find some records:
5
+ #
6
+ # results = myLayout.find({"First Name" => "Bill"})
7
+ #
8
+ # The +results+ variable in this example now contains a ResultSet object. ResultSets are really just arrays of
9
+ # Record objects (with a little extra added in). So you can get a record object just like you would access any
10
+ # typical array element:
11
+ #
12
+ # first_record = results[0]
13
+ #
14
+ # You can find out how many record were returned:
15
+ #
16
+ # record_count = results.size
17
+ #
18
+ # And you can of course iterate:
19
+ #
20
+ # results.each (|record|
21
+ # // you can work with the record here
22
+ # )
23
+ #
24
+ # =Accessing Field Data
25
+ #
26
+ # You can access field data in the Record object in two ways. Typically, you simply treat Record like a hash
27
+ # (because it _is_ a hash...I love OOP). Keys are field names:
28
+ #
29
+ # first = myRecord["First Name"]
30
+ # last = myRecord["Last Name"]
31
+ #
32
+ # If your field naming conventions mean that your field names are also valid Ruby symbol named (ie: they contain only
33
+ # letters, numbers, and underscores) then you can treat them like attributes of the record. For example, if your fields
34
+ # are called "first_name" and "last_name" you can do this:
35
+ #
36
+ # first = myRecord.first_name
37
+ # last = myRecord.last_name
38
+ #
39
+ # Note: This shortcut will fail (in a rather mysterious way) if your field name happens to match any real attribute
40
+ # name of a Record object. For instance, you may have a field called "server". If you try this:
41
+ #
42
+ # server_name = myRecord.server
43
+ #
44
+ # you'll actually set +server_name+ to the Rfm::Server object this Record came from. This won't fail until you try
45
+ # to treat it as a String somewhere else in your code. It is also possible a future version of Rfm will include
46
+ # new attributes on the Record class which may clash with your field names. This will cause perfectly valid code
47
+ # today to fail later when you upgrade. If you can't stomach this kind of insanity, stick with the hash-like
48
+ # method of field access, which has none of these limitations. Also note that the +myRecord[]+ method is probably
49
+ # somewhat faster since it doesn't go through +method_missing+.
50
+ #
51
+ # =Accessing Repeating Fields
52
+ #
53
+ # If you have a repeating field, RFM simply returns an array:
54
+ #
55
+ # val1 = myRecord["Price"][0]
56
+ # val2 = myRecord["Price"][1]
57
+ #
58
+ # In the above example, the Price field is a repeating field. The code puts the first repetition in a variable called
59
+ # +val1+ and the second in a variable called +val2+.
60
+ #
61
+ # =Accessing Portals
62
+ #
63
+ # If the ResultSet includes portals (because the layout it comes from has portals on it) you can access them
64
+ # using the Record::portals attribute. It is a hash with table occurrence names for keys, and arrays of Record
65
+ # objects for values. In other words, you can do this:
66
+ #
67
+ # myRecord.portals["Orders"].each {|record|
68
+ # puts record["Order Number"]
69
+ # }
70
+ #
71
+ # This code iterates through the rows of the _Orders_ portal.
72
+ #
73
+ # =Field Types and Ruby Types
74
+ #
75
+ # RFM automatically converts data from FileMaker into a Ruby object with the most reasonable type possible. The
76
+ # type are mapped thusly:
77
+ #
78
+ # * *Text* fields are converted to Ruby String objects
79
+ #
80
+ # * *Number* fields are converted to Ruby BigDecimal objects (the basic Ruby numeric types have
81
+ # much less precision and range than FileMaker number fields)
82
+ #
83
+ # * *Date* fields are converted to Ruby Date objects
84
+ #
85
+ # * *Time* fields are converted to Ruby DateTime objects (you can ignore the date component)
86
+ #
87
+ # * *Timestamp* fields are converted to Ruby DateTime objects
88
+ #
89
+ # * *Container* fields are converted to Ruby URI objects
90
+ #
91
+ # =Attributes
92
+ #
93
+ # In addition to +portals+, the Record object has these useful attributes:
94
+ #
95
+ # * *record_id* is FileMaker's internal identifier for this record (_not_ any ID field you might have
96
+ # in your table); you need a +record_id+ to edit or delete a record
97
+ #
98
+ # * *mod_id* is the modification identifier for the record; whenever a record is modified, its +mod_id+
99
+ # changes so you can tell if the Record object you're looking at is up-to-date as compared to another
100
+ # copy of the same record
101
+ class Record < Rfm::CaseInsensitiveHash
102
+
103
+ attr_reader :record_id, :mod_id, :portals
104
+
105
+ def initialize(record, result, field_meta, layout, portal=nil)
106
+ @record_id = record['record-id']
107
+ @mod_id = record['mod-id']
108
+ @mods = {}
109
+ @layout = layout
110
+ @portals ||= Rfm::CaseInsensitiveHash.new
111
+
112
+ relatedsets = !portal && result.instance_variable_get(:@include_portals) ? record.xpath('relatedset') : []
113
+
114
+ record.xpath('field').each do |field|
115
+ field_name = field['name']
116
+ field_name.gsub!(Regexp.new(portal + '::'), '') if portal
117
+ datum = []
118
+
119
+ field.xpath('data').each do |x|
120
+ datum.push(field_meta[field_name].coerce(x.inner_text, result))
121
+ end
122
+
123
+ if datum.length == 1
124
+ self[field_name] = datum[0]
125
+ elsif datum.length == 0
126
+ self[field_name] = nil
127
+ else
128
+ self[field_name] = datum
129
+ end
130
+ end
131
+
132
+ unless relatedsets.empty?
133
+ relatedsets.each do |relatedset|
134
+ tablename, records = relatedset['table'], []
135
+
136
+ relatedset.xpath('record').each do |record|
137
+ records << self.class.new(record, result, result.portal_meta[tablename], layout, tablename)
138
+ end
139
+
140
+ @portals[tablename] = records
141
+ end
142
+ end
143
+
144
+ @loaded = true
145
+ end
146
+
147
+ def self.build_records(records, result, field_meta, layout, portal=nil)
148
+ records.each do |record|
149
+ result << self.new(record, result, field_meta, layout, portal)
150
+ end
151
+ end
152
+
153
+ # Saves local changes to the Record object back to Filemaker. For example:
154
+ #
155
+ # myLayout.find({"First Name" => "Bill"}).each(|record|
156
+ # record["First Name"] = "Steve"
157
+ # record.save
158
+ # )
159
+ #
160
+ # This code finds every record with _Bill_ in the First Name field, then changes the first name to
161
+ # Steve.
162
+ #
163
+ # Note: This method is smart enough to not bother saving if nothing has changed. So there's no need
164
+ # to optimize on your end. Just save, and if you've changed the record it will be saved. If not, no
165
+ # server hit is incurred.
166
+ def save
167
+ self.merge(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
168
+ @mods.clear
169
+ end
170
+
171
+ # Like Record::save, except it fails (and raises an error) if the underlying record in FileMaker was
172
+ # modified after the record was fetched but before it was saved. In other words, prevents you from
173
+ # accidentally overwriting changes someone else made to the record.
174
+ def save_if_not_modified
175
+ self.merge(@layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
176
+ @mods.clear
177
+ end
178
+
179
+ # Gets the value of a field from the record. For example:
180
+ #
181
+ # first = myRecord["First Name"]
182
+ # last = myRecord["Last Name"]
183
+ #
184
+ # This sample puts the first and last name from the record into Ruby variables.
185
+ #
186
+ # You can also update a field:
187
+ #
188
+ # myRecord["First Name"] = "Sophia"
189
+ #
190
+ # When you do, the change is noted, but *the data is not updated in FileMaker*. You must call
191
+ # Record::save or Record::save_if_not_modified to actually save the data.
192
+ def []=(pname, value)
193
+ return super unless @loaded # keeps us from getting mods during initialization
194
+ name = pname
195
+ if self[name] != nil
196
+ @mods[name] = val
197
+ else
198
+ raise Rfm::Error::ParameterError.new("You attempted to modify a field called '#{name}' on the Rfm::Record object, but that field does not exist.")
199
+ end
200
+ end
201
+
202
+ def method_missing (symbol, *attrs)
203
+ # check for simple getter
204
+ return self[symbol.to_s] if self.include?(symbol.to_s)
205
+
206
+ # check for setter
207
+ symbol_name = symbol.to_s
208
+ if symbol_name[-1..-1] == '=' && self.has_key?(symbol_name[0..-2])
209
+ return @mods[symbol_name[0..-2]] = attrs[0]
210
+ end
211
+ super
212
+ end
213
+
214
+ def respond_to?(symbol, include_private = false)
215
+ return true if self[symbol.to_s] != nil
216
+ super
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,136 @@
1
+ # This module includes classes that represent FileMaker data. When you communicate with FileMaker
2
+ # using, ie, the Layout object, you typically get back ResultSet objects. These contain Records,
3
+ # which in turn contain Fields, Portals, and arrays of data.
4
+ #
5
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
6
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
7
+ # License:: See MIT-LICENSE for details
8
+ require 'nokogiri'
9
+ require 'bigdecimal'
10
+ require 'rfm/record'
11
+ require 'rfm/metadata/field'
12
+
13
+ module Rfm
14
+
15
+ # The ResultSet object represents a set of records in FileMaker. It is, in every way, a real Ruby
16
+ # Array, so everything you expect to be able to do with an Array can be done with a ResultSet as well.
17
+ # In this case, the elements in the array are Record objects.
18
+ #
19
+ # Here's a typical example, displaying the results of a Find:
20
+ #
21
+ # myServer = Rfm::Server.new(...)
22
+ # results = myServer["Customers"]["Details"].find("First Name" => "Bill")
23
+ # results.each {|record|
24
+ # puts record["First Name"]
25
+ # puts record["Last Name"]
26
+ # puts record["Email Address"]
27
+ # }
28
+ #
29
+ # =Attributes
30
+ #
31
+ # The ResultSet object has these attributes:
32
+ #
33
+ # * *field_meta* is a hash with field names for keys and Field objects for values; it provides
34
+ # info about the fields in the ResultSet
35
+ #
36
+ # * *portal_meta* is a hash with table occurrence names for keys and arrays of Field objects for values;
37
+ # it provides metadata about the portals in the ResultSet and the Fields on those portals
38
+
39
+ class Resultset < Array
40
+
41
+ attr_reader :layout
42
+ attr_reader :field_meta, :portal_meta
43
+ attr_reader :date_format, :time_format, :timestamp_format
44
+ attr_reader :total_count, :foundset_count
45
+
46
+ # Initializes a new ResultSet object. You will probably never do this your self (instead, use the Layout
47
+ # object to get various ResultSet obejects).
48
+ #
49
+ # If you feel so inclined, though, pass a Server object, and some +fmpxmlresult+ compliant XML in a String.
50
+ #
51
+ # =Attributes
52
+ #
53
+ # The ResultSet object includes several useful attributes:
54
+ #
55
+ # * *fields* is a hash (with field names for keys and Field objects for values). It includes an entry for
56
+ # every field in the ResultSet. Note: You don't use Field objects to access _data_. If you're after
57
+ # data, get a Record object (ResultSet is an array of records). Field objects tell you about the fields
58
+ # (their type, repetitions, and so forth) in case you find that information useful programmatically.
59
+ #
60
+ # Note: keys in the +fields+ hash are downcased for convenience (and [] automatically downcases on
61
+ # lookup, so it should be seamless). But if you +each+ a field hash and need to know a field's real
62
+ # name, with correct case, do +myField.name+ instead of relying on the key in the hash.
63
+ #
64
+ # * *portals* is a hash (with table occurrence names for keys and Field objects for values). If your
65
+ # layout contains portals, you can find out what fields they contain here. Again, if it's the data you're
66
+ # after, you want to look at the Record object.
67
+
68
+ def initialize(server, xml_response, layout, portals=nil)
69
+ @layout = layout
70
+ @field_meta ||= Rfm::CaseInsensitiveHash.new
71
+ @portal_meta ||= Rfm::CaseInsensitiveHash.new
72
+ @include_portals = portals
73
+
74
+ doc = Nokogiri.XML(remove_namespace(xml_response))
75
+
76
+ error = doc.xpath('/fmresultset/error').attribute('code').value.to_i
77
+ check_for_errors(error, server.state[:raise_on_401])
78
+
79
+ datasource = doc.xpath('/fmresultset/datasource')
80
+ meta = doc.xpath('/fmresultset/metadata')
81
+ resultset = doc.xpath('/fmresultset/resultset')
82
+
83
+ @date_format = convert_date_time_format(datasource.attribute('date-format').value)
84
+ @time_format = convert_date_time_format(datasource.attribute('time-format').value)
85
+ @timestamp_format = convert_date_time_format(datasource.attribute('timestamp-format').value)
86
+
87
+ @foundset_count = resultset.attribute('count').value.to_i
88
+ @total_count = datasource.attribute('total-count').value.to_i
89
+
90
+ parse_fields(meta)
91
+ parse_portals(meta) if @include_portals
92
+
93
+ Rfm::Record.build_records(resultset.xpath('record'), self, @field_meta, @layout)
94
+
95
+ end
96
+
97
+ private
98
+ def remove_namespace(xml)
99
+ xml.gsub('xmlns="http://www.filemaker.com/xml/fmresultset" version="1.0"', '')
100
+ end
101
+
102
+ def check_for_errors(code, raise_401)
103
+ raise Rfm::Error.getError(code) if code != 0 && (code != 401 || raise_401)
104
+ end
105
+
106
+ def parse_fields(meta)
107
+ meta.xpath('field-definition').each do |field|
108
+ @field_meta[field['name']] = Rfm::Metadata::Field.new(field)
109
+ end
110
+ end
111
+
112
+ def parse_portals(meta)
113
+ meta.xpath('relatedset-definition').each do |relatedset|
114
+ table, fields = relatedset.attribute('table').value, {}
115
+
116
+ relatedset.xpath('field-definition').each do |field|
117
+ name = field.attribute('name').value.gsub(Regexp.new(table + '::'), '')
118
+ fields[name] = Rfm::Metadata::Field.new(field)
119
+ end
120
+
121
+ @portal_meta[table] = fields
122
+ end
123
+ end
124
+
125
+ def convert_date_time_format(fm_format)
126
+ fm_format.gsub!('MM', '%m')
127
+ fm_format.gsub!('dd', '%d')
128
+ fm_format.gsub!('yyyy', '%Y')
129
+ fm_format.gsub!('HH', '%H')
130
+ fm_format.gsub!('mm', '%M')
131
+ fm_format.gsub!('ss', '%S')
132
+ fm_format
133
+ end
134
+
135
+ end
136
+ end
@@ -254,7 +254,7 @@ module Rfm
254
254
  # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
255
255
  # "Customers" database whose first name is "Bill" you might do this:
256
256
  #
257
- # response = myServer.do_action(
257
+ # response = myServer.connect(
258
258
  # '-find',
259
259
  # {
260
260
  # "-db" => "Customers",
@@ -263,7 +263,7 @@ module Rfm
263
263
  # },
264
264
  # { :max_records => 20 }
265
265
  # )
266
- def do_action(account_name, password, action, args, options = {})
266
+ def connect(account_name, password, action, args, options = {})
267
267
  post = args.merge(expand_options(options)).merge({action => ''})
268
268
  http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
269
269
  end
@@ -276,7 +276,7 @@ module Rfm
276
276
  private
277
277
 
278
278
  def http_fetch(host_name, port, path, account_name, password, post_data, limit=10)
279
- raise Rfm::Error::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.") if limit == 0
279
+ raise Rfm::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.") if limit == 0
280
280
 
281
281
  if @state[:log_actions] == true
282
282
  qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
@@ -300,7 +300,6 @@ module Rfm
300
300
  end
301
301
 
302
302
  response = response.start { |http| http.request(request) }
303
-
304
303
  if @state[:log_responses] == true
305
304
  response.to_hash.each { |key, value| warn "#{key}: #{value}" }
306
305
  warn response.body
@@ -318,13 +317,13 @@ module Rfm
318
317
  http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
319
318
  when Net::HTTPUnauthorized
320
319
  msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
321
- raise Rfm::Error::AuthenticationError.new(msg)
320
+ raise Rfm::AuthenticationError.new(msg)
322
321
  when Net::HTTPNotFound
323
322
  msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
324
- raise Rfm::Error::CommunicationError.new(msg)
323
+ raise Rfm::CommunicationError.new(msg)
325
324
  else
326
- msg = "Unexpected response from server: #{result.code} (#{result.class.to_s}). Unable to communicate with the Web Publishing Engine."
327
- raise Rfm::Error::CommunicationError.new(msg)
325
+ msg = "Unexpected response from server: #{response.code} (#{response.class.to_s}). Unable to communicate with the Web Publishing Engine."
326
+ raise Rfm::CommunicationError.new(msg)
328
327
  end
329
328
  end
330
329
 
@@ -338,14 +337,14 @@ module Rfm
338
337
  result['-skip'] = value
339
338
  when :sort_field
340
339
  if value.kind_of? Array
341
- raise Rfm::Error::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
340
+ raise Rfm::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
342
341
  value.each_index { |i| result["-sortfield.#{i+1}"] = value[i] }
343
342
  else
344
343
  result["-sortfield.1"] = value
345
344
  end
346
345
  when :sort_order
347
346
  if value.kind_of? Array
348
- raise Rfm::Error::ParameterError.new(":sort_order can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
347
+ raise Rfm::ParameterError.new(":sort_order can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
349
348
  value.each_index { |i| result["-sortorder.#{i+1}"] = value[i] }
350
349
  else
351
350
  result["-sortorder.1"] = value
@@ -378,7 +377,7 @@ module Rfm
378
377
  when :modification_id
379
378
  result['-modid'] = value
380
379
  else
381
- raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
380
+ raise Rfm::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
382
381
  end
383
382
  end
384
383
  return result
@@ -0,0 +1,10 @@
1
+ module Rfm
2
+ class CaseInsensitiveHash < Hash
3
+ def []=(key, value)
4
+ super(key.downcase, value)
5
+ end
6
+ def [](key)
7
+ super(key.downcase)
8
+ end
9
+ end
10
+ end
@@ -7,7 +7,7 @@
7
7
 
8
8
  module Rfm
9
9
  module Factory # :nodoc: all
10
- class DbFactory < Rfm::Utility::CaseInsensitiveHash
10
+ class DbFactory < Rfm::CaseInsensitiveHash
11
11
 
12
12
  def initialize(server)
13
13
  @server = server
@@ -20,7 +20,7 @@ module Rfm
20
20
 
21
21
  def all
22
22
  if !@loaded
23
- Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-dbnames', {}).body).each {|record|
23
+ Rfm::Result::ResultSet.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-dbnames', {}).body).each {|record|
24
24
  name = record['DATABASE_NAME']
25
25
  self[name] = Rfm::Database.new(name, @server) if self[name] == nil
26
26
  }
@@ -31,7 +31,7 @@ module Rfm
31
31
 
32
32
  end
33
33
 
34
- class LayoutFactory < Rfm::Utility::CaseInsensitiveHash
34
+ class LayoutFactory < Rfm::CaseInsensitiveHash
35
35
 
36
36
  def initialize(server, database)
37
37
  @server = server
@@ -45,7 +45,7 @@ module Rfm
45
45
 
46
46
  def all
47
47
  if !@loaded
48
- Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-layoutnames', {"-db" => @database.name}).body).each {|record|
48
+ Rfm::Result::ResultSet.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-layoutnames', {"-db" => @database.name}).body).each {|record|
49
49
  name = record['LAYOUT_NAME']
50
50
  self[name] = Rfm::Layout.new(name, @database) if self[name] == nil
51
51
  }
@@ -56,7 +56,7 @@ module Rfm
56
56
 
57
57
  end
58
58
 
59
- class ScriptFactory < Rfm::Utility::CaseInsensitiveHash
59
+ class ScriptFactory < Rfm::CaseInsensitiveHash
60
60
 
61
61
  def initialize(server, database)
62
62
  @server = server
@@ -70,7 +70,7 @@ module Rfm
70
70
 
71
71
  def all
72
72
  if !@loaded
73
- Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-scriptnames', {"-db" => @database.name}).body).each {|record|
73
+ Rfm::Result::ResultSet.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-scriptnames', {"-db" => @database.name}).body).each {|record|
74
74
  name = record['SCRIPT_NAME']
75
75
  self[name] = Rfm::Script.new(name, @database) if self[name] == nil
76
76
  }