lardawge-rfm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,713 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'cgi'
4
+
5
+ # This module includes classes that represent base FileMaker concepts like servers,
6
+ # layouts, and scripts. These classes allow you to communicate with FileMaker Server,
7
+ # send commands, and receive responses.
8
+ #
9
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
10
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
11
+ # License:: See MIT-LICENSE for details
12
+ module Rfm
13
+
14
+ # This class represents a single FileMaker server. It is initialized with basic
15
+ # connection information, including the hostname, port number, and default database
16
+ # account name and password.
17
+ #
18
+ # Note: The host and port number refer to the FileMaker Web Publishing Engine, which
19
+ # must be installed and configured in order to use RFM. It may not actually be running
20
+ # on the same server computer as FileMaker Server itself. See your FileMaker Server
21
+ # or FileMaker Server Advanced documentation for information about configuring a Web
22
+ # Publishing Engine.
23
+ #
24
+ # =Accessing Databases
25
+ #
26
+ # Typically, you access a Database object from the Server like this:
27
+ #
28
+ # myDatabase = myServer["Customers"]
29
+ #
30
+ # This code gets the Database object representing the Customers object.
31
+ #
32
+ # Note: RFM does not talk to the server when you retrieve a database object in this way. Instead, it
33
+ # simply assumes you know what you're talking about. If the database you specify does not exist, you
34
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object you get
35
+ # from this database. This makes debugging a little less convenient, but it would introduce too much
36
+ # overhead to hit the server at this point.
37
+ #
38
+ # The Server object has a +db+ attribute that provides alternate access to Database objects. It acts
39
+ # like a hash of Database objects, one for each accessible database on the server. So, for example, you
40
+ # can do this if you want to print out a list of all databses on the server:
41
+ #
42
+ # myServer.db.each {|database|
43
+ # puts database.name
44
+ # }
45
+ #
46
+ # The Server::db attribute is actually a DbFactory object, although it subclasses hash, so it should work
47
+ # in all the ways you expect. Note, though, that it is completely empty until the first time you attempt
48
+ # to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of databases, and
49
+ # constructs a Database object for each one. In other words, it incurrs no overhead until you use it.
50
+ #
51
+ # =Attributes
52
+ #
53
+ # In addition to the +db+ attribute, Server has a few other useful attributes:
54
+ #
55
+ # * *host_name* is the host name this server points to
56
+ # * *post* is the port number this server communicates on
57
+ # * *state* is a hash of all server options used to initialize this server
58
+
59
+ class Server
60
+
61
+ # To create a Server obejct, you typically need at least a host name:
62
+ #
63
+ # myServer = Rfm::Server.new({:host => 'my.host.com'})
64
+ #
65
+ # Several other options are supported:
66
+ #
67
+ # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
68
+ #
69
+ # * *port* the port number the WPE is listening no (defaults to 80)
70
+ #
71
+ # * *ssl* +true+ if you want to use SSL (HTTPS) to connect to FileMaker (defaults to +false+)
72
+ #
73
+ # * *account_name* the default account name to log in to databases with (you can also supply a
74
+ # account name on a per-database basis if necessary)
75
+ #
76
+ # * *password* the default password to log in to databases with (you can also supplly a password
77
+ # on a per-databases basis if necessary)
78
+ #
79
+ # * *log_actions* when +true+, RFM logs all action URLs that are sent to FileMaker server to stderr
80
+ # (defaults to +false+)
81
+ #
82
+ # * *log_responses* when +true+, RFM logs all raw XML responses (including headers) from FileMaker to
83
+ # stderr (defaults to +false+)
84
+ #
85
+ # * *warn_on_redirect* normally, RFM prints a warning to stderr if the Web Publishing Engine redirects
86
+ # (this can usually be fixed by using a different host name, which speeds things up); if you *don't*
87
+ # want this warning printed, set +warn_on_redirect+ to +true+
88
+ #
89
+ # * *raise_on_401* although RFM raises error when FileMaker returns error responses, it typically
90
+ # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
91
+ # prefer a raised error when a find produces no errors, set this option to +true+
92
+ def initialize(options)
93
+ @state = {
94
+ :host => 'localhost',
95
+ :port => 80,
96
+ :ssl => false,
97
+ :account_name => '',
98
+ :password => '',
99
+ :log_actions => false,
100
+ :log_responses => false,
101
+ :warn_on_redirect => true,
102
+ :raise_on_401 => false
103
+ }.merge(options)
104
+
105
+ if @state[:username] != nil
106
+ warn("the :username option on Rfm::Server::initialize has been deprecated. Use :account_name instead.")
107
+ @state[:account_name] = @state[:username]
108
+ end
109
+
110
+ @state.freeze
111
+
112
+ @host_name = @state[:host]
113
+ @scheme = @state[:ssl] ? "https" : "http"
114
+ @port = @state[:port]
115
+
116
+ @db = Rfm::Factory::DbFactory.new(self)
117
+ end
118
+
119
+ # Access the database object representing a database on the server. For example:
120
+ #
121
+ # myServer['Customers']
122
+ #
123
+ # would return a Database object representing the _Customers_
124
+ # database on the server.
125
+ #
126
+ # Note: RFM never talks to the server until you perform an action. The database object
127
+ # returned is created on the fly and assumed to refer to a valid database, but you will
128
+ # get no error at this point if the database you access doesn't exist. Instead, you'll
129
+ # receive an error when you actually try to perform some action on a layout from this
130
+ # database.
131
+ def [](dbname)
132
+ self.db[dbname]
133
+ end
134
+
135
+ attr_reader :db, :host_name, :port, :scheme, :state
136
+
137
+ # Performs a raw FileMaker action. You will generally not call this method directly, but it
138
+ # is exposed in case you need to do something "under the hood."
139
+ #
140
+ # The +action+ parameter is any valid FileMaker web url action. For example, +-find+, +-finadny+ etc.
141
+ #
142
+ # The +args+ parameter is a hash of arguments to be included in the action url. It will be serialized
143
+ # and url-encoded appropriately.
144
+ #
145
+ # The +options+ parameter is a hash of RFM-specific options, which correspond to the more esoteric
146
+ # FileMaker URL parameters. They are exposed separately because they can also be passed into
147
+ # various methods on the Layout object, which is a much more typical way of sending an action to
148
+ # FileMaker.
149
+ #
150
+ # This method returns the Net::HTTP response object representing the response from FileMaker.
151
+ #
152
+ # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
153
+ # "Customers" database whose first name is "Bill" you might do this:
154
+ #
155
+ # response = myServer.do_action(
156
+ # '-find',
157
+ # {
158
+ # "-db" => "Customers",
159
+ # "-lay" => "Details",
160
+ # "First Name" => "Bill"
161
+ # },
162
+ # { :max_records => 20 }
163
+ # )
164
+ def do_action(account_name, password, action, args, options = {})
165
+ post = args.merge(expand_options(options)).merge({action => ''})
166
+ http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
167
+ end
168
+
169
+ def load_layout(layout)
170
+ post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
171
+ http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
172
+ end
173
+
174
+ private
175
+
176
+ def http_fetch(host_name, port, path, account_name, password, post_data, limit = 10)
177
+ if limit == 0
178
+ raise Rfm::Error::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.")
179
+ end
180
+
181
+ if @state[:log_actions] == true
182
+ qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
183
+ warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
184
+ end
185
+
186
+ request = Net::HTTP::Post.new(path)
187
+ request.basic_auth(account_name, password)
188
+ request.set_form_data(post_data)
189
+
190
+ response = Net::HTTP.start(host_name, port) { |http|
191
+ http.request(request)
192
+ }
193
+
194
+ if @state[:log_responses] == true
195
+ response.to_hash.each {|key, value|
196
+ warn "#{key}: #{value}"
197
+ }
198
+ warn response.body
199
+ end
200
+
201
+ case response
202
+ when Net::HTTPSuccess
203
+ response
204
+ when Net::HTTPRedirection
205
+ if @state[:warn_on_redirect]
206
+ warn "The web server redirected to " + response['location'] + ". You should revise your connection hostname or fix your server configuration if possible to improve performance."
207
+ end
208
+ newloc = URI.parse(response['location'])
209
+ http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
210
+ when Net::HTTPUnauthorized
211
+ msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
212
+ raise Rfm::Error::AuthenticationError.new(msg)
213
+ when Net::HTTPNotFound
214
+ msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
215
+ raise Rfm::Error::CommunicationError.new(msg)
216
+ else
217
+ msg = "Unexpected response from server: #{result.code} (#{result.class.to_s}). Unable to communicate with the Web Publishing Engine."
218
+ raise Rfm::Error::CommunicationError.new(msg)
219
+ end
220
+ end
221
+
222
+ def expand_options(options)
223
+ result = {}
224
+ options.each {|key,value|
225
+ case key
226
+ when :max_records:
227
+ result['-max'] = value
228
+ when :skip_records:
229
+ result['-skip'] = value
230
+ when :sort_field:
231
+ if value.kind_of? Array
232
+ if value.size > 9
233
+ raise Rfm::Error::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.")
234
+ end
235
+ value.each_index {|i|
236
+ result["-sortfield.#{i+1}"] = value[i]
237
+ }
238
+ else
239
+ result["-sortfield.1"] = value
240
+ end
241
+ when :sort_order:
242
+ result['-sortorder'] = value
243
+ when :post_script:
244
+ if value.class == Array
245
+ result['-script'] = value[0]
246
+ result['-script.param'] = value[1]
247
+ else
248
+ result['-script'] = value
249
+ end
250
+ when :pre_find_script:
251
+ if value.class == Array
252
+ result['-script.prefind'] = value[0]
253
+ result['-script.prefind.param'] = value[1]
254
+ else
255
+ result['-script.presort'] = value
256
+ end
257
+ when :pre_sort_script:
258
+ if value.class == Array
259
+ result['-script.presort'] = value[0]
260
+ result['-script.presort.param'] = value[1]
261
+ else
262
+ result['-script.presort'] = value
263
+ end
264
+ when :response_layout:
265
+ result['-lay.response'] = value
266
+ when :logical_operator:
267
+ result['-lop'] = value
268
+ when :modification_id:
269
+ result['-modid'] = value
270
+ else
271
+ raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
272
+ end
273
+ }
274
+ result
275
+ end
276
+
277
+ end
278
+
279
+ # The Database object represents a single FileMaker Pro database. When you retrieve a Database
280
+ # object from a server, its account name and password are set to the account name and password you
281
+ # used when initializing the Server object. You can override this of course:
282
+ #
283
+ # myDatabase = myServer["Customers"]
284
+ # myDatabase.account_name = "foo"
285
+ # myDatabase.password = "bar"
286
+ #
287
+ # =Accessing Layouts
288
+ #
289
+ # All interaction with FileMaker happens through a Layout object. You can get a Layout object
290
+ # from the Database object like this:
291
+ #
292
+ # myLayout = myDatabase["Details"]
293
+ #
294
+ # This code gets the Layout object representing the layout called Details in the database.
295
+ #
296
+ # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
297
+ # simply assumes you know what you're talking about. If the layout you specify does not exist, you
298
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
299
+ # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
300
+ # overhead to hit the server at this point.
301
+ #
302
+ # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
303
+ # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
304
+ # can do this if you want to print out a list of all layouts:
305
+ #
306
+ # myDatabase.layout.each {|layout|
307
+ # puts layout.name
308
+ # }
309
+ #
310
+ # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
311
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
312
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
313
+ # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
314
+ #
315
+ # =Accessing Scripts
316
+ #
317
+ # If for some reason you need to enumerate the scripts in a database, you can do so:
318
+ #
319
+ # myDatabase.script.each {|script|
320
+ # puts script.name
321
+ # }
322
+ #
323
+ # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
324
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
325
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
326
+ # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
327
+ #
328
+ # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
329
+ #
330
+ # =Attributes
331
+ #
332
+ # In addition to the +layout+ attribute, Server has a few other useful attributes:
333
+ #
334
+ # * *server* is the Server object this database comes from
335
+ # * *name* is the name of this database
336
+ # * *state* is a hash of all server options used to initialize this server
337
+ class Database
338
+
339
+ # Initialize a database object. You never really need to do this. Instead, just do this:
340
+ #
341
+ # myServer = Rfm::Server.new(...)
342
+ # myDatabase = myServer["Customers"]
343
+ #
344
+ # This sample code gets a database object representing the Customers database on the FileMaker server.
345
+ def initialize(name, server)
346
+ @name = name
347
+ @server = server
348
+ @account_name = server.state[:account_name] or ""
349
+ @password = server.state[:password] or ""
350
+ @layout = Rfm::Factory::LayoutFactory.new(server, self)
351
+ @script = Rfm::Factory::ScriptFactory.new(server, self)
352
+ end
353
+
354
+ attr_reader :server, :name, :account_name, :password, :layout, :script
355
+ attr_writer :account_name, :password
356
+
357
+ # Access the Layout object representing a layout in this database. For example:
358
+ #
359
+ # myDatabase['Details']
360
+ #
361
+ # would return a Layout object representing the _Details_
362
+ # layout in the database.
363
+ #
364
+ # Note: RFM never talks to the server until you perform an action. The Layout object
365
+ # returned is created on the fly and assumed to refer to a valid layout, but you will
366
+ # get no error at this point if the layout you specify doesn't exist. Instead, you'll
367
+ # receive an error when you actually try to perform some action it.
368
+ def [](layout_name)
369
+ self.layout[layout_name]
370
+ end
371
+
372
+ end
373
+
374
+ # The Layout object represents a single FileMaker Pro layout. You use it to interact with
375
+ # records in FileMaker. *All* access to FileMaker data is done through a layout, and this
376
+ # layout determins which _table_ you actually hit (since every layout is explicitly associated
377
+ # with a particular table -- see FileMakers Layout->Layout Setup dialog box). You never specify
378
+ # _table_ information directly in RFM.
379
+ #
380
+ # Also, the layout determines which _fields_ will be returned. If a layout contains only three
381
+ # fields from a large table, only those three fields are returned. If a layout includes related
382
+ # fields from another table, they are returned as well. And if the layout includes portals, all
383
+ # data in the portals is returned (see Record::portal for details).
384
+ #
385
+ # As such, you can _significantly_ improve performance by limiting what you put on the layout.
386
+ #
387
+ # =Using Layouts
388
+ #
389
+ # The Layout object is where you get most of your work done. It includes methods for all
390
+ # FileMaker actions:
391
+ #
392
+ # * Layout::all
393
+ # * Layout::any
394
+ # * Layout::find
395
+ # * Layout::edit
396
+ # * Layout::create
397
+ # * Layout::delete
398
+ #
399
+ # =Running Scripts
400
+ #
401
+ # In FileMaker, execution of a script must accompany another action. For example, to run a script
402
+ # called _Remove Duplicates_ with a found set that includes everybody
403
+ # named _Bill_, do this:
404
+ #
405
+ # myLayout.find({"First Name" => "Bill"}, :post_script => "Remove Duplicates")
406
+ #
407
+ # ==Controlling When the Script Runs
408
+ #
409
+ # When you perform an action in FileMaker, it always executes in this order:
410
+ #
411
+ # 1. Perform any find
412
+ # 2. Sort the records
413
+ # 3. Return the results
414
+ #
415
+ # You can control when in the process the script runs. Each of these options is available:
416
+ #
417
+ # * *post_script* tells FileMaker to run the script after finding and sorting
418
+ # * *pre_find_script* tells FileMaker to run the script _before_ finding
419
+ # * *pre_sort_script* tells FileMaker to run the script _before_ sorting, but _after_ finding
420
+ #
421
+ # ==Passing Parameters to a Script
422
+ #
423
+ # If you want to pass a parameter to the script, use the options above, but supply an array value
424
+ # instead of a single string. For example:
425
+ #
426
+ # myLayout.find({"First Name" => "Bill"}, :post_script => ["Remove Duplicates", 10])
427
+ #
428
+ # This sample runs the script called "Remove Duplicates" and passes it the value +10+ as its
429
+ # script parameter.
430
+ #
431
+ # =Common Options
432
+ #
433
+ # Most of the methods on the Layout object accept an optional hash of +options+ to manipulate the
434
+ # action. For example, when you perform a find, you will typiclaly get back _all_ matching records.
435
+ # If you want to limit the number of records returned, you can do this:
436
+ #
437
+ # myLayout.find({"First Name" => "Bill"}, :max_records => 100)
438
+ #
439
+ # The +:max_records+ option tells FileMaker to limit the number of records returned.
440
+ #
441
+ # This is the complete list of available options:
442
+ #
443
+ # * *max_records* tells FileMaker how many records to return
444
+ #
445
+ # * *skip_records* tells FileMaker how many records in the found set to skip, before
446
+ # returning results; this is typically combined with +max_records+ to "page" through
447
+ # records
448
+ #
449
+ # * *sort_field* tells FileMaker to sort the records by the specified field
450
+ #
451
+ # * *sort_order* can be +desc+ (descending) or +asc+ (ascending) and determines the order
452
+ # of the sort when +sort_field+ is specified
453
+ #
454
+ # * *post_script* tells FileMaker to perform a script after carrying out the action; you
455
+ # can pass the script name, or a two-element array, with the script name first, then the
456
+ # script parameter
457
+ #
458
+ # * *pre_find_script* is like +post_script+ except the script runs before any find is
459
+ # performed
460
+ #
461
+ # * *pre_sort_script* is like +pre_find_script+ except the script runs after any find
462
+ # and before any sort
463
+ #
464
+ # * *response_layout* tells FileMaker to switch layouts before producing the response; this
465
+ # is useful when you need a field on a layout to perform a find, edit, or create, but you
466
+ # want to improve performance by not including the field in the result
467
+ #
468
+ # * *logical_operator* can be +and+ or +or+ and tells FileMaker how to process multiple fields
469
+ # in a find request
470
+ #
471
+ # * *modification_id* lets you pass in the modification id from a Record object with the request;
472
+ # when you do, the action will fail if the record was modified in FileMaker after it was retrieved
473
+ # by RFM but before the action was run
474
+ #
475
+ #
476
+ # =Attributes
477
+ #
478
+ # The Layout object has a few useful attributes:
479
+ #
480
+ # * +name+ is the name of the layout
481
+ #
482
+ # * +field_controls+ is a hash of FieldControl objects, with the field names as keys. FieldControl's
483
+ # tell you about the field on the layout: how is it formatted and what value list is assigned
484
+ #
485
+ # Note: It is possible to put the same field on a layout more than once. When this is the case, the
486
+ # value in +field_controls+ for that field is an array with one element representing each instance
487
+ # of the field.
488
+ #
489
+ # * +value_lists+ is a hash of arrays. The keys are value list names, and the values in the hash
490
+ # are arrays containing the actual value list items. +value_lists+ will include every value
491
+ # list that is attached to any field on the layout
492
+
493
+ class Layout
494
+
495
+ # Initialize a layout object. You never really need to do this. Instead, just do this:
496
+ #
497
+ # myServer = Rfm::Server.new(...)
498
+ # myDatabase = myServer["Customers"]
499
+ # myLayout = myDatabase["Details"]
500
+ #
501
+ # This sample code gets a layout object representing the Details layout in the Customers database
502
+ # on the FileMaker server.
503
+ #
504
+ # In case it isn't obvious, this is more easily expressed this way:
505
+ #
506
+ # myServer = Rfm::Server.new(...)
507
+ # myLayout = myServer["Customers"]["Details"]
508
+ def initialize(name, db)
509
+ @name = name
510
+ @db = db
511
+
512
+ @loaded = false
513
+ @field_controls = Rfm::Util::CaseInsensitiveHash.new
514
+ @value_lists = Rfm::Util::CaseInsensitiveHash.new
515
+ end
516
+
517
+ attr_reader :name, :db
518
+
519
+ def field_controls
520
+ load if !@loaded
521
+ @field_controls
522
+ end
523
+
524
+ def value_lists
525
+ load if !@loaded
526
+ @value_lists
527
+ end
528
+
529
+ # Returns a ResultSet object containing _every record_ in the table associated with this layout.
530
+ def all(options = {})
531
+ get_records('-findall', {}, options)
532
+ end
533
+
534
+ # Returns a ResultSet containing a single random record from the table associated with this layout.
535
+ def any(options = {})
536
+ get_records('-findany', {}, options)
537
+ end
538
+
539
+ # Finds a record. Typically you will pass in a hash of field names and values. For example:
540
+ #
541
+ # myLayout.find({"First Name" => "Bill"})
542
+ #
543
+ # Values in the hash work just like value in FileMaker's Find mode. You can use any special
544
+ # symbols (+==+, +...+, +>+, etc...).
545
+ #
546
+ # If you pass anything other than a hash as the first parameter, it is converted to a string and
547
+ # assumed to be FileMaker's internal id for a record (the recid).
548
+ def find(hash_or_recid, options = {})
549
+ if hash_or_recid.kind_of? Hash
550
+ get_records('-find', hash_or_recid, options)
551
+ else
552
+ get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
553
+ end
554
+ end
555
+
556
+ # Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
557
+ # data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
558
+ #
559
+ # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
560
+ # myLayout.edit(recid, {"First Name" => "Steve"})
561
+ #
562
+ # The above code would find the first record with _Bill_ in the First Name field and change the
563
+ # first name to _Steve_.
564
+ def edit(recid, values, options = {})
565
+ get_records('-edit', {'-recid' => recid}.merge(values), options)
566
+ end
567
+
568
+ # Creates a new record in the table associated with this layout. Pass field data as a hash in the
569
+ # +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned
570
+ # record to, ie, discover the values in auto-enter fields (like serial numbers).
571
+ #
572
+ # For example:
573
+ #
574
+ # result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"})
575
+ # id = result[0]["ID"]
576
+ #
577
+ # The above code adds a new record with first name _Jerry_ and last name _Robin_. It then
578
+ # puts the value from the ID field (a serial number) into a ruby variable called +id+.
579
+ def create(values, options = {})
580
+ get_records('-new', values, options)
581
+ end
582
+
583
+ # Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
584
+ #
585
+ # For example:
586
+ #
587
+ # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
588
+ # myLayout.delete(recid)
589
+ #
590
+ # The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
591
+ def delete(recid, options = {})
592
+ get_records('-delete', {'-recid' => recid}, options)
593
+ return nil
594
+ end
595
+
596
+ private
597
+
598
+ def load
599
+ @loaded = true
600
+ fmpxmllayout = @db.server.load_layout(self).body
601
+ doc = REXML::Document.new(fmpxmllayout)
602
+ root = doc.root
603
+
604
+ # check for errors
605
+ error = root.elements['ERRORCODE'].text.to_i
606
+ raise Rfm::Error::FileMakerError.getError(error) if error != 0
607
+
608
+ # process valuelists
609
+ if root.elements['VALUELISTS'].size > 0
610
+ root.elements['VALUELISTS'].each_element('VALUELIST') { |valuelist|
611
+ name = valuelist.attributes['NAME']
612
+ @value_lists[name] = valuelist.elements.collect {|e| e.text}
613
+ }
614
+ @value_lists.freeze
615
+ end
616
+
617
+ # process field controls
618
+ root.elements['LAYOUT'].each_element('FIELD') { |field|
619
+ name = field.attributes['NAME']
620
+ style = field.elements['STYLE'].attributes['TYPE']
621
+ value_list_name = field.elements['STYLE'].attributes['VALUELIST']
622
+ value_list = @value_lists[value_list_name] if value_list_name != ''
623
+ field_control = FieldControl.new(name, style, value_list_name, value_list)
624
+ existing = @field_controls[name]
625
+ if existing
626
+ if existing.kind_of?(Array)
627
+ existing << field_control
628
+ else
629
+ @field_controls[name] = Array[existing, field_control]
630
+ end
631
+ else
632
+ @field_controls[name] = field_control
633
+ end
634
+ }
635
+ @field_controls.freeze
636
+ end
637
+
638
+ def get_records(action, extra_params = {}, options = {})
639
+ Rfm::Result::ResultSet.new(
640
+ @db.server, @db.server.do_action(@db.account_name, @db.password, action, params().merge(extra_params), options).body,
641
+ self)
642
+ end
643
+
644
+ def params
645
+ {"-db" => @db.name, "-lay" => self.name}
646
+ end
647
+ end
648
+
649
+ # The FieldControl object represents a field on a FileMaker layout. You can find out what field
650
+ # style the field uses, and the value list attached to it.
651
+ #
652
+ # =Attributes
653
+ #
654
+ # * *name* is the name of the field
655
+ #
656
+ # * *style* is any one of:
657
+ # * * :edit_box - a normal editable field
658
+ # * * :scrollable - an editable field with scroll bar
659
+ # * * :popup_menu - a pop-up menu
660
+ # * * :checkbox_set - a set of checkboxes
661
+ # * * :radio_button_set - a set of radio buttons
662
+ # * * :popup_list - a pop-up list
663
+ # * * :calendar - a pop-up calendar
664
+ #
665
+ # * *value_list_name* is the name of the attached value list, if any
666
+ #
667
+ # * *value_list* is an array of strings representing the value list items, or nil
668
+ # if this field has no attached value list
669
+ class FieldControl
670
+ def initialize(name, style, value_list_name, value_list)
671
+ @name = name
672
+ case style
673
+ when "EDITTEXT"
674
+ @style = :edit_box
675
+ when "POPUPMENU"
676
+ @style = :popup_menu
677
+ when "CHECKBOX"
678
+ @style = :checkbox_set
679
+ when "RADIOBUTTONS"
680
+ @style = :radio_button_set
681
+ when "POPUPLIST"
682
+ @style = :popup_list
683
+ when "CALENDAR"
684
+ @style = :calendar
685
+ when "SCROLLTEXT"
686
+ @style = :scrollable
687
+ end
688
+ @value_list_name = value_list_name
689
+ @value_list = value_list
690
+ end
691
+
692
+ attr_reader :name, :style, :value_list_name, :value_list
693
+
694
+ end
695
+
696
+ # The Script object represents a FileMaker script. At this point, the Script object exists only so
697
+ # you can enumrate all scripts in a Database (which is a rare need):
698
+ #
699
+ # myDatabase.script.each {|script|
700
+ # puts script.name
701
+ # }
702
+ #
703
+ # If you want to _run_ a script, see the Layout object instead.
704
+ class Script
705
+ def initialize(name, db)
706
+ @name = name
707
+ @db = db
708
+ end
709
+
710
+ attr_reader :name
711
+ end
712
+
713
+ end