rfm 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/rfm.rb +228 -1
- data/lib/rfm_command.rb +562 -29
- data/lib/rfm_error.rb +241 -14
- data/lib/rfm_factory.rb +14 -7
- data/lib/rfm_result.rb +268 -11
- data/lib/rfm_util.rb +10 -0
- data/tests/rfm_test_errors.rb +53 -0
- data/tests/rfm_tester.rb +2 -0
- metadata +6 -4
data/lib/rfm.rb
CHANGED
@@ -1,5 +1,232 @@
|
|
1
|
+
# RFM provides easy access to FileMaker Pro data. With it, Ruby scripts can
|
2
|
+
# perform finds, read records and fields, update data, and perform scripts using
|
3
|
+
# a simple ruby-like syntax.
|
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
|
+
#
|
9
|
+
# RFM uses the FileMaker XML API, so it requires:
|
10
|
+
# - FileMaker Server 9.0 or later
|
11
|
+
# - or FileMaker Server Advanced 7.0 or later
|
12
|
+
#
|
13
|
+
# This documentation serves as a reference to the classes in the API. For more complete
|
14
|
+
# usage documentation, see the RFM home page at http://sixfriedrice.com/wp/products/rfm/
|
15
|
+
#
|
16
|
+
# = Quick Start
|
17
|
+
#
|
18
|
+
# Rfm is a Gem. As such, any ruby file that uses it, needs to have these two lines on top:
|
19
|
+
#
|
20
|
+
# require "rubygems"
|
21
|
+
# require "rfm"
|
22
|
+
#
|
23
|
+
# (If you don't have Rfm installed, use the +gem install rfm+ command to get it.)
|
24
|
+
#
|
25
|
+
# === Get a Server
|
26
|
+
#
|
27
|
+
# Everything in Rfm starts with the Server object. You create a Server object like this:
|
28
|
+
#
|
29
|
+
# myServer = Rfm::Server.new(
|
30
|
+
# :host => "yourhost",
|
31
|
+
# :account_name => "someone",
|
32
|
+
# :pasword => "secret"
|
33
|
+
# )
|
34
|
+
#
|
35
|
+
# The Server object supports many other options, which you'll find explained in its
|
36
|
+
# documentation.
|
37
|
+
#
|
38
|
+
# Note: The account name and password are optional. You can instead provide them on
|
39
|
+
# a per-database basis (using Database::account_name and Database::password). But
|
40
|
+
# it is convenient to do it here because you often have one set of credentials
|
41
|
+
# across all databases. Also, you must provide an account_name and password if you
|
42
|
+
# want to ask the server for a list of available databases.
|
43
|
+
#
|
44
|
+
# === Get a Database
|
45
|
+
#
|
46
|
+
# Once you have a Server object, you can use it to get a Database. For example, if your
|
47
|
+
# database is called "Customers", you get it like this:
|
48
|
+
#
|
49
|
+
# myDatabase = myServer["Customers"]
|
50
|
+
#
|
51
|
+
# If you need to supply account and password info specifically for this database
|
52
|
+
# (rather than doing it at the Server level), do this:
|
53
|
+
#
|
54
|
+
# myDatabase.account_name = "someone"
|
55
|
+
# myDatabase.password = "secret"
|
56
|
+
#
|
57
|
+
# *IMPORTANT NOTE:* The account name you use to access FileMaker must have the
|
58
|
+
# +fmxml+ extended privilege. In other words, edit its privilege set and turn on
|
59
|
+
# "Access via XML Web Publishing (fmxml)" in the Extended Privileges section
|
60
|
+
# at the bottom-left of the Edit Privilege Set window. If you don't do this,
|
61
|
+
# Rfm will report that it can't log in.
|
62
|
+
#
|
63
|
+
# === Get a Layout
|
64
|
+
#
|
65
|
+
# Every action you send to FileMaker always goes through a layout. This is how Rfm knows
|
66
|
+
# which table you want to work with, and which fields on that table you care about. This
|
67
|
+
# should feel pretty familiar now:
|
68
|
+
#
|
69
|
+
# myLayout = myDatabase["Details"]
|
70
|
+
#
|
71
|
+
# You might use layouts you already have, or make new layout just for Rfm. Just remember that
|
72
|
+
# if you delete a layout, or remove a field from a layout that your Rfm code uses, the
|
73
|
+
# code will stop working.
|
74
|
+
#
|
75
|
+
# === Putting it Together
|
76
|
+
#
|
77
|
+
# Usually you don't care much about the intermediate Database object (it's a gateway object,
|
78
|
+
# if you will). So it is often easiest to combine all the above steps like this:
|
79
|
+
#
|
80
|
+
# myLayout = myServer["Customers"]["Details"]
|
81
|
+
#
|
82
|
+
# === Performing Actions
|
83
|
+
#
|
84
|
+
# The Layout object can do a lot of things (see its documentation for a full list). But
|
85
|
+
# in general, it involves records. For instance, you can find records:
|
86
|
+
#
|
87
|
+
# result = myLayout.find({"First Name" => "Bill"})
|
88
|
+
#
|
89
|
+
# That code finds everybody whose first name in Bill. All the Layout methods return an
|
90
|
+
# ResultSet object. It contains the records, as well as metadata about the fields and
|
91
|
+
# portals on the layout. Usually you'll only concern yourself with the records (and you
|
92
|
+
# can read about the others in the ResultSet documentation).
|
93
|
+
#
|
94
|
+
# ResultSet is a subclass of Array, Ruby's built in array type. So you can treate it just
|
95
|
+
# like any other array:
|
96
|
+
#
|
97
|
+
# first_record = result[0]
|
98
|
+
# a_few_records = result[3,7]
|
99
|
+
# record_count = result.size
|
100
|
+
#
|
101
|
+
# But usually you'll want to loop through them all. Because this is an array, you can use
|
102
|
+
# code that is familiar to any Ruby whiz:
|
103
|
+
#
|
104
|
+
# result.each { |record|
|
105
|
+
# # do something with record here
|
106
|
+
# }
|
107
|
+
#
|
108
|
+
# === Working with Records
|
109
|
+
#
|
110
|
+
# The records in a ResultSet are actually Record objects. They hold the actual data from
|
111
|
+
# FileMaker. Record subclasses Hash, another built in Ruby type, so you can use them like
|
112
|
+
# this:
|
113
|
+
#
|
114
|
+
# full_name = record["First Name"] + ' ' + record["Last Name"]
|
115
|
+
# info.merge(record)
|
116
|
+
# record.each_value { |value| puts value }
|
117
|
+
# if record.value?("Bill") then puts "Bill is in there somewhere"
|
118
|
+
#
|
119
|
+
# The field name serves as the hash key, so these examples get fields called First Name and
|
120
|
+
# Last Name. (Note: Unlike a typical Ruby hash, Record objects are not case sensitive. You
|
121
|
+
# can say +record["first name"]+ or +record["FIRST NAME"]+ and it will still work.)
|
122
|
+
#
|
123
|
+
# A record object has the power to save changes to itself back to the database. For example:
|
124
|
+
#
|
125
|
+
# records.each { |record|
|
126
|
+
# record["First Name"] = record["First Name"].upcase
|
127
|
+
# record.save
|
128
|
+
# }
|
129
|
+
#
|
130
|
+
# That concise code converts the First Name field to all uppercase in every record in the
|
131
|
+
# ResultSet. Note that each time you call Record::save, if the record has been modified,
|
132
|
+
# Rfm has to send an action to FileMaker. A loop like the one above will be quite slow
|
133
|
+
# across many records. There is not fast way to update lots of records at once right now,
|
134
|
+
# although you might be able to accomplish it with a FileMaker script by passing a
|
135
|
+
# parameter).
|
136
|
+
#
|
137
|
+
# === Editing and Deleting Records
|
138
|
+
#
|
139
|
+
# Any time you edit or delete a record, you *must* provide the record's internal record
|
140
|
+
# if. This is not the value in any field. Rather, it is the ID FileMaker assigns to the
|
141
|
+
# record internally. So an edit or delete is almost always a two-step process:
|
142
|
+
#
|
143
|
+
# record = myLayout.find({"Customer ID" => "1234"})[0]
|
144
|
+
# myLayout.edit(record.record_id, {"First Name" => "Steve"})
|
145
|
+
#
|
146
|
+
# The code above first finds a Customer record. It then uses the Record::record_id method
|
147
|
+
# to discover that record's internal id. That id is passed to the Layout::edit method.
|
148
|
+
# The edit method also accepts a hash of record changes. In this case, we're changing
|
149
|
+
# the value in the First Name field to "Steve".
|
150
|
+
#
|
151
|
+
# Also, note the [0] on the end of the first line. A find _always_ returns a ResultSet.
|
152
|
+
# If there's only one record, it is still in an array. This array just happens to have only
|
153
|
+
# one element. The [0] pulls out that single record.
|
154
|
+
#
|
155
|
+
# To delete a record, you would do this instead:
|
156
|
+
#
|
157
|
+
# record = myLayout.find({"Customer ID" => "1234"})[0]
|
158
|
+
# myLayout.delete(record.record_id)
|
159
|
+
#
|
160
|
+
# Finally, the Layout::find method can also find a record using its internal id:
|
161
|
+
#
|
162
|
+
# record = myLayout.find(some_id)
|
163
|
+
#
|
164
|
+
# If the parameter you pass to Layout::find is not a hash, it is converted to a string
|
165
|
+
# and assumed to be a record id.
|
166
|
+
#
|
167
|
+
# === Performing Scripts
|
168
|
+
#
|
169
|
+
# Rfm can run a script in conjunction with any other action. For example, you might want
|
170
|
+
# to find a set of records, then run a script on them all. Or you may want to run a script
|
171
|
+
# when you delete a record. Here's how:
|
172
|
+
#
|
173
|
+
# myLayout.find({"First Name" => "Bill"}, {:post_script => "Process Sales"})
|
174
|
+
#
|
175
|
+
# This code finds every record with "Bill" in the First Name field, then runs the script
|
176
|
+
# called "Process Sales." You can control when the script actually runs, as explained in
|
177
|
+
# the documentation for Common Options for the Layout class.
|
178
|
+
#
|
179
|
+
# You can also pass a parameter to the script when it runs. Here's the deal:
|
180
|
+
#
|
181
|
+
# myLayout.find(
|
182
|
+
# {"First Name" => "Bill"},
|
183
|
+
# {:post_script => ["Process Sales", "all"]}
|
184
|
+
# )
|
185
|
+
#
|
186
|
+
# This time, the text value "all" is passed to the script as a script parameter.
|
187
|
+
#
|
188
|
+
# =Notes on Rfm with Ruby on Rails
|
189
|
+
#
|
190
|
+
# Rfm is a great fit for Rails. But it isn't ActiveRecord, so you need to do things
|
191
|
+
# a little differently.
|
192
|
+
#
|
193
|
+
# === Configuration
|
194
|
+
#
|
195
|
+
# To avoid having to reconfigure your Server object in every Rails action, you
|
196
|
+
# might add a configuration hash to the environment.rb. It can include all the
|
197
|
+
# options you need to connecto to your server:
|
198
|
+
#
|
199
|
+
# RFM_CONFIG = {
|
200
|
+
# :host => "yourhost",
|
201
|
+
# :account_name => "someone",
|
202
|
+
# :password => "secret",
|
203
|
+
# :db => "Customers"
|
204
|
+
# }
|
205
|
+
#
|
206
|
+
# Then you can get a server concisely:
|
207
|
+
#
|
208
|
+
# myServer = Server.net(RFM_CONFIG)
|
209
|
+
# myServer[RFM_CONFIG[:db]]["My Layout"]...
|
210
|
+
#
|
211
|
+
# You might even want to add code to your application.rb to centralize access
|
212
|
+
# to your various layouts.
|
213
|
+
#
|
214
|
+
# === Disable ActiveRecord
|
215
|
+
#
|
216
|
+
# If you're not using any SQL database in your Rails app, you'll quickly discover
|
217
|
+
# that Rails insists on a SQL database configuration anyway. This is easy to fix.
|
218
|
+
# Just turn off ActiveRecord. In the environment.rb, find the line that starts with
|
219
|
+
# +config.frameworks+. This is where you can disable the parts of Rails you're not
|
220
|
+
# using. Uncomment the line and make it look like this:
|
221
|
+
#
|
222
|
+
# config.frameworks -= [ :active_record ]
|
223
|
+
#
|
224
|
+
# Now Rails will no longer insist on a SQL database.
|
225
|
+
|
1
226
|
$: << File.expand_path(File.dirname(__FILE__))
|
2
227
|
|
3
228
|
require 'rfm_command'
|
229
|
+
require 'rfm_util'
|
4
230
|
require 'rfm_result'
|
5
|
-
require 'rfm_factory'
|
231
|
+
require 'rfm_factory'
|
232
|
+
require 'rfm_error'
|
data/lib/rfm_command.rb
CHANGED
@@ -2,62 +2,223 @@ require 'net/http'
|
|
2
2
|
require 'rexml/document'
|
3
3
|
require 'cgi'
|
4
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
|
5
12
|
module Rfm
|
6
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
|
+
|
7
59
|
class Server
|
8
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+
|
9
92
|
def initialize(options)
|
10
93
|
@state = {
|
11
94
|
:host => 'localhost',
|
12
95
|
:port => 80,
|
13
|
-
:
|
96
|
+
:ssl => false,
|
97
|
+
:account_name => '',
|
14
98
|
:password => '',
|
15
99
|
:log_actions => false,
|
16
100
|
:log_responses => false,
|
101
|
+
:warn_on_redirect => true,
|
17
102
|
:raise_on_401 => false
|
18
|
-
}.merge(options)
|
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
|
19
111
|
|
20
112
|
@host_name = @state[:host]
|
113
|
+
@scheme = @state[:ssl] ? "https" : "http"
|
21
114
|
@port = @state[:port]
|
22
|
-
@username = @state[:username]
|
23
|
-
@password = @state[:password]
|
24
115
|
|
25
116
|
@db = Rfm::Factory::DbFactory.new(self)
|
26
117
|
end
|
27
|
-
|
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.
|
28
131
|
def [](dbname)
|
29
132
|
self.db[dbname]
|
30
133
|
end
|
31
134
|
|
32
|
-
attr_reader :db, :host_name, :port, :state
|
135
|
+
attr_reader :db, :host_name, :port, :scheme, :state
|
33
136
|
|
34
|
-
|
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 = {})
|
35
165
|
post = args.merge(expand_options(options)).merge({action => ''})
|
36
|
-
|
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
|
+
|
37
181
|
if @state[:log_actions] == true
|
38
|
-
qs =
|
39
|
-
warn "
|
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}"
|
40
184
|
end
|
41
|
-
|
42
|
-
request = Net::HTTP::Post.new(
|
43
|
-
request.basic_auth(
|
44
|
-
request.set_form_data(
|
45
|
-
|
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|
|
46
191
|
http.request(request)
|
47
192
|
}
|
48
193
|
|
49
194
|
if @state[:log_responses] == true
|
50
|
-
|
195
|
+
response.to_hash.each {|key, value|
|
51
196
|
warn "#{key}: #{value}"
|
52
197
|
}
|
53
|
-
warn
|
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)
|
54
219
|
end
|
55
|
-
|
56
|
-
result
|
57
220
|
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
221
|
+
|
61
222
|
def expand_options(options)
|
62
223
|
result = {}
|
63
224
|
options.each {|key,value|
|
@@ -67,7 +228,16 @@ module Rfm
|
|
67
228
|
when :skip_records:
|
68
229
|
result['-skip'] = value
|
69
230
|
when :sort_field:
|
70
|
-
|
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
|
71
241
|
when :sort_order:
|
72
242
|
result['-sortorder'] = value
|
73
243
|
when :post_script:
|
@@ -98,7 +268,7 @@ module Rfm
|
|
98
268
|
when :modification_id:
|
99
269
|
result['-modid'] = value
|
100
270
|
else
|
101
|
-
raise "Invalid option: #{key} (are you using a string instead of a symbol?)"
|
271
|
+
raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
|
102
272
|
end
|
103
273
|
}
|
104
274
|
result
|
@@ -106,50 +276,318 @@ module Rfm
|
|
106
276
|
|
107
277
|
end
|
108
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
|
109
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.
|
110
345
|
def initialize(name, server)
|
111
346
|
@name = name
|
112
347
|
@server = server
|
348
|
+
@account_name = server.state[:account_name] or ""
|
349
|
+
@password = server.state[:password] or ""
|
113
350
|
@layout = Rfm::Factory::LayoutFactory.new(server, self)
|
114
351
|
@script = Rfm::Factory::ScriptFactory.new(server, self)
|
115
352
|
end
|
116
353
|
|
117
|
-
attr_reader :server, :name, :layout, :script
|
354
|
+
attr_reader :server, :name, :account_name, :password, :layout, :script
|
355
|
+
attr_writer :account_name, :password
|
118
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.
|
119
368
|
def [](layout_name)
|
120
369
|
self.layout[layout_name]
|
121
370
|
end
|
122
371
|
|
123
372
|
end
|
124
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
|
+
|
125
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"]
|
126
508
|
def initialize(name, db)
|
127
509
|
@name = name
|
128
510
|
@db = db
|
511
|
+
|
512
|
+
@loaded = false
|
513
|
+
@field_controls = Rfm::Util::CaseInsensitiveHash.new
|
514
|
+
@value_lists = Rfm::Util::CaseInsensitiveHash.new
|
129
515
|
end
|
130
516
|
|
131
|
-
attr_reader :name
|
517
|
+
attr_reader :name, :db
|
518
|
+
|
519
|
+
def field_controls
|
520
|
+
load if !@loaded
|
521
|
+
@field_controls
|
522
|
+
end
|
132
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.
|
133
530
|
def all(options = {})
|
134
531
|
get_records('-findall', {}, options)
|
135
532
|
end
|
136
533
|
|
534
|
+
# Returns a ResultSet containing a single random record from the table associated with this layout.
|
137
535
|
def any(options = {})
|
138
536
|
get_records('-findany', {}, options)
|
139
537
|
end
|
140
538
|
|
141
|
-
|
142
|
-
|
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
|
143
554
|
end
|
144
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_.
|
145
564
|
def edit(recid, values, options = {})
|
146
565
|
get_records('-edit', {'-recid' => recid}.merge(values), options)
|
147
566
|
end
|
148
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+.
|
149
579
|
def create(values, options = {})
|
150
580
|
get_records('-new', values, options)
|
151
581
|
end
|
152
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.
|
153
591
|
def delete(recid, options = {})
|
154
592
|
get_records('-delete', {'-recid' => recid}, options)
|
155
593
|
return nil
|
@@ -157,9 +595,49 @@ module Rfm
|
|
157
595
|
|
158
596
|
private
|
159
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
|
+
|
160
638
|
def get_records(action, extra_params = {}, options = {})
|
161
639
|
Rfm::Result::ResultSet.new(
|
162
|
-
@db.server, @db.server.do_action(action, params().merge(extra_params), options).body,
|
640
|
+
@db.server, @db.server.do_action(@db.account_name, @db.password, action, params().merge(extra_params), options).body,
|
163
641
|
self)
|
164
642
|
end
|
165
643
|
|
@@ -168,6 +646,61 @@ module Rfm
|
|
168
646
|
end
|
169
647
|
end
|
170
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.
|
171
704
|
class Script
|
172
705
|
def initialize(name, db)
|
173
706
|
@name = name
|