Implementing Multi-User Data Management Applications with Rebol Contents: 1. Multi-User Database Systems In Rebol

2. The Typical REBOL 101 Example

3. Multi-User Databases

4. A Longer Example

5. Obtaining Dynamically Assigned Server Addresses

6. Serving Clients HTML Form Interfaces

7. Simplicity

1. Multi-User Database Systems In Rebol Trivial single-user Rebol apps typically make use of saved block structures for persistent data storage. The routine is simple: create a block, manipulate the block in memory as needed by your app (append values, search, sort, remove values, etc.), using native series functions, and then use the 'save function to store the block to a text or binary file. This article demonstrates how to extend the use of simple blocks so they can operate with similar functionality in multi-user network based data management applications. In many cases, the need for third party DBMSs such as MySQL can be eliminated. 2. The Typical REBOL 101 Example Below is an example of a trivial single-user data management app. New Rebolers can learn to write this sort of code within a few days: rebol [title: "Single User Contacts App"] do %rebgui.r ; http://re-bol.com/rebgui.r if not exists? %contacts [write %contacts ""] display/close "Contacts" [ t: table 78x34 options [ "Name" left .3 "Address" left .4 "Phone" left .3 ] data (load %contacts) [set-texts [n a p] t/selected] f: panel data [ after 2 text 18 "Name:" n: field text 18 "Address:" a: field text 18 "Phone:" p: field reverse button " Submit " [ attempt [t/remove-row (index? find t/data t/selected) + 2 / 3] t/add-row/position reduce [ copy n/text copy a/text copy p/text ] 1 set-texts [n a p] "" ] button " New " [set-texts [n a p] "" t/redraw] button " Delete " [ t/remove-row (index? find t/data t/selected) + 2 / 3 set-texts [n a p] "" ] ] ] [save %contacts t/data alert "saved" quit] do-events In this app, data is read from a file and loaded into memory, manipulated, and saved when user operations are complete (when the GUI is closed). This sort of application model is trivial, but in-memory series containing 1 million+ data values can be manipulated in a fraction of a second, so performance when dealing with even significant mid sized volumes of information can be quite fast (GUIs need to be tuned to only display portions of large volumes of data, but managing lists themselves in memory is fast). Working with strings, directory listings, emails, network connections, widgets in GUIs, graphics, sounds, and even basic code structures in Rebol programs requires managing series, so proficiency with series manipulation is strengthened by constant use throughout most sorts of coding activities. Because native series operations are so pervasive in Rebol, it's natural for Rebol developers to "roll their own" series data structures and manipulation techniques, and to use saved blocks as a simple method for persistent storage (as opposed to using 3rd party RDBMSs), especially in small apps and utilities. There's generally no problem with the application and data model above, if only a single user ever runs the application on a single machine. 3. Multi-User Databases If several people need to edit the above contacts database simultaneously, then the model above will not operate properly. For example, if User1 runs the script and makes some changes to the loaded data, while at the same time User2 opens the same file and makes some changes, when each user saves their data file, one of the users will overwrite the other user's changes. Because each user never gets a chance to load the other's edited data before saving their own changes, each user's loaded data never gets a chance to include the other's concurrent edits, and that data is lost. Also, for example, if User2 needs to search the database for a piece of information, while User1 is in the process of adding data, the changes made by User1 will not appear in User2's search until User1 saves his unique version of the data. Additionally, allowing multiple users to write to the same file simultaneously can result in data write errors. To see a real example of this, try running the following script (10 instances of a program, all writing data to a file concurrently), and you'll see that file write errors actually do occur multiple times. This can't ever be allowed to occur in production code: rebol [title: "Test for concurrent file write errors"] x: copy [] insert/dup x 0 10000000 write %testdata.txt x write %concurrent-file-write-errors.r { rebol [title: "Test for concurrent file write errors"] script-name: form now repeat i 25 [ probe i write/append %testdata.txt a: rejoin [script-name " " i newline] if not find read %testdata.txt a [ alert rejoin ["Error: " script-name ", " i] ] ] print "Done" halt } loop 10 [launch %concurrent-file-write-errors.r] Other issues, such as secure data storage, password access, etc., also can't always be handled appropriately if all potential users have direct access to every file in which data is stored. The simple solution to all these problems is to hand over management of the data to a server script, which each of the client user apps connects to via a network connection. The server script loads the data series into memory, and performs data management operations in the exact same way, and with the same speed, as in the single user version of the app. A network server port (just a few lines of Rebol code), is opened by the server, to accept data management expressions from each of the client users. When each client script needs to perform an operation upon the series data, the necessary code (or some representation of that code) is sent to the server to evaluate and return results. Here's an example of a server which loads the %contacts file, as in the GUI example above. The network loop loads a string of code sent from a client, evaluates the code with 'do, and returns the molded results of the 'do evaluation back to the client. Some printed feedback is also presented in the server console, so the server administrator can watch any activity (the 'probe functions can be removed to improve performance): rebol [title: "Simple Data Server"] if not exists? %contacts [write %contacts ""] contacts: load %contacts print "waiting..." port: open/lines tcp://:55555 forever [ probe commands: load data: first wait connect: first wait port probe result: mold do commands insert connect result close connect ] Here's an example of a client script which sends some code to the server above, to be executed. The server evaluates the code with 'do and returns the result (the last expression in the line of code, which in the case below is just the number 1). As long as the server is running and the client is able to connect over the network, the data block ["Jim" "" ""] will be appended to the 'contacts block loaded in the server memory, the updated 'contacts block will be saved to the %contacts file on the server, and the integer value 1 will be sent back to the client script. When the client receives the response, if it's the integer 1, the client script prints a success message, then the connection to the server is closed: rebol [title: "Simple Client 1"] print "sending..." serverip: "localhost" port: open/lines rejoin [tcp:// serverip ":55555"] insert port {append contacts ["Jim" "" ""] save %contacts contacts 1} data-response: load first connect: wait port [ either 1 = data-response [print "Success"] [print "Failure"] close connect close port halt The following client script extends the idea above, sending 3 blocks of data to the server. Notice that because the 'contact variable label in the 'foreach loop only exists in the context of the client script (not anywhere in the server memory), the values it represents on each iteration of the loop must be concatenated (rejoined) into the string sent to the server. You'll need to do this in any client code which sends data represented by a local variable: rebol [title: "Simple Client 2"] print "sending..." serverip: "localhost" foreach [contact] [ ["Jim" "" "123-1234"] ["Bob" "" "234-2345"] ["Joe" "" "345-3456"] ] [ port: open/lines rejoin [tcp:// serverip ":55555"] insert port rejoin [ {append contacts } mold contact { save %contacts contacts 1} ] if 1 = data-response: load first connect: wait port [print "Success"] close connect close port ] halt The idea above can be extended to create a generalized 'server-exec function which sends any string of code to a server, and receives the server response. Notice that because the server port is opened in /lines mode, the 'trim/lines function is used to remove all carriage returns from the code string (/lines mode is a clean and simple way to transfer molded data - just always be sure to remove all line breaks using that 'trim/lines function). This 'server-exec function also handles errors, writes the current time and date, the sent code string, and the error information to a file, and alerts the client if ever a network error occurs: server-ip: "localhost" server-exec: func [strng] [ commands: trim/lines strng if error? err: try [ port: open/lines rejoin [tcp:// serverip ":55555"] insert port mold commands data-response: first connect: wait port close connect close port return data-response ] [ err: disarm :err write/append %net-err.txt rejoin [ now newline commands newline err newline newline ] alert "** Network Connection Error ** Try Again" return none ] ] Here's a slightly more robust generalized server script, with similar error handling: REBOL [title: "Server"] print "waiting..." port: open/lines tcp://:55555 forever [ if error? er: try [ probe commands: load data: first wait connect: first wait port probe result: mold do commands insert connect result close connect true ] [ er: disarm :er net-error: rejoin [ form now newline mold commands newline er newline newline ] write %server-error.txt net-error print net-error close connect ] ] As an example, we can send commands from the client GUI script presented earlier, to be processed by the generalized server script above. All we need to do is load the contacts data in the server app, and use the generalized 'server-exec function to send any changes to be made to the contacts data, from the client to the server. In the example below, the 'save operation (which occurs when the GUI window closes) and all changes made when the client user clicks any buttons in the GUI, are sent across the network to be processed by the server. This code looks hairy, but it's just a copy of the 'server-exec function, and then a copy of the contacts GUI, with those few 'server-exec operations added: rebol [title: "Network Contacts"] serverip: "localhost" server-exec: func [strng] [ commands: trim/lines strng if error? err: try [ port: open/lines rejoin [tcp:// serverip ":55555"] insert port mold commands data-response: first connect: wait port close connect close port return data-response ] [ err: disarm :err write/append %net-err.txt rejoin [ now newline commands newline err newline newline ] alert "** Network Connection Error ** Try Again" return none ] ] do %rebgui.r contacts: load server-exec {to-block load %contacts} display/close "Contacts" [ t: table 78x34 options [ "Name" left .3 "Address" left .4 "Phone" left .3 ] data contacts [set-texts [n a p] t/selected] f: panel data [ after 2 text 18 "Name:" n: field text 18 "Address:" a: field text 18 "Phone:" p: field reverse button " Submit " [ server-exec rejoin [{ remove/part find contacts } mold t/selected { 3 insert contacts [} mold copy n/text { } mold copy a/text { } mold copy p/text {] to-block contacts }] attempt [t/remove-row (index? find t/data t/selected) + 2 / 3] t/add-row/position reduce [ copy n/text copy a/text copy p/text ] 1 set-texts [n a p] "" ] button " New " [set-texts [n a p] "" t/redraw] button " Delete " [ server-exec rejoin [{ remove/part find/skip contacts } mold t/selected { 3 3 to-block contacts }] t/remove-row (index? find t/data t/selected) + 2 / 3 set-texts [n a p] "" ] ] ] [ if 1 = load server-exec {save %contacts contacts 1} [alert "Saved"] quit ] do-events Here's the particular variation of the server we'll use to handle the client script above. The only difference between it and the generalized server is that the %contacts file is loaded into memory: rebol [title: "Contacts Data Server"] if not exists? %contacts [write %contacts ""] contacts: load %contacts print "waiting..." port: open/lines tcp://:55555 forever [ if error? er: try [ probe commands: load data: first wait connect: first wait port probe result: mold do commands insert connect result close connect true ] [ er: disarm :er net-error: rejoin [ form now newline mold commands newline er newline newline ] write %server-error.txt net-error print net-error close connect ] ] Run the server above, then run as many different instances of the client script, all on different machines if you'd like, and they will all work well together. The model above is useful for handling a wide variety of data management activities, and it can handle many concurrent users, because all network requests are naturally queued by the server. That's a result of the inherent synchronous nature of the server code loop. Only one connection can be opened at any given instant and only one operation is ever being performed at a time to the data. So at any given instant, each network request is working only with the most current data block maintained in the server memory. This ensures that all users have concurrent access to the most up-to-date data at any given instant they perform a data manipulation operation, that multiple users never overwrite changes made by others who are working simultaneously with the data, that the system never experiences concurrent data write errors, etc. Other features such as secure password management are easy to add, because none of the client users ever need to have access to the server file system. Just move any such code to the server script. (If you really need to be secure, then the next level of defense is to encrypt the client script and all data sent across the network, so that the potential for code injection attempts is reduced). Data manipulation operations occur quickly in this model, because the entire database is still managed in the memory of the server machine. The only additional performance overhead is in the transfer of request and result values across the network. Requests are typically comprised of very small packets of code and data, and results tend to be limited to response codes and/or small subsets of fields or rows of values selected from the database, so the performance of the network model tends to stand up well in most typical applications. The full gamut of series functions, along with looping structures, conditional expressions, parse, math, and other native language features in Rebol enable all the data manipulation capabilities typically provided by an SQL DBMS (and more). Notice that very few changes need to be made to the code of the single-user GUI script. The data manipulation operations are just wrapped in the 'server-exec function, and the user interface is kept in the client code, which can be executed on any number of client machines simultaneously. Aside from this separation of code between client and server, and the small addition of some network communication code (the generalized 'server-exec function is all you need), the program is still basically the same as the single-user script. You can treat this tiny native client-server 'framework' as a replacement for heavier RDBMS tools such as MySQL. Because it's all native Rebol code, you have complete control of every operation. Any code that can be run in a simple single-user app can be sent to the server, using the 'server-exec function. You can choose which code to put in your client app, and which code to move to the server script, which security features to include, how to optimize performance, how to improve readability, etc. - it's all just pure simple Rebol code. 4. A Longer Example Below is a more detailed example of a network enabled client-server database application. It allows multiple groups of users to work with shared data in different ways. The first client app allows users to enter work order information using a tabbed GUI panel (just a generalized GUI form in this example). The second client app allows a separate group of users to view a table of entered work orders which are meant to be monitored and completed. Various levels of password protection are built in to ensure that only correct users are allowed access in each area of the different applications. Here's some common code used by both client applications: REBOL [title: "Common Functions and Code"] ; FOLDER, FILE, AND VARIABLE INITIALIZATION ============================== unless exists? %serverip.txt [write %serverip.txt "localhost"] serverip: read %serverip.txt ; FUNCTIONS ============================================================== server-exec: func [strng] [ commands: trim/lines strng if error? err: try [ port: open/lines rejoin [tcp:// serverip ":55555"] insert port mold commands data-response: first connect: wait port close connect close port return data-response ] [ err: disarm :err write/append %net-err.txt rejoin [ now newline commands newline err newline newline ] alert "** Network Connection Error ** Try Again" return none ] ] change-adminpw: func [/dlt] [ if error? try [ old-userpass-block: copy load server-exec {get-old-userpass-block} ] [return] alert rejoin [ "Current Usernames and Passwords:^/^/" (mold old-userpass-block) ] title-text: either dlt [ "Remove username/password: " ] [ "New username/password: " ] new-userpass: request-password/verify title-text: copy system/script/header/title show main-screen if any [ new-userpass = none new-userpass/1 = "" new-userpass/2 = "" ] [ alert "Changes NOT SAVED (both fields must be completed)." return ] either dlt [ if error? try [ server-exec rejoin [ {delete-adminpw } mold old-userpass-block { } mold new-userpass " true" ] ] [ alert "Admin username/password NOT removed." return ] alert "Admin username/password removed." ] [ if error? try [ server-exec rejoin [ {add-adminpw } mold old-userpass-block { } mold new-userpass " true" ] ] [ alert "New Admin username/password NOT saved." return ] alert "New admin username/password saved." ] ] ; LOGIN ================================================================== do %rebgui.r if error? try [admin-database: load server-exec {get-admin-database}] [ quit ] do login: [ userpass: request-password if any [userpass = none find userpass ""] [quit] logged-in: false foreach [user pass] admin-database [ if (userpass/1 = user) and (userpass/2 = pass) [ logged-in: true break ] ] either logged-in [] [alert "Incorrect Username/Password" do login] ] Here's the server app: REBOL [title: "Work Order Server"] ; FOLDER, FILE, AND VARIABLE INITIALIZATION ============================== unless exists? %adminpw [ save %adminpw to-binary encloak mold ["admin" "password"] "1234" ] unless exists? %superpw [ save %superpw to-binary encloak mold ["super" "secret"] "1234" ] make-dir %./history/ if not exists? %orders-data.txt [write %orders-data.txt ""] orders: load %orders-data.txt if not exists? %ordernum.txt [save %ordernum.txt 1] ; FUNCTIONS ============================================================== get-super-pass: does [load decloak to-string load %superpw "1234"] get-admin-database: does [load decloak to-string load %adminpw "1234"] get-old-userpass-block: does [load decloak to-string load %adminpw "1234"] delete-adminpw: func [old-userpass-block new-userpass] [ save %adminpw to-binary encloak ( mold admin-database: head remove/part find/skip old-userpass-block new-userpass/1 2 2 ) "1234" ] add-adminpw: func [old-userpass-block new-userpass] [ save %adminpw to-binary encloak ( mold admin-database: append old-userpass-block new-userpass ) "1234" ] ; SERVER LOOP ============================================================ print "waiting..." port: open/lines tcp://:55555 forever [ if error? er: try [ probe commands: load data: first wait connect: first wait port probe result: mold do commands insert connect result close connect true ] [ er: disarm :er net-error: rejoin [ form now newline mold commands newline er newline newline ] write %server-error.txt net-error print net-error close connect ] ] Here's the first client application, which allows users to enter work orders: REBOL [title: "New Work Orders"] ; FUNCTIONS ============================================================== save-order: func [order-data] [ server-exec rejoin [{ order-data: } mold order-data { write to-file rejoin [ %./history/ now/year "-" now/month "-" now/day "_" replace/all form now/time ":" "-" "_" "orders-data.txt" ] read %orders-data.txt append orders order-data save %orders-data.txt orders order-transaction: rejoin [ form now " - Saved Order:" newline mold order-data newline newline ] write/append %order-history.txt order-transaction print order-transaction true }] ] submit-all: does [ submitted: copy [] repeat i 2 [append submitted get-values main-screen/pane/:i] replace/all submitted 1 "Yes" replace/all submitted 2 "No" insert head submitted get-values customer-panel if any [ find submitted none find submitted "" ] [ if not question "Submit incomplete data?" [return] ] insert head submitted now unless next-inv: server-exec { ordernum: load %ordernum.txt ordernum: ordernum + 1 save %ordernum.txt ordernum ordernum } [return] insert head submitted next-inv either save-order submitted [alert "Saved"] [return] ] do %common.r ; GUI ==================================================================== ctx-rebgui/on-fkey/f3: make function! [face event] [submit-all] display/maximize/close/min-size copy system/script/header/title [ main-screen: tab-panel #LVHW data [ action [wait .2 set-focus name-field] " Customer " [ after 1 customer-panel: panel 87 data [ after 2 text 23 "Name:" name-field: field text 23 "Address:" field text 23 "City, State:" field text 23 "Zip code:" field ] text ; placeholder after 2 text 28 "Drop Down 1:" drop-list data ["aaa" "bbb" "ccc"] text 28 "Drop Down 2:" drop-list data ["ddd" "eee" "fff"] text 28 "Edit List 1:" edit-list data ["111" "222" "333"] text 28 "Edit List 2:" edit-list data ["444" "555" "666"] text 28 "Yes/No 1:" radio-group 30x5 data ["Yes" "No"] text 28 "Yes/No 2:" radio-group 30x5 data ["Yes" "No"] ] action [wait .2 face/color: 244.241.255] " Order Info " [ after 3 text 28 "Date 1:" day-field1: field text "..." [set-text day-field1 request-date] text 28 "Date 2:" day-field2: field text "..." [set-text day-field2 request-date] after 2 text 28 "Area1:" area text 28 "Area2:" area text text bar text text 44 button 35x15 " Submit All Entries " [submit-all] ] action [ if error? try [ unless ( load server-exec {get-super-pass} ) = request-password [ main-screen/select-tab 1 ] ] [main-screen/select-tab 1 return] wait .2 face/color: 240.255.240 ] " Options " [ after 1 text bold "Add New Admin Username/Password" [ change-adminpw ] text bold "Remove Admin Username/Password" [ change-adminpw/dlt ] bar text bold "Version" [ alert form system/script/header/version ] ] ] ] [question "Really Close?"] (system/view/screen-face/size / 4) set-focus name-field do-events Here's the second client application, which allows users to view selected work orders: REBOL [title: "Order Viewer"] ; FUNCTIONS ============================================================== extract-table-data: does [ if error? try [ ordrs: load server-exec {to-block load %orders-data.txt} ] [ quit ] gui-table-data: copy [] forskip ordrs 16 [ append gui-table-data reduce [ ordrs/1 ordrs/3 ordrs/2 ordrs/7 ordrs/9 ordrs/11 ordrs/13 ] ] gui-table-data ] do %common.r ; GUI ==================================================================== screen-size: system/view/screen-face/size cell-width: to-integer (screen-size/1) / (ctx-rebgui/sizes/cell) - 16 cell-height: to-integer (screen-size/2) / (ctx-rebgui/sizes/cell) table-size: as-pair cell-width (to-integer cell-height / 1.21) extracted-table-data: extract-table-data display/maximize/close/min-size copy system/script/header/title [ main-screen: tab-panel #LVHW data [ action [ wait .2 ] " Orders " [ orders-table: table table-size options [ "Order #" left .1 "Name" left .3 "Date" left .2 "Info 1" left .1 "Info 2" left .1 "Info 3" left .1 "Info 4" left .1 ] data extracted-table-data ] action [ if error? try [ unless ( load server-exec {get-super-pass} ) = request-password [ main-screen/select-tab 1 ] ] [main-screen/select-tab 1] wait .2 face/color: 240.255.240 ] " Options " [ after 1 text bold "Add New Admin Username/Password" [ change-adminpw ] text bold "Remove Admin Username/Password" [ change-adminpw/dlt ] bar text bold "Version" [ alert form system/script/header/version ] ] ] ] [question "Really Close?"] (system/view/screen-face/size / 2) do-events 5. Obtaining Dynamically Assigned Server Addresses A TCP server needs to be found at a known IP address (i.e., 192.168.1.10). If the network router is set up to assign IP addresses using DHCP (the default option for most off-the-shelf routers), then the IP address to which clients connect may need to be changed in the client program, potentially any time the server computer is restarted. One solution is to configure your server machine with a static IP address in the router. This setup step is different for every router manufacturer, it's often beyond the technical ability of the client app user, the router may not be accessible due to security concerns in an enterprise environment, etc. Another solution is to upload the server's current IP address to a server at a known URL (web server, FTP, etc. managed off site), but this just extends the problem to another network server, requires an Internet connection, etc. Another potential solution is to save the server IP address to a file on a mapped network drive, but this still requires some configuration which may be out of the user's capability (mapping network drives to a folder on the server machine, on each client computer, may not be possible in corporate environments, on the OS running the client script, etc.). A crude solution for simple applications is just to manually enter the IP address of the server (i.e., Joe yells to John down the hall "the server IP address is 192.168.1.10"). The code below demonstrates a consistently usable solution for all TCP apps, which requires no router or external network configuration. It creates two separate scripts which run on the client and server, to manage all server IP address updates. The %send-ip.r script runs on the server machine and continuously broadcasts the IP address over UDP. The %receive-ip.r script runs on the client, receives the current IP and writes it to a file. Because UDP is a broadcast protocol, no known IP addresses are required for this to work. Once the server script is running, the clients can all simply start and receive the current IP address being broadcast. This example includes a separate TCP chat app which simply reads the saved IP address and connects to the server. No other network configuration is required. To implement this routine in any TCP application, just run the %send-ip.r script on any server, run the %receive-ip.r script on any client(s), and you can read the %local-ip.r file in your client apps to connect to the current IP address of the server: REBOL [title: "UDP Communicate Server IP"] write %receive-ip.r {rebol [] net-in: open udp://:9905 print "waiting..." forever [ received: wait [net-in] probe join "Received: " trim/lines ip: copy received write %local-ip.r ip wait 2 ]} launch %receive-ip.r write %send-ip.r {rebol [] net-out: open/lines udp://255.255.255.255:9905 set-modes net-out [broadcast: on] print "Sending..." forever [ insert net-out form read join dns:// read dns:// wait 2 ]} launch %send-ip.r write %tcp-chat.r {rebol [title: "TCP-Chat"] view layout [ across q: btn "Serve"[focus g p: first wait open/lines tcp://:8 z: 1]text"OR" k: btn "Connect"[focus g p: open/lines rejoin[tcp:// i/text ":8"]z: 1] i: field form read %local-ip.r return ; read join dns:// read dns:// r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [ if error? try [x: first wait p] [quit] r/text: rejoin [x newline r/text] show r ]]] return g: field "Type message here [ENTER]" [insert p value focus face] ]} wait 2 launch %tcp-chat.r launch %tcp-chat.r 6. Serving Clients HTML Form Interfaces Another useful solution in some multi-user environments is to create a server app which serves HTML forms and processes data returned by users from those forms. This can be useful when collecting input from users who connect to your network using various ad hoc mobile devices. Clients can input data with any device (iPhone, Android, netbook, etc.) to enter information into the server app, as long as the device has a basic web browser and Wifi (or other network) connectivity. This is practical in environments where customers, clients, students or other walk-in groups of users need to interact with the server app in your local environment. Because mobile devices with Wifi and browsers are so pervasive, it's often a viable and convenient method for users off the street to interact immediately with your server app, without having to provide any public hardware access, and without requiring the users to install any software. Also, this option can be useful in environments where many in-house users need to be outfitted with inexpensive devices to input data. Various Android devices, for example, are available in the $50 - $70 price range. They can be carried easily, used without a desk, mouse or keyboard, etc., so they're great for situations in which employees are moving around a job site, stocking inventory, etc. The example below provides a short code framework to enable this possibility. Just edit the HTML form example, and do what you want with the 'z variable data block returned by the user(s). Beyond the essential network port code and server loop, the 'decode-cgi and 'write-io functions do most of the work. The 'decode-cgi function processes the incoming data submitted by the HTML form. The 'write-io function sends the HTML string to the client, through the network port: REBOL [title: "HTML Form Server"] l: read join dns:// read dns:// print join "Waiting on: " l port: open/lines tcp://:80 browse join l "?" forever [ connect: first port if error? try [ z: decode-cgi replace next find first connect "?" " HTTP/1.1" "" prin rejoin ["Received: " mold z newline] my-form: rejoin [ {HTTP/1.0 200 OK^/Content-type: text/html^/^/ <HTML><BODY><FORM ACTION="} l {">Server: } l {<br><br> Name:<br><INPUT TYPE="TEXT" NAME="name" SIZE="35"><br> Address:<br><INPUT TYPE="TEXT" NAME="addr" SIZE="35"><br> Phone:<br><INPUT TYPE="TEXT" NAME="phone" SIZE="35"><br> <br><input type="checkbox" name="checks" value="i1">Item 1 <input type="checkbox" name="checks" value="i2">Item 2 <input type="radio" name="radios" value="yes">Yes <input type="radio" name="radios" value="no">No<br><br> <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit"> </FORM></BODY></HTML>} ] write-io connect my-form (length? my-form) ] [print "(empty submission)"] close connect ] The code below demonstrates a useful app created from the Form Server script above. On each run, it generates a unique HTML form based on user specs (any number of check, radio, and text entry items), and starts a server to receive survey responses from the audience (they all connect to the LAN server using phones or any other Wifi Internet device). The survey responses are all saved to a user-specified file and an included demo report displays all submitted entries, plus a total list of all check items and radio selections. Then it presents a bar chart displaying the survey's check and radio results: REBOL [title: "Room Poll (HTML Survey Generator for LANs)"] view center-face layout [ style area area 500x100 across h4 200 "SURVEY TOPIC:" h4 200 "Response File:" return f1: field 200 "Survey #1" f2: field 200 "survey1.db" below h4 "SURVEY CHECK BOX OPTIONS:" a1: area "Check Option 1^/Check Option 2^/Check Option 3" h4 "SURVEY RADIO BUTTON OPTIONS:" a2: area "Radio Option 1^/Radio Option 2^/Radio Option 3" h4 "SURVEY TEXT ENTRY FIELDS:" a3: area "Text Field 1^/Text Field 2^/Text Field 3" btn "Submit" [ checks: parse/all a1/text "^/" remove-each i checks [i = ""] radios: parse/all a2/text "^/" remove-each i radios [i = ""] texts: parse/all a3/text "^/" remove-each i texts [i = ""] title: join uppercase f1/text ":" response-file: to-file f2/text unview ] ] write response-file "" write %poll-report.r rejoin [{ rebol [title: "Poll Report"] view center-face layout [ btn 100 "Generate Report" [ all-checks: copy [] all-radios: copy [] print newpage print {All Entries:^/} foreach response load %} response-file {[ x: construct response ?? x if find first x 'checks [ either block? x/checks [ foreach check x/checks [ append all-checks check ] ][ append all-checks x/checks ] ] if find first x 'radios [ either block? x/radios [ foreach radio x/radios [ append all-radios radio ] ][ append all-radios x/radios ] ] ] alert rejoin [ "All Checks: " mold all-checks " All Radios: " mold all-radios ] check-count: copy [] foreach i unique all-checks [ cnt: 0 foreach j all-checks [ if i = j [cnt: cnt + 1] ] append check-count reduce [i cnt] ] radio-count: copy [] foreach i unique all-radios [ cnt: 0 foreach j all-radios [ if i = j [cnt: cnt + 1] ] append radio-count reduce [i cnt] ] bar-size: to-integer request-text/title/default "Bar Chart Size:" "40" g: copy [backdrop white text "Checks:"] foreach [m v] check-count [ append g reduce ['button m v * bar-size] ] append g [text "Radios:"] foreach [m v] radio-count [ append g reduce ['button gray m v * bar-size] ] view/new center-face layout g ] btn 100 "Edit Raw Data" [ alert "Be careful!" editor %} response-file { ] ] }] launch %poll-report.r poll: copy "" repeat i len: length? checks [ append poll rejoin [ {<input type="checkbox" name="checks" value="} i {">} checks/:i {<br>} newline ] ] append poll {<br>} repeat i len: length? radios [ append poll rejoin [ {<input type="radio" name="radios" value="} i {">} radios/:i {<br>} newline ] ] append poll {<br>} repeat i len: length? texts [ append poll rejoin [ texts/:i {:<br><INPUT TYPE="TEXT" NAME="text} i {" SIZE="35"><br>} newline ] ] append poll {<br><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">} l: read join dns:// read dns:// print join "Waiting on: " l port: open/lines tcp://:80 browse join l "?" responses: copy [] forever [ q: first port if error? try [ z: decode-cgi replace next find first q "?" " HTTP/1.1" "" if not empty? z [ append/only responses z save response-file responses print newpage entry-received: construct z ?? entry-received ] d: rejoin [ {HTTP/1.0 200 OK^/Content-type: text/html^/^/ <HTML><BODY><FORM ACTION="} l {">} title {<br><br>} poll {</FORM></BODY></HTML>} ] write-io q d length? d ] [] ;[print "(empty submission)"] close q ] halt Mixing Rebol and HTML is a simple way to extend the reach and possibilities of your multi-user data management applications. Learn to use the 'write/custom, 'decode-cgi, and 'write-io, and you can create servers which work with both web based and pure Rebol clients. You can see more about this at http://www.rebol.com/docs/quick-start6.html. Pay particular attention to the 'send-server function in that tutorial. 7. Simplicity The example code presented here provides everything you need to build network enabled multi-user data management apps. The general model of using the 'server-exec function and a bit of server code to process requests, provides a basis for handling all necessary client-server code separation. The entire framework consists of just a few dozen lines of entirely native Rebol code. There's no need to deal with third party software installation and support problems, upgrade challenges, compatibility issues, or other maintenance dilemmas. The client and server machines can consist of any mix of hardware and OS platforms which have Rebol installed. Because any software created using this design is purpose-built to handle only the operations required, it is absolutely no more complex or heavyweight than needed to complete its purpose, and it is as malleable and capable as needed to satisfy its functionality. And because the code and all parts of the system are built purely from native Rebol language constructs, the code can be simplified even further by the development of dialects, if that becomes a worthwhile endeavor (for example, if a developer's API were needed to interface with a more complex distributed system). The complications associated with integrating third party data models and APIs are all completely eliminated. It's all just basic Rebol. Anything that can be accomplished in a simple single user script can be easily divided into multi-client components (and multi-server components ... but that's a topic for a whole other article), by applying the simple concepts demonstrated here.