An open software
lightweight web application server framework built in C++, and programmed in
XML and C++.
Retro is a web application server framework. That is it provides an environment in which complex web applications can be designed, built and executed. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. The software and documentation is distributed in the hope that it may be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. (GNU General Public License can be found here.) Downloads are available for Linux/Apache and for Windows/IIS.
Retro requires sets of related web applications to be described in an XML Application Set Description - ASD. Such a description specifies what applications constitute the set, and what HTML pages or other server responses constitute each application. The flow of control between these pages is also specified in the ASD, with facilities for conditional branching, looping, etc. Variables can also be defined in the ASD at the application-set, application, or page scope, so that business data can be kept out of the programming arena, and can be easily changed at a central point. The ASD allows much of the high-level structure and logic of a complex system to be defined and documented before a single page is designed, or any custom logic is programmed.
HTML pages can be generated from templates in the fashion of .asp or ,jsp pages. Alternatively, entire responses can be generated, rather like Java Servlets. The templates - .csp files - use familiar constructs to interpolate variable values and embed custom logic. However, instead of embedding script in the template, the corresponding custom logic is provided by compiled C++ code modules according to the platform, e.g. shared object libraries for a Linux based Apache server, or DLLs in the case of Windows/IIS. This separation of page design and custom logic implementation, as well as that between the higher level ASD, should generally be helpful for team efforts.
Retro provides for persistent application instances using some technique appropriate to the particular web server. It is currently implemented for the Apache web server on GNU/Linux using the Apache FastCGI module, and for Windows/IIS using ISAPI. The combination of persistent applications and the efficiency of compiled code make for high performance.
Retro embeds support for MySQL as its database system of choice, and is therefore suitable for typical 3-tier applications. However the custom logic framework is sufficiently flexible that working with other database systems should be perfectly feasible. It also provides inherent support for intelligent client-side processing, and persistent application state, based on XML data islands. It uses the Expat XML parser internally at the server side, and provides Javascript components for browser-insensitive XML handling at the client side.
Retro recognizes that most web facilities consist of a set of related applications. The individual applications may be discrete, or may communicate, either directly through URLs, or through a shared database. A single application set might well constitute the entire role of some web server. However, the notion of an application set will often also be convenient for the administrative separation of responsibility for development, maintenance, and operation of applications. Currently, the Retro hierarchy consists of the Application Set, which is composed of Applications, which are in turn composed of Pages. The pages don’t have to be HTML, they just represent individual responses from the web server
At the application set level, it’s possible to define constantsvariables that
apply to the collection of applications, and the destination of log/debug
information for the application set. It’s also possible to set a variety of
defaults that can be overridden as required at the application level, such as
the host names for databases,and other database parameters,
andthe paths for error and exit
pages.various
paths. For example:
<?xml version="1.0"?>
<ApplicationSet verbosity="error"verbosity="debug"
logpath="/dev/pts/2">
<MySQL
host="hostname" db="dbname"dblist="dbname1,dbname2"
preload=”1”
user="username" epw="a1Bcdef3+yhg" cache="y"/>
<Paths
exit="/AppSetName/thankyou.htm"
error="/AppSetName/errpage.htm"/><Error
path="/AppSetName/errpage.htm"/>
<Exit path="/AppSetName/thankyou.htm"
<Variable name="varname1" value="varvalue1"/>
<Variable name="varname2" value="varvalue2"/>
…...
</ApplicationSet>
An application defines a set of Pages, and determines where
among them an application is to start, and possibly where it is to finish. It can override database information,
and exit and error page details,information specified at the application-set
level. It can also specify variablesand values
in the scope of the application. These are in a different namespace than the
Application-Set variables. For example:
<?xml version="1.0"?>
<ApplicationSet name=”AppSetName” verbosity="error" logpath="/dev/pts/2">>
<MySQL
host="hostname1" dblist=”dbname” preload=”1”
user="username"
epw="a1Bcdef3+yhg"/>
<Paths
exit="/AppSetName/thankyou.htm"
error="/AppSetName/errpage.htm"/><Exit
path="/AppSetName/thankyou.htm"
<Error path="/AppSetName/errpage.htm"/>
<Variable name="orgname" value="ACME Industries Inc"/>
<Variable name="copyright" value="Copyright (C) 1999 - 2002"/>
<Application name="app1"start=”page1”
state="hxml" xmlvar=”__retro_xml”>
<Paths
exit="/AppSetName/AppName/thankyou.htm"<Exit
path="/AppSetName/AppName/thankyou.htm"
error="/AppsetName/Appname/errpage.htm"/>
<MySQL host="hostname2" db="dbname"db="db1,db2"
preload=”1” user="another"
epw="39hjer+uj=" cache="y"/>
<Variable name="MaxQueryRows" value="50"/>
<Variable name="Version" value="1.9.1"/>
<Page … />
<Page … />
</Application>
…...
</ApplicationSet>
An application comprises one or more pages, each of which
may require a template and/or some code to provide logic. Pages have attributes that determine
whether or not they will use a Page Implementation Object (PIO), and if so, for
what purposes or phases. They also
determine whether the template for the page should be cached, and whether a
template is to be used at all.
all. A page may also
declare itself to be the start page for its parent application
They also describe a path or paths through the application
by specifying the page that is to follow a particular page,
subject to any requiredthem, possibly subject to dynamically determined
conditions. Becauseof
this conditional flow control is provided at the page level, an Application
definition constitutes a high-level program.
Page scope variables can also be defined.
<?xml version="1.0"?>
<ApplicationSet name=”AppSetName” verbosity="error" logpath="/dev/pts/2">>
<MySQL
host="hostname1" dblist=”dbname” preload=”1”
user="username"
epw="a1Bcdef3+yhg"/>
<Exit
path="/AppSetName/thankyou.htm"
<Error path="/AppSetName/errpage.htm"/>
<Variable name="orgname" value="ACME Industries Inc"/>
<Variable name="copyright" value="Copyright (C) 1999 - 2002"/>
<Application name="app1" state="hxml" xmlvar=”__retro_xml”>
<Exit path="/AppSetName/AppName/thankyou.htm"/>
<Error
path="/AppsetName/Appname/errpage.htm"/>path="/AppSetName/AppName/thankyou.htm"
error="/AppsetName/Appname/errpage.htm"/>
<MySQL host="hostname2" db="dbname"db="db1,db2"
user="another"
epw="39hjer+uj=" cache="y"/>
<Variable name="MaxQueryRows" value="50"/>
<Variable name="Version" value="1.9.1"/>
<Page name="page1" phases="pci" options="si" next="+">
<Variable name=”foo” value=”bar”/>
<Variable name=”quality” value=”horrid”/>
</Page>
<Page name="page2" phases="pir" options=”g” next="next? page1: page3"/>
<Page name="page3" phases="r"/>
</Application>
<XML
path=”/AppSetName/xml”/>
…...
</ApplicationSet>
Retro finds out what its expected to do with an HTTP request by looking for supplementary path information. A typical URL submitted to the server by a Retro application page might be of the form:
http://www.retroserver.com/trivia/bin/trivia.fcgi/app1/page2?somevar=whatever
In this URL, "trivia" is a real directory (or a
web-server alias for a real directory), corresponding to the application
set. The real file /trivia/trivia.retro/trivia/bin/trivia.fcgi
is the executable that FastCGI starts to service its applications (or in the
Win32 case, it’s \trivia\trivia.retro, and the file holds the XML that
describes the application set..set named “trivia”. The path information following thethis file name
simply states the application name and the name of the submitting page. In the
example there’s some query data appended to the URL as well.
This URL makes no attempt to specify what page should be processed next. It merely reports what page submitted the request. The Retro framework will determine what page is to be served up next. You make those decisions when you write the XML for the application set. In the first instance, when an application is invoked, the page name is omitted, and is inferred from the application-set description. This, the ASD, is parsed and loaded whenever an application in the set is first requested. If a persistent executable is loaded (as in FastCGI), the application-set data becomes global read-only data in that application. If there is no loading of an executable, as in the ISAPI context, read-only application-set data is stored in a look-up table in the Retro ISAPI extension. From then on, while the server stays up, the cached data is used in read-only mode.
When Retro receives such a request, the first thing it does is to parse out the variable values supplied in the GET/POST request. Retro makes no particular distinction between these two types of request, and simply places the variable name/value pairs in a symbol table for future reference.
What happens next is determined initially by the ‘phases’ attribute on the page of the supplied name, or the page designated as first. If this contains the ‘r’ flag for return processing, then the PIO for the page is loaded and cached, and its returnproc() method is called (as long as this isn’t the first call to the first page). If the PIO doesn’t have the appropriate entry point, retro reports an error.
Once any return processing has been completed, Retro determines from the page definition, using any supplied conditions, what page is to be served next. (The return processing can if necessary force an arbitrary page, but this is rather like using goto. You can’t then tell what will happen simply by knowing the values of variables and looking at the ASD.)
A sequence of operations is then possible, depending on the value of the ‘phases’ attribute on this next page:
1) Preamble (p) – general processing before the next page can be generated,
2) Set cookies (c) – set up the values for any required cookies,
3) Extra headers (h) – set up the strings for any HTTP headers other than the standard ones,
4) Prepare to make any template insertions.
5) Programmatically generate the entire response
Each of these, if specified, requires a call to the PIO for the page, so if any these flags are specified, the PIO for the new page is loaded/cached. It’s quite possible, indeed normal, for none of these to be required, in which case, if the new page doesn’t require return processing either, you don’t need to create a PIO for it.
The PIO entry points for 1, 2, and 3, as required, are then
called, and the entry point for any insertions is set up. Then the standard HTTP headers are
generated, followed by the headers for any stipulated cookies, and any
supplementary headers you created.
Following these operations, Retro isthen in a
position to generate the HTML for the page from the page template, a .csp file.
The .csp file is a normal HTML file with the addition of insertion (<%ipname%>) and substitution (<%=varname%>) tags. The latter are simply replaced with the values previously populated into the variable symbol table, possibly modified and/or augmented by return and preamble processing. The former generate a call to the PIO insertions() entry point, which is free to generate HTML directly via an ostream object. The insertion method should check the supplied insertion point name and behave accordingly, returning an error message if the insertion point name is not recognized.
When the template processing is complete, the generated HTML is flushed to the client. If it is specified in the ‘phases’ for the page, the PIO postamble entry point is then called to give you an opportunity to clear up any data structures you may have created in the stages described above.
The page sentin this way
to the browser in
this way will in turn submit a request embodying its qualified name
(/app/page), and the sequence will be repeated as required.
In the example above,
<Page name="page1" phases="pci" options="si" next="+"/>
page1 and page 2 are used in sequence, as indicated by the next="+" attribute on page1. The possible values for next have the following effects:
|
"." |
Repeat the same page |
|
“+” |
Go to the next page as defined in the ASD |
|
"-" |
Go to the previous page as defined in the ASD |
|
"^" |
Go back to the first page of the application |
|
"pageX" |
Go to the named page |
|
*varX |
Go to the page named by variable varX |
|
“cond? page2, page3” |
Ternary conditional, if cond true (“1”, “y”, “Y”), page2, else page3 |
|
|
|
|
"option# page2, page3, page4" |
Switch to a page
depending on the integer zero based value of variable "option". |
If there is no ‘next’ attribute, the next page will be the exit page specified for the application set, or for the particular application. It is generally assumed that this exit page does not submit a request referencing a page in the application. Nothing prohibits this, but really it is a new invocation of the application.
Most pages will use a .csp template, but this is not required. If you choose to do so, you can generate the entire page, or everything after the HTTP headers. Where most of the page needs to be generated, skipping the overhead associated with template interpretation may result in improved performance.
If a template is required, you may choose to parse and interpret it as required, or to parse it once, and cache a more efficiently interpretable version or further use. Cached templates will give better performance, but will of course use more memory – the usual tradeoff.
These choices are made in the page options flags. The ‘G’ option indicates that you will generate everything; ‘g’ indicates that you will generate everything after the HTTP headers. In either case a generate() entry point is required in the PIO, and this will be called once at the appropriate point to generate whatever was specified.
The ‘c’ option indicates that the template should be parsed and cached. The ‘i’ option indicates that the page is to initialize the application state mechanism as described in the next section.
Retro supports the use of XML data islands in your HTML pages for the maintenance of application state. You can specify that you want to do this by setting the ‘state’ attribute on the Application tag in the ASD to “hxml” (XML in a hidden HTML field). The attributes of the application can also provide the name of an HTML field and Retro variable that is to be used to store the application state XML (the default name is __retro_xml).
When a page submits, the value of this field is examined. If it exists, and contains text, its value is parsed, and the results are loaded into a simple XML object model. This read/write object exists on the Retro request object, and is available through the request object to all phases of page processing.
When the page template is interpreted, the interpreter treats an occurrence of <%=__retro_xml%> (or whatever you specified as the name in the application definition) as a special case. At that point, it will generate packed XML (no extraneous whitespace, and appropriate encodings) from the XML object, and substitute the XML as the field value.
This mechanism requires your .csp file to contain a hidden variable such as:
<input
type=hidden name=__retro_xml value=’<%=__retro_xml%>’>
The XML variable name appears twice, once as the name of the
HTML input field (from which itXML from the page will be loaded), and again
as the name of the substitution, so that the XML will be placed there during
template processing. Note the
single quotes around ‘<%=__retro_xml%>’. These are necessary since your XML will doubtless contain numerous
double quote characters. Single
quotes in the XML are encoded (").
It’s also worth noting that to prevent redundant submission of HTML form data when using XML, you should use two or more forms in your HTML document. One of these will be reserved for submission of the page, e.g.:
<form name=f0 action=”/appset/appset.retro/app1/page1”action="/appset/bin/appset.fcgi/app1/page1"
method=POST>
<input
type=hidden … >name=__retro_xml
value='<%=__retro_xml%>'>
<input type=submit value=” Submit “>
</form>
For an example, see the trivia/dbopsdbops/member
application. Note that the submit
button(s) will probably have to be in this same form, since browsers appear to have
difficulty submitting formA from a submit button in formB.
The initial representation of your application data should
be named for the application – appname.xml – and should be put
wherever the templates foryou can specify its location by using the XML tag
in the ASD. By default retro will
look for a directory called “xml” under the root of the application are
located. set. It will be loaded, once per application
invocation, by the page that has the ‘i’ option set. If the XML variable already contains data, this step is
skipped – it’s already been loaded, and the application has recycled to the
same page.
Although you can override most of this in the ASD file,
Retro has a default directory structure for theits execution
environment. For each application
set there will be a top-level directory named for the application set. Under this, the directory structure
should be as follows for the Apache/FastCGI and IIS versions respectively:
AppSetName
|
------------------------------------------------------------------------------------------------------
| | | | |
log ------------------------------- AppSetName.retro xml bin
| | | |
App1 App2 App3 AppSetName.fcgi
|
---------------------
| |
template PIO
AppSetName
|
-------------------------------------------------------------------------------------------------------
| | | | |
log ------------------------------- AppSetName.retro xml bin
| | |
App1 App2 App3
|
---------------------
| |
templates PIOs
In the IIS case, an ISAPI extension is loaded explicitly, using a full path/name, and retroapi.dll can be kept in the bin directory of any one of the application sets, or elsewhere.
In the Apach/FastCGI case, the loaded executable,Apache/FastCGI case,
the executable - AppSetName.fcgi - named for the application set to
distinguish its log records, will be a symbolic link to an
executable -
raf.exe - kept in one of the usual places, such as /usr/local/bin, and
the shared library that implements most of the retro API will be kept in a similar place, e.g. /usr/local/lib. The bin directory is used to hold this
executable link, where it can be given appropriate ownership/permissions.
Other than in these respects, the directory structures are equivalent. There is a subdirectory under AppSetName for each application, and under each of these directories for the application page templates, and for the PIO objects. (In each case these are loaded explicitly as required.)
The application-set directory can conveniently contain static web content file common to the set of applications. The application directories can conveniently contain static web content files particular to that application, including, but not limited to the exit HTML page and any error HTML.
Tags are described here using a loose notational convention as follows:
Required attribute: attname=”attvalue”
Optional attribute [attname=”attvalue”]
Enumerated attributes “cat | dog | mongoose”
Required child tag <tagname . . . />
One or more of same name <tagname . . . />+
Optional tag (zero or one) <tagname . . . />?
Zero or more tags of same name <tagname . . . />*
Attribute values in italics are explained separately.
ApplicationSet tag
The top level tag in a Retro ASD is the application set tag. It takes the following form:
<ApplicationSet logpath=”/path/to/log/file” [name=”AppSetName”]
[verbosity=”error | warning | info | debug”]
[version=”versionstring”]
[xmlvar=”subst_here”]>
<Application . . . />+
<Error . . . />?
<Exit . . . />?
<MySQL . . . />?
<Namespaces . . . />?
<PIO . . ./>?
<Template . . ./>?
<Variable . . . />*
<XML . . ./>?
</AppSetName>
Tags of any other name are ignored.
If the name attribute is not present, it defaults to the first joint of the name of the file from which the xml file was loaded (e.g. AppSetName.retro -> name=”AppSetName”). If the verbosity attribute is not present at the application-set scope, it defaults to error. If the version attribute is not present it defaults to the empty string. If the xmlvar attribute is not present, it defaults to “__retro_xml”.
Application tag
There must be one or more Application tags, taking the form:
<Application name=”AppName” [state=”hxml | cxml”] [xmlvar=”subst_here”]>
<Error . . . />?
<Exit . . . />?
<Page . . . />+
<PIO . . . />?
<Template . . . />?
<Variable . . . />*
</Application>
If the state attribute is not defined, it defaults to the empty string. If the xmlvar attribute is not defined it defaults to whatever is set for the application set.
Error tag
The Error tag is optional. It takes the form:
<Error path=”/path/to/error.html”/>
The path provided will be used to construct an error page by
appending error information and “</body></html>”. It may be defined at application-set or
at application scope. A definition
at application scope overrides any value at application-set scope.
scope. This is a filesystem path. A path without a leading ‘/’ or “http” will be taken as relative to the application set, e.g. path=”path/to/error.html” in application-set “trivia” will be taken as “/trivia/path/to/error.html”, or if trivia is an alias, the corresponding path.
Exit tag
The Exit tag is optional. It takes the form:
<Exit path=”/path/to/exit.html”/>
The path provided will be used as a target URL when the
application submits from a page for which no “next” attribute is provided. It may be defined at application-set
scope. A definition at application
scope overrides any value at application-set scope. The tag is optional, a Retro application that has no exit tag,
and any page with no next attribute, will attempt to load a page “/index.html”.“/appsetname/index.html”.
Note that the path here is relative to your web server root. A path without a
leading ‘/’ or “http” will be taken as relative to the application set, e.g.
path=”path/to/error.html” in applicatio-set “trivia” will be taken as “/trivia/
path/to/error.html”.
MySQL tag
The MySQL tag is optional in the sense that you don’t need one if your application(s) don’t use a database. It has optional attributes at application-set scope:
<MySQL [none=”anystring”]
[host=”hostname”]
[dd=”dbNameList”] [user=”username”]host=”hostname”
dblist=”dbNameList”
user=”username”
epw=”encoded password”
[preload=”countToPreload”]
[epw=”encoded password”]/>[cachecon=”Y”] />
The “none” attribute with any non-empty value e.g.
none=”true”, overrides all other attributes here, and says this application
makes no use of a database. Specifying none will marginally increase
performance. Otherwise,if any of thesethe non-optional
attributes not defined at application-set scope must be provided at application
scope. The epw attribute is a
base64 encoded encryption of the specified user’s database password. The Retro utility retropwe will encode
passwords, and the retro framework will decode them using the same key. You can set your own key by recompiling
a small Retro module.
A dbNameList takes the form of a comma separated list: “dbName1,dbName2,prefix*var+suffix,prefix*var+suffix” The preload attribute is interpreted as an integer countToPreload. This number of database connections will be established when the application starts, and Retro will report an error if any of them can’t be opened at that time. Database names of the form prefix*var+suffix are expanded by taking the literal value of “prefix”, appending the value of request variable var, then appending the literal value of suffix. So a database specification with preload=”1” and dd=”admin,*customer+_users”, will attempt to connect to the database “admin” at start-up, and to a database acme_users (when the value of request variable customer is “acme”), at such time as the value of customer is found to be non-blank. Namespaces tag The Namespaces tag is optional, and takes the following form: <Namespaces [appset=”appsetNSQ”] [app=”appNSQ”] [cookies=”cookiesNSQ”] [page=”pageNSQ”]/> At substitution points in pages, where you maywant to use variables defined in the ASD, you must qualify the variable name with a namespace qualifier. The Namespaces tag allows you to choose your own qualifiers. By default they are:
Application set scope |
AS |
Application scope |
AP |
Page scope |
PG |
Cookies |
CK |
These namespaces are accessed programmatically through the appSetVar(), appVar(), pageVar(), and cookie() methods of the Request object. Variables derived from the GET/POST data don’t have to be qualified, and are accessed programmatically through the value() method of the Request object. For example, your page might contain: <h2><%=AS::company%> <%=AP::product%> (<%=PG::version%>)></h2> which would use variables from the application-set scope, the application scope, and the GET/POSTpage variables respectively. Page tag Your application must define one or more Page tags, of the following form: <Page name=”PageName” [phases=phaseSet] [options=optionSet] [next=nextSpec] [xheads=”extraHeaders”] [mimetype=”mimeType”]/>[mimetype=”mimetype”]/> The phaseSet attribute is any combination of the character flags:
Return processing |
r |
Preamble processing |
p |
Cookie processing |
c |
Extra headers |
h |
Insertion points |
i |
Postamble processing |
a |
If no phases attribute is provided, your page will do no custom processing, only variable substitution in the template. Youtemplate, and you do not need to provide a PIO. The optionSet attribute is any combination of the character flags:
Cache template |
c |
Generate all after headers |
g |
Generate all |
G |
Initialize state at this page |
i |
Use this page as the start page |
s |
The nextSpec attribute was described under Page Sequencing Options above,above; it may take values as follows:
|
"." |
Repeat the same page |
|
“+” |
Go to the next page as defined in the ASD |
|
"-" |
Go to the previous page as defined in the ASD |
|
"^" |
Go back to the first page of the application |
|
"pageX" |
Go to the named page |
|
*varX |
Go to the page named by variable varX |
|
“cond? page2, page3” |
Ternary conditional, if cond true (“1”, “y”, “Y”), page2, else page3 |
|
"option# page2, page3, page4" |
Jump to a page depending on the integer zero based value of variable "option". |
If no next attribute is provided, Retro will jump to the Exit page specified at application-set scope, or overridden at Application scope. The extraHeader attribute allows for shorthand representation of some common headers as follows:
Content length |
c |
Kill the connection |
k |
Don’t cache this response |
n |
The mimetype attribute value is used in the generation of headers for the page. It defaults to “text/html”.
PIO tag
The PIO tag is optional. It takes the form:
<PIO path=”/path/to/pios/for/this/application”/>
The path provided will be used to load page implementation objects for pages in this application. It may be defined at application-set scope. A definition at application scope overrides any value at application-set scope. If no PIO tag is present, Retro will search for templates according to the directory structure defined under Layout and Structure Conventions above. This is a filesystem path. A path without a leading ‘/’ will be taken as relative to the application set, e.g. path=”path/to/pios” in application-set “trivia” will be taken as “/trivia/path/to/pios.
Template tag
The Template tag is optional. It takes the form:
<Template path=”/path/to/templates/for/this/application”/>
The path provided will be used to load templates for pages
in this application. It may be defined at application-set scope. A definition at application scope
overrides any value at application-set scope. If no Template tag is present, Retro will search for templates
according to the directory structure defined under Layout and Structure
Conventions above.
above. This is a
filesystem path. A
path without a leading ‘/’ will be taken as relative to the application set,
e.g. path=”path/to/template” in application-set “trivia” will be taken as
“/trivia/path/to/template”, or if trivia is an alias, the corresponding
path.
Variable tag
The Variable tag is used to define an application-set, application, or page scope variable. It takes the form:
<Variable name=”varName” value=”varValue”/>
Variables in these scopes live in their own namespaces. Variables at application-set scope are visible to all applications in the set. Variables at application scope are visible only to that application. Variables at page scope are visible only to that page. The namespace of variables defined in the ASD is distinct from the separate namespaces used for cookies and request variables.
XML tag
The XML tag is optional. It takes the form:
<XML path=”/path/to/xml”/>
It is used to specify the path to XML files representing the data to be passed between pages in the applications of a particular application set. By default, Retro will look for these in .../appsetname/xml. The XML files stored in this directory should be named for the applications they relate to – e.g. myapp1.xml. Only applications whose state attribute is set to “hxml” need to have such XML files. This is a filesystem path. A path without a leading ‘/’ will be taken as relative to the application set, e.g. path=”path/to/template” in application-set “trivia” will be taken as “/trivia/path/to/template”, or if trivia is an alias, the corresponding path.
CSP pages provide some of the capabilities associated with ASP or JSP pages. They don't support embedded scripts, but they do allow for generation of HTML of arbitrary complexity. The principle aim of Retro is to avoid slow interpreted code. Because they don’t allow an arbitrary mixture of script and HTML, they are usually easier to read than many ASP or JSP pages.
If your template has an entry of the familiar form:
<%=varname%>
The CSP page interpreter will look up the value of variable varname in its symbol tables and will substitute its value for the pseudo-tag. On the other hand, where an ASP page might have:
<table>
<%
for (var i = 0; i < limit; i++) {
response.write("...");
}
%>
</table>
a CSP page will simply have:
<table>
<%ipname%>
</table>
where ipname is the name of an insertion point. You must provide a block of C++ code in a page-implementation object - PIO - that gets used to satisfy the named insertion point. It can be argued that his rather sparse style makes for better maintainability. At least you know exactly where to look for the code that generates this part of the HTML.
These are objects in the sense of Unix shared objects, or in the Windows version, DLLs. You generate your PIO by adding code to a predefined shared object code fragment that implements one of the following entry points:
|
Preamble |
const char *preamble(Request *, Response *); |
|
Set Cookies |
const char *cookies(Request *, Response *, void *); |
|
Extra Headers |
const char *headers(Request *, Response *, void *); |
|
Insertions |
const char *insertions(Request *, Response *, std::ostream &html, const char *ipname, void *appdata); |
|
Generate |
const char *generate(Request *pr, Response *prs, ostream &html, void *appdata); |
|
Postamble |
const char *postamble(Request *, Response *, void *); |
|
Return Processing |
Const char *returnproc(Request *, Response *); |
These are all optional. If they are to be called, you
indicate that they are needed in the ASD as previously described. They may
return null, or a null terminated C style error string. In the
latter case Retro will eitherstatic error string, or the special constant
PIO_ERR_INDIRECT. A non-null return value will cause Retro either to
generate a simple error HTML page describing which entry point was involved and
what error message it returned. Alternatively it can interpolate the error
message into an error page template that you provide, provided that no response
has yet been flushed to the web server. If response has been flushed, the error
message is simply appended to what has been sent, with a following
“</body></html>”.
You will find more on PIOs, which are the guts of Retro programming, below, in Working with PIOs.
Retro implements Connection, Result, and Row classes as a thin layer over MySQL native APIs. Retro worker threads normally cache connection objects, so they are immediately available in the context of a Retro page submission, and can be directly used to execute SQL queries/commands.
An application-set or application definition may include a MySQL tag to describe its use of one or more databases (MAX_DB_PER_APP is currently defined as 4):
<MySQL host="hostname" dblist="dbname1,prefix*var+suffix" preload=”1” user="username"
epw="a1Bcdef3+yhg" cachecon="y"/>
This specifies that the application will use two databases. The connection to the first is to be established and cached immediately (preload=”1”), and if this is not possible, then it’s a fatal error. The second connection will be established during page processing as soon as the value of var is nonblank, and then cached. It’s the application designer’s responsibility to ensure that the database name can be established before it is actually required.
The resolved form of prefix*var+suffix consists of the concatenation of an optional literal prefix, the value of the nonblank request variable var, and an optional literal suffix. Retro will check if it can construct the name as each page is processed. When it can, it will attempt to establish and cache the connection. You can see an example of this feature in the OpenTS application set. In that case, there is a separate database for the data of each client, and the name of the database can’t be determined until an administrator or a user has logged on.
If you need to use a different user/password combination in some part of the application, establish the connection in your PIO. In this case it won’t be cached. There’s an example of this in the signup application of the OpenTS application set. You can define the user name and encoded password as variables in the ASD.
Retro consists notionally of a dispatcher that is started when the Retro application starts. The dispatcher manages a worker thread pool, allocating each request to the first available thread. From that point on, the chosen worker thread handles the request exclusively, and multi-threading synchronization issues are minimized.
To minimize inefficiencies due to contention for data objects, the worker threads are highly independent, each with its own complete set of data. The only global data items are the read-only application-set definition object, the collection of cached templates, and the log. The former exists before any worker threads start to run, and is read-only. The other two have access controlled by a mutex, so synchronization overhead is minimal.
In the Apache implementation, the server is instructed to deal with files with the .fcgi extension using the FastCGI module. The Apache FastCGI module allows for a dynamic mode, where instances of FastCGI applications are started as required when no existing instance responds in a timely fashion to a connect on its FCGI listen socket. Alternatively you can specify that it should pre-load a number of application instances. Retro is designed to work efficiently in either of these circumstances. It takes a two-tier approach to minimizing any overhead associated with loading of application instances. First it uses a relatively small executable to implement the dispatcher, with the bulk of the framework capabilities provided by shared libraries. The executable simple creates the dispatcher object, which reads the ASD for the application set, then listens for FCGI requests from the web server. The first request for an application from an application set takes the hit of loading the dispatcher and the larger shared libraries. Subsequent requirements to load instances for other application sets need only load another copy of the small executable. Specific pages need only dynamically load the (hopefully small) shared libraries that implement the PIOs. Once the latter are loaded, they are cached.
Each instance of the dispatcher can handle a band of load, corresponding to its thread pool. The FastCGI module can start more dispatchers as required. The Linux/Apache/FastCGI implementation uses POSIX threads - pthreads.
An ISAPI Extension DLL provides the IIS implementation. The web server is set up to associate files with the extension .retro (actually XML) with the ISAPI Extension. The dispatcher is set up by the DLL startup code. The ASD is read for each application set at the time of the first request, for that application-set, and the read-only definition is cached, keyed by application-set name. Basically all the overhead of loading code etc is taken as a hit by the first request for any Retro application page. As in the Apache case, PIOs are loaded on demand, and cached.
The dispatcher listens for requests, and for each request that it accepts, creating a vestigial Request object that owns an instance of the communication mechanism between it and the web server – its channel, and a reference to the application-set object. The Request object is then given to a thread that is awoken to deal with the request. It is this thread (implemented by a WorkerThread object) that is then entirely responsible for processing the request.
The thread has the Request object read from its channel to build its data dictionaries from the HTTP headers and the get/post information. It checks the application and page specification and if necessary loads the required PIO, and creates any required database connections. These may well already be in its thread specific cache.
To actually process the request, it creates a Response object. This provides an HTML output ostream object corresponding the outgoing aspect of the channel, and creates and deals with parsing of the CSP page, error reporting, etc.
Then it calls the specified entry points for the that page, feeding them with pointers/references to the Request object, Response object, HTML output ostream, etc.
The thread then has the Response object open the appropriate template file and process the template, making substitutions, and calling the insertions entry point when named insertion points are found.
Finally, it flushes its output to the server, and then calls the postamble method if one was specified. This gives you an opportunity to clean up any data structures you may have created. Then it deletes its Request and Response objects, and the worker thread goes back to sleep.
Retro has quite flexible logging, error reporting, and debugging capabilities. All of these can be omitted from Retro compilations at compile time, and can also be controlled by the ASD. You can build Retro and its applications without logging when you're confident that they are stable, and this will give you maximum performance. Retro will still send the normal information to the web server log. If it's compiled with logging, Retro can be instructed in its ASD file what level of logging it is to do (error, warning, info, debug), and where it is to put its log files.
Logging is done at the level of the application set. It is capable of displaying its output at a console, or writing it to a file, using a synchronization method appropriate to the environment. In the Apache implementation, the distinction is trivial; you can simply provide a console name as a log path. In the Windows case, a named pipe is used for console output, and a Retro utility is provided to act as the receiving end of log pipes, that can display to a console, or write to a file, or both.
Retro goes to some lengths to maintain a set of viable worker threads. When it is confronted with bugs in it's own code, or in the code of PIOs, or with bad data, or some other catastrophe, it attempts to deal with these errors at two levels:
a) Before the response object has been created
b) After the response object has been created.
Before the creation of the response object, the worker thread is concerned with extracting the CGI variables and the GET/POST variables. At this stage it will respond to exceptions thrown in the C++ code, and to signals it might catch, in the most fail-safe way it can. It does this by telling its channel that it has completed processing the request, and including an error code. This stops the channel mechanism from re-trying a request doomed to failure. The thread then marks itself as dead, and exits. In the course of doing this, it will report any information it has to whatever logging mechanism has been specified. At the next opportunity, the dispatcher will start a new thread in its place.
After the Response object has been created, Retro behaves differently. At that point it will catch exceptions (signals handling remains unchanged), and will use them to compose a message. If no output has been sent to the server, this message will be interpolated into an HTML page based on the error page specified by the ASD, or simply generated by Retro. If output has been sent, and the mime type is text/html, the message will be inserted into the page it has already started to generate. It will emit corresponding log/debug messages at the same time. If the mime type is not text/html, Retro will simply tell the web server that it encountered an internal error.
You'll need to have MySQL, libstdc++v3, libexpat, libz and associated headers installed on your system. The headers and libraries for these should be in a customary place (e.g. /usr/local/lib, /usr/local/include).
To run Retro over Apache, you'll also need to have installed Apache mod_fastcgi, which you can get from www.fastcgi.com.
Build Retro in the usual GNU-oriented way:
1) Un-tar retro_VERSION.tar.gz into some convenient place.
2) In the resulting top-level directory, run ./configure
3) In the top-level directory, run make
4) In the top-level directory, run make install
You might want to vary where things are installed, in which case you can call ./configure with appropriate arguments, e.g.
./configure --prefix=PREFIX
The retro executable will be placed in PREFIX/bin, and the shared library in PREFIX/lib. Default for PREFIX is /usr/local.
During the build, the shared PIO objects (shared libraries) for the Retro examples are retained in suitable directory structures under retro_VERSION. These directory structures are rooted at opents, stress, and trivia respectively. You can use Alias directives in your Apache httpd.conf to point at them to run the examples. These directory structures also contain the .csp files and application description xml files for the example applications. Symbolic links to the RAF executable are created in the raf subdirectory for each of the test applications by make install.
The required Apache configuration can be appended to your httpd.conf file. If you installed Retro in /usr/local/retro_VERSION for example, it can look as follows:
AddType application/x-httpd-fcgi .fcgi
AddHandler fastcgi-script .fcgi
Alias /dbops /usr/local/retro_VERSION/dbops
Alias /imggen /usr/local/retro_VERSION/imggen
Alias /opents /usr/local/retro_VERSION/opents
Alias /stress /usr/local/retro_VERSION/stress
Alias /trivia /usr/local/retro_VERSION/trivia
<Directory /usr/local/retro_VERSION/dbops/bin>
Options ExecCGI FollowSymLinks
</Directory>
<Directory /usr/local/retro_VERSION/imggen/bin>
Options ExecCGI FollowSymLinks
</Directory>
<Directory /usr/local/retro_VERSION/opents/bin>
Options ExecCGI FollowSymLinks
</Directory>
<Directory /usr/local/retro_VERSION/stress/bin>
Options ExecCGI FollowSymLinks
</Directory>
<Directory /usr/local/retro_VERSION/trivia/bin>
Options ExecCGI FollowSymLinks
</Directory>
Once you’ve reached that point, you can sanity check by invoking:
http://retro-host/trivia/raf/trivia.fcgi/echo?thephrase=Hello+World
TBD.
Processing of Retro application pages notionally takes place in seven separate phases:
|
Preamble |
Look up or calculate and set any Request variable that will be needed in the population of the page template that is about to be displayed. |
|
Cookies |
Specify what cookies should be set when HTTP headers are generated. |
|
Extra headers |
Explicitly generate any extra HTTP headers than your application might require. |
|
Headers |
Generate standard headers and append any extra headers and cookies generated above. |
|
Process the template or generate response |
Perform substitutions, and generate HTML on-the-fly at points in the page template where this is required. |
|
Postamble |
Clean up any data structures you created during the above. |
|
Return Processing |
Take whatever actions are necessary to deal with data returned by the submission of the page. |
Your interaction with the Retro framework happens during these phases when Retro calls corresponding methods in your Page Implementation Object. (A shared library/DLL) The methods are as follows.
The methods you may want to provide in a PIO are described in file pio.h.
const char *preamble(Request *pr, Response *prs, void **appdata);
const char *cookies(Request *pr, Response *prs, void *appdata);
const char *headers(Request *pr, Response *prs, ostream &html, void *appdata);
const char *insertions(Request *pr, Response *prs, ostream &html,
const char *ipname, void *appdata);
const char *generate(Request *pr, Response *prs, ostream &out, void *appdata);
const char *postamble(Request *pr, Response *prs, void *appdata);
const char *returnproc(Request *pr, Response *prs);
There are two objects that are directly accessible to all of the methods of PIO objects. One is the Request object, which holds the repository of variables associated with the current request, and provides methods to control the transition to the next page. The other is the Response object, on which cookie and header values can be set, and through which error information may be written.
The headers, insertions and generate methods also receive a reference to an ostream, through which the appropriate information may be written.
These methods should return null if they succeed, or a pointer to an error message on failure. The special value PIO_ERR_INDIRECT can be returned to indicate that a composite error message has been placed in the Response object.
Each of the methods listed above is called by the Retro framework if it is specified in the definition of the page being processed – e.g. phases=”pchiar” in the page definition tag causes all the methods to be called at appropriate points in the processing of the page (the flags don’t need to be in order).
Use the preamble method to do things like examination of cookie values, or the value of specific HTTP headers that may determine the future course of the application, and for recovery of data needed for the next page from the database.
You can also create some sort of application data object in the preamble, as in:
DataObject *pdo = new DataObject(. . .);
*appdata = pdo;
Retro will cache such pointers for you with a key that you
supply if requested, and will appropriately synchronize object cache methods. But beyond that, you are entirely
responsible for lifetime control, concurrency issues, reference counting etc of
such persistent objects. The object cache is just a place to park pointers
between requests.
The cookies method is called before HTTP headers are emitted, thus ensuring that all the information required for generation of headers is available. Use the cookie methods of the Response object to manipulate cookie values.
The headers method is called before HTTP headers are emitted, thus ensuring that all the information required for generation of headers is available. All you need to do at this point is output lines of text to the supplied ostream, e.g.
const char *headers(Request *pr, Response *prs, ostream &exheads,
void *appdata)
{
exheads << “MySpecialHeader: Whatever\n”;
return 0;
}
You must provide an insertions method in your PIO if you use the <%ipname%> construct in the corresponding template, or if the page has the ‘g’ or ‘G’ option specified.
Simply output HTML text to the supplied ostream, e.g.
const char *insertions(Request *pr, Response *prs, ostream &html,
const char *ipname, void *appdata)
{
html << “<table width=600 border=1>\n”;
for (int i = 0; i < 10; i++) {
html << “<tr>\n”;
for (int j = 0; j < 10; j++) {
html << <td> </td>
}
html << “\n</tr>\n”
}
return 0;
}
Hint: Use literal newlines in formatting the HTML (or none at all – the browser doesn’t care).
html << “<table>\n”; // Yes
html << “<table>” << endl; // No
If you use endl, you’ll force the output buffer to be flushed, and then if you subsequently encounter an error condition, the Response object won’t be able to backtrack and create a tidy error page.
Use this method simply to clean up any application data object you created in the preamble method.
Presumably the page submitted data that is of some value. Use the returnproc method to do the right thing with it.
The request object provides access to the variable collection, a hash table in which values derived from HTTP headers, cookies, the ASD, and HTML GET/FORM fields are stored. Retro applications work largely through testing and manipulation of these values, either automatically as the GET/POST is processed, or by the developer.
Different types of variables are segregated in separate namespaces, so that you can have an HTML field name that clashes with a header or CGI variable name, and so on.
// Set the value of a variable - creating the variable if necessary
void set(const string &key, const string &value);
// Get a variable value
const string &value(const string &s) const;
// Get a value as a C style string
const char *str(const string &s) ;
// Get the value of a header/CGI variable – equivalent to value(“HD:”+name);
const string &header(const string &name) const;
// Get the value of a cookie – equivalent to value(“CK:”+name);
const string &cookie(const string &name) const;
// Get the value of an ASD variable
const string &asvar(const string &name) const; // value(“AS:”+name);
const string &appvar(const string &name) const; // value(“AP:”+name);
const string &pagevar(const string &name) const; // value(“PG:”+name);
Note that the result of getting the value of a variable that has never been set is a blank string.
The request object also provides access to the various values and flags specified in the Application Set Definition (ASD), or more specifically to those that relate to the current application/page.
// What application/page are we in?
const string &appName() const;
const string &pageName() const;
// Where can various files be found?
// - the CSP files
const string &templatePath() const;
// - the PIOs
const string &pioPath() const;
// What is the default exit page for the application?
const string &exitPage() const;
// Does the application use XML to maintain state, and if so, how?
bool useXML() const;
bool useHiddenXML();
bool useCookieXML();
// Where is the initial XML file
const string &xmlSrc() const;
// What variable name holds the state XML
const string &xmlVar() const;
// What phases will the current page call methods from the PIO for, and
// what page options are set
unsigned long phases() const;
unsigned long options() const;
The following methods deal with situations where your page processing (often the preamble or the returnproc) must override the next page specified in the ASD. Such an override can be:
// Override the next page in the ASD
void setNextPage(const string &np);
// Specify an alternate template file with name other than the page name
void setNextTemplate(const string &nt);
// Force a redirect after preamble, cookies, and headers phases
void setRedirect(const string &dest);
// Check what we set the redirect to
const string &redirectTo() const;
The following methods provide access to any state XML. You can get a pointer to the top-level SimpleXMLDomElement, and extract the XML in its current form.
// Access to the XML DOM object
SimpleXMLDOMElement *XMLTop();
// Get the current XML from the DOM
string packedXML();
The remaining methods deal with the applications relationship with the MySQL database
// Info about the applications use of the database
bool useDB() const;
const string &dbHost() const;
const string &dbDB(int index) const;
int dbCount() const;
const string &dbUser() const;
const string &dbEpw() const;
// Get the cached database connection(s)
MySQLConnection *getDBCon() const; // gets database 0
MySQLConnection *getDBCon(int which) const;
The Response object provides the primary functions of transmitting the generated page or errors arising during its processing back to the web server, and hence to the browser.
It also provides the capability to set cookies and extra headers.
// The primary output
ostream &html();
// The error stream - to the web-server log
ostream &errors();
// An ostream to marshal error messages
ostream &errStream();
// Set a cookie, optionally using explicit time string like
// Sat, 22-Apr-2000 00:00:00 GMT
void setCookie(const string &name, const string &value,
const string &expire = emptyString,
const string &path = emptyString,
const string &domain = emptyString,
bool bsecure = false);
// Set a cookie using a time difference like TimeDiff(30, TD_DAYS)
void setCookie(const string &name, const string &value, TimeDiff td,
const string &path = emptyString,
const string &domain = emptyString,
bool bsecure = false);
void setSessionCookie(const string &name, const string &value);
// Delete a cookie at the browser (set it to a time in the past)
void deleteCookie(const string &name);
// This is probably not required in any of
the PIO phases, but it tells where
// the response object thinks it is in terms
of processing stages (see
// enum CSP_state in response.h
int getState()
const;
Errors are reported by PIO methods in one of two ways. If your error message is a simple static string, simply return a pointer to the error string from the method.
Alternatively, if your error message must be constructed from transient data, send it to the ostream provided by the Response object errStream() method, and return the value PIO_ERR_INDIRECT which is defined in response.h.
So, if it’s just a plain old constant string:
If (stupidinput)
return “That input was stupid – pull yourself together”;
But if you want to construct something more complex:
If (requested > allowed) {
prs->errStream() << “You asked for “ << requested
<< “, but we only have “ << allowed;
return PIO_ERR_INDIRECT;
}
There is no significant penalty for using the second form consistently. Either way, Retro will wrap the error message with text stating which PIO method originated the error.
A non-zero return from any of the phase methods will terminate processing of the request. If no output has been flushed to the web server (all output so far has been buffered), the output is simply thrown away, and an error page is emitted. If your ASD defines an error page for the application, this is used to construct the error page. It should be of the form:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
HTML 4.0 Transitional//EN">
<html>
<head>
<style>
<!-- Your styles here -->
</style>
</head>
<body>
<!-- Your content here -->
<%errmsg%>
<!—The rest of your content here -->
</body>
</html>
Lines should be restricted to 1024 characters. Longer lines won’t crash anything, but may cause the error message to be omitted. The line with text “<%errmsg%>” should contain just that, though leading whitespace and trailing garbage will be ignored. Retro’s error message is substituted for that line.
Otherwise the emitted error page is:
<html><body>
error_message
</body></html>
If output has been flushed to the web server, and the content type is text/html, then:
error_message
</body></html>
is simply sent after whatever has already been flushed. The effect may be somewhat unpredictable, but any problem information is usually better than none.
There's a PIO outline file in the Retro top-level directory, which is reproduced here:
#include <request.h>
#include <response.h>
#include <retrostring.h>
// During debugging!!
#define LOG 1
#include <util.h>
#include <pio.h>
// If you're using the database
#include <connection.h>
#include <result.h>
#include <row.h>
/*****************************************************************************
Use this entry point to prepare for display of the next page
Typically:
1) Perform any database operations necessary to display the next page
2) Set the values of variables required for substitutions when the template
is processed
2) Set up any data structures you'll need to perform insertions when the
template is processed
******************************************************************************/
const char *preamble(Request *pr, Response *prs, void **appdata)
{
*appdata = new MyDataObject(...);
return 0;
}
/*****************************************************************************
Use the methods of the Response object to set any required cookie values.
******************************************************************************/
const char *cookies(Request *pr, Response *prs, void *appdata)
{
return 0;
}
/****************************************************************************
Explicitly generate any further HTML headers that your application requires.
Retro will add the final extra newline.
******************************************************************************/
const char *headers(Request *pr, Response *prs, ostream &html, void *appdata)
{
html << "MyHeader: Whatever\n";
return 0;
}
/*****************************************************************************
Use this entry point to provide for points in your template where you have
<%insertion-point-name%>
The name of the insertion point is passed as an argument to the entry point call.
******************************************************************************/
const char *insertions(Request *pr, Response *prs, ostream &html, const char *ipname, void *appdata)
{
if (!strcmp(ipname, "my-ip1")) {
html << "Stuff for insertion point my-ip1";
} else if (!strcmp(ipname, "my-1p2")) {
// and so forth
} else
return "Unrecognized insertion point name";
return 0;
}
/*****************************************************************************
Clean up after yourself.
******************************************************************************/
const char *postamble(Request *pr, Response *prs, void *appdata)
{
if (appdata)
delete (MyDatObject *) appdata;
return 0;
}
/*****************************************************************************
Use this entry point to process data submitted by the page. (When you use a URI
like http://www.retroserver.com/appset/raf/apset.fcgi/app1, this does not get called.
Typically:
1) Recover the relevant parts of the data and do database updates.
2) If something went wrong, force a return to the same page
(pr->setNextPage("whatever"))
******************************************************************************/
const char *returnproc(Request *pr, Response *pi)
{
return 0;
}
You can use the Makefile.in from the trivia_pio example directory as a template for your Makefile.in. All you should need to change are the couple of lines at the top:
# These will vary from application to application
APPROOT = myapp
PIOS = page1.so page2.so
This is not quite ‘hello world’, but it can be if you like. It’s going to be invoked by a URL like:
http://retro/trivia/bin/trivia.fcgi/echo?thephrase=Hello+World
Here “retro” is a placeholder for the machine that’s hosting the web server. We’re going to be working in /home/me/retro/trivia, and we’ll alias the path /trivia to this actual path in Apache’s httpd.conf. So we’ll just refer to it as /trivia.
It’s a Retro application so it must have an ASD (Yes, it’s something of an overhead for a big complex application like this, but there you go.) The ASD for our set of trivial applications lives in /trivia/trivia.retro, and for the moment is:
<?xml version="1.0"?>
<ApplicationSet name=”trivia” verbosity="debug" logpath=”/dev/pts/1”>
<Application name="echo">
<Page name="doit" options=”c” next=""/>
</Application>
</ApplicationSet>
The required directory and file structure (files in italics) is:
/trivia
trivia.retro
/trivia/bin
trivia.fcgi (a symbolic link to /usr/local/bin/raf.fcgi)
/trivia/echo
/trivia/echo/template
doit.csp
No PIO phases are specified, so we won’t need a PIO or a directory to contain it. All we need is a template – doit.csp (templates are named for pages):
<html>
<body>
<h2><%=thephrase%></h2>
</body>
</html>
In /trivia/bin, create a symbolic link called trivia.fcgi to /usr/local/bin/raf.fcgi – making sure that it is executable by Apache. At this stage, also make sure you’ve got libretro.so in /usr/local/lib, or wherever it should go on your system. Make install should have taken care of that. You also need to have configured Apache to understand FastCGI.
Now open another shell session, and run the tty command to find out which one it is. Our ASD assumes it’s /dev/pts/1. Change the ASD if it’s something else. Assuming it’s /dev/pts/1, make it writeable using:
chmod 0666 /dev/pts/1
Invoke the application using the URL we set out at the beginning of this section. You’ll notice that the first time, the performance is awful. To display your small piece of text, you had to load a quite large shared object, wait for Retro to parse the ASD and create its worker threads, and parse and cache the page template (page definition said options=”c”). After that, however, if you send the same request again the response should be pretty quick. You should also see some log output to the new shell window that you opened.
The next application requires less effort, since we’ve done most of the groundwork. It will be part of the trivia application-set, so we’ll use the same ASD, and just add an application:
<Application name="echoupr">
<Page name="doit" phases=”p” options=”c” next=""/>
</Application>
It can use the same template file, but this time we’ve specified a preamble, so we will need a PIO. Our directory/file structure will have to grow to:
/trivia
trivia.retro
/trivia/bin
trivia.fcgi
/trivia/echo
/trivia/echo/template
doit.csp
/trivia/echoupr
/trivia/echoupr/template
doit.csp
/trivia/echoupr/pio
doit.so
The PIO in this case is pretty minimal. We’ll just modify the single value we got from the query string. The rest of the stuff from the outline PIO has been omitted for clarity.
#include <request.h>
#include <response.h>
#include <retrostring.h>
#define LOG 1
#include <util.h>
const char *preamble(Request *pr, Response *prs, void **appdata)
{
string s(pr->value(“thephrase”));
dd(DBG, “We executed the preamble, with value: ” << s)
pr->set(“thephrase”, toUpper(s));
return 0;
}
This contains a log macro at DGB verbosity just to illustrate how to use them. The second argument to the macro can be an arbitrary sequence of values to send to that log entry though an ostream.
All we have to do here is to make sure that our input variable is converted to upper case before the template gets interpreted – impressive, uh? Note that retrostring.h defines a number of useful methods that take a const string& (std::basic_string<char>) argument, such as toUpper().
Build the shared object, then invoke
http://retro/trivia/bin/trivia.fcgi/echoupr?thephrase=Hello+world
You should see the upper-case result in the browser, and the debug message in console /dev/pts/1.
This example is also a part of the trivia application set:
<Application name="error">
<Error
path="errpage.html"/>
<Page name="fail"
phases="p" next="+"/>
<Page name="crash"
phases="p" next=""/>
</Application>
We don’t need a template for this example, so the directory structure becomes:
/trivia
trivia.retro
/trivia/bin
trivia.fcgi
/trivia/echo
/trivia/echo/template
doit.csp
/trivia/echoupr
/trivia/echoupr/template
doit.csp
/trivia/echoupr/pio
doit.so
/trivia/error
/trivia/error/template
/trivia/error/pio
doit.so
We need two PIOs, one for the “fail” page, and one for the “crash” page. They both provide just the preamble method – fail has:
const char *preamble(Request *pr, Response *prs,
void **appdata)
{
return "Something went badly wrong!";
}
crash has:
const char
*preamble(Request *pr, Response *prs, void **appdata)
{
string s(pr->value("action"));
dd(DBG, s)
if (s == "throw")
throw
"Something went badly wrong!";
else {
// provoke a
memory access violation
char *p = (char
*) 1;
*p = 0;
}
return 0;
}
The business of selecting what happens is
actually handled by the error page. The significant bit of this is:
<h2>This is the error page for the
/trivia/error example</h2>
<%errmsg%>
<p>
<form name=f0
action="/trivia/bin/trivia.fcgi/error/fail" method=get>
<input type=hidden name=action
value="throw">
<input
onclick="document.f0.action.value='throw'" type=submit
value=" Throw ">
<input
onclick="document.f0.action.value='crash'" type=submit
value=" Crash ">
</form>
This contains the usual interpolation point for the error message, but it goes on to offer two submit buttons that allow us to continue to other types of error. The “fail” page preamble simply reports an error in the way it should. This causes the error page to be displayed with the wrapped message:
Retro error:
The preamble method of the error.fail application object returned the
error message:
Something went badly wrong!
If you click the “Throw” button displayed by this page, then the preamble of the “crash” page will throw the same message. Retro will catch this, and again invoke the error page. This time though it can’t wrap it with context information, and you’ll just see:
Retro error:
Something went badly wrong!
Other types of exception that Retro might catch will be reported in the same way with as much information as can be gleaned – experiment with the preamble method to investigate these behaviors.
If you click the “Crash” button, the preamble method will provoke a memory access violation. Retro will catch the signal, and will attempt to emit a log record noting it. The browser will receive a 500 Internal Error response from the web server. It isn’t reasonable after we’ve caught a signal to assume we can press on and send error HTML.
So far we’ve just used simple substitutions into a template file. However in many cases it will be necessary to generate content that is more flexible and dynamic. In these cases, you’ll probably want to generate HTML on the fly, and use insertions. The next application will generate an arbitrary table of m rows and n columns.
We’ll ad this as part of our trivia application set, so once again we have a good start. Add the new application:
<Application name="mntab">
<Page name="render" phases=”i” next=""/>
</Application>
Expand the directory/file structure to:
/trivia
trivia.retro
/trivia/bin
trivia.fcgi
/trivia/echo
/trivia/echo/template
doit.csp
/trivia/echoupr
/trivia/echoupr/template
doit.csp
/trivia/echoupr/pio
doit.so
/trivia/mntab
/trivia/mntab/template
render.csp
/trivia/mntab/pio
render.so
creating the template /trivia/template/render.csp:
<html>
<body>
<center>
<h3>Enter a query string like: http://retro/trivia/bin/trivia/fcgi/mntab?rows=20&cols=10</h3>
<table border=1 width=800>
<%trows%>
</table>
</center>
</body>
</html>
This calls for an insertion, the insertion point name being “trows”, so the PIO in this case has a little more to do:
#include <stlib.h>
#include <request.h>
#include <response.h>
#include <retrostring.h>
#include <pio.h>
#define LOG 1
#include <util.h>
const char *insertions(Request *pr, Response *prs, ostream &html,
const char *ipname, void *appdata)
{
if (strcmp(ipname, "trows")) {
prs->errStream() << “Unknown insertion point: “ << ipname;
return PIO_ERR_INDIRECT;
}
int m = atoi(pr->str(“rows”));
int n = atoi(pr->str(“cols”));
int k = 0;
for (int i = 0; i < rows; I++) {
html << “ <tr>” << endl;
for (int j = 0; j < cols; j++) {
html << “ <td> ” << k++ << “ </td>” << endl;
}
html << “ </tr>” << endl;
}
return 0;
}
The URL to invoke the mntab application is of the form:
http://retro/trivia/raf/trivia.fcgi/mntab?rows=20cols=10
Let’s say we’ve decided that if a user invokes the mnrows application with no query string, she should get the same matrix as the last time she used it, or a default 16x16 matrix if it has never been used. The PIO code becomes:
#include <stlib.h>
#include <sstream>
#include <request.h>
#include <response.h>
#include <retrostring.h>
#include <pio.h>
#define LOG 1
#include <util.h>
const char *cookies(Request *pr, Response *prs, void *appdata)
{
string rs(pr->value("rows"));
string cs(pr->value("cols"));
std::stringstream cvs;
if (rs.length() && cs.length())
cvs << rs << ',' << cs;
else
cvs << "16,16";
// create the cookie, or extend its lifetime
prs->setCookie("mnrows_cookie", cvs.str(), TimeDiff(1, TD_YEARS));
return 0;
}
const char *insertions(Request *pr, ostream &html,
const char *ipname, void *appdata)
{
if (strcmp(ipname, "trows")) {
prs->errStream() << “Unknown insertion point: “ << ipname;
return PIO_ERR_INDIRECT;
}
int m = 16;
int n = 16;
string rs(pr->value("rows"));
string cs(pr->value("cols"));
string ss(pr->cookie("mnrows_cookie"));
if (rs.empty() || cs.empty()) {
if (ss.length()) {
std::stringstream cvs(ss);
cvs >> m;
cvs.get();
cvs >> n;
}
} else {
m = atoi(rs.c_str());
n = atoi(cs.c_str());
}
int k = 0;
for (int i = 0; i < rows; i++) {
html << “ <tr>” << endl;
for (int j = 0; j < cols; j++) {
html << “ <td> ” << k++ << “ </td>” << endl;
}
html << “ </tr>” << endl;
}
return 0;
}
The cookie get and set functions are in request.h and response.h respectively.
The next test application was created so I could generate load, and generally stress Retro. It does this by the simple subterfuge of having a body.onLoad handler that immediately submits the page as soon as the browser has loaded it. This can generate some pretty savage thrashing.
The directory/file structure is:
/stress
stress.retro
/stress/bin
stress.fcgi
/stress/minimal
/stress/minimal/pio
count.so
/stress/minimal/template
count.csp
For this purpose, we’ll create a separate application-set (stress.retro), defined initially as follows:
<?xml
version="1.0"?>
<ApplicationSet
name=”stress” verbosity="error" logpath="/dev/pts/1">
<Application name="minimal">
<Variable
name="maxiter" value="100000"/>
<Page
name="it" phases="p" options="c"
next="."/>
</Application>
</ApplicationSet>
Note that we have the verbosity set to error. Otherwise we could flood the log file/console with a load of output which would partly negate the purpose of the test. We’ll actually create this application so that we can test with an interpreted template, a cached template, and with a completely generated response. The template is as follows:
<html>
<head>
<script>
function OnLoad()
{
document.f0.submit();
}
</script>
</head>
<body onLoad="OnLoad()">
<form name=f0
action="/stress/bin/stress.fcgi/minimal/it" method=POST>
<input type=hidden name=iterations
value=<%=iterations%>>
</form>
<h2><%=iterations%></h2>
</body>
</html>
The PIO is dual purpose, to deal with the cases when we use a template, and when we generate the entire response:
#include <stdio.h>
#include <time.h>
#include <request.h>
#include <response.h>
#include <pio.h>
#define LOG 1
#include <util.h>
// This is for the template version
const char *preamble(Request *pr, Response *prs,
void **appdata)
{
char buf[20];
int ni = atoi(pr->value("iterations").c_str());
sprintf(buf, "%d", ni+1);
pr->set("iterations", buf);
return 0;
}
// This is to generate the complete page
const char *generate(Request *pr, Response *prs,
ostream &out, void *appdata)
{
int ni = atoi(pr->value("iterations").c_str()) + 1;
out << "Content-type: text/html\n\n";
out << "<html><head><script>function OnLoad()
{ document.f0.submit(); }“;
out <<
“</script></head><body onLoad=\"OnLoad()\">";
out << "<form name=f0
action=\"/stress/bin/stress.fcgi/minimal/it\" ”;
out << “method=POST>";
out << "<input type=hidden name=iterations value="
<< ni <<
"</form><h2>"
<< ni << "</h2></body></html>";
return 0;
}
If you run this application in three or four instances of the browser on a couple of machines, you can give Retro, and the web server that spawns it a pretty good beating. To complete the investigation, change the ASD as follows to generate the output:
<?xml
version="1.0"?>
<ApplicationSet
name=”stress” verbosity="error" logpath="/dev/pts/1">
<Application
name="minimal">
<Variable
name="maxiter" value="100000"/>
<Page name="it"
options="G" next="."/>
</Application>
</ApplicationSet>
We’ve removed the phases attribute from the count page, and changed the options to “G” – generate everything. It would be possible to go a little further by modifying the PIO to double buffer the generated HTML, and thus determine its size, and then to emit a Content-length header before content type, followed by the buffered HTML.
The next example uses a MySQL database, and also illustrates how you can maintain application state using XML data islands. Once again, it’s a single-application, single-page application-set, defined as follows:
<?xml version="1.0"?>
<ApplicationSet name="dbops"
verbosity="debug" logpath="/dev/pts/1">
<MySQL host="localhost" dblist="retro"
user="retro" preload="1"
epw="ZJMeQhB3AcQ="/>
<Variable name="organization" value="ACME"/>
<Application name="members" state="hxml">
<Variable name="title"
value="DBOPS"/>
<Exit
path="/dbops/members/thanks.html"/>
<Error
path="error.html"/>
<Page name="admin"
phases="rp" options="is" next=".">
<Variable
name="element" value="Membership Admin"/>
</Page>
</Application>
<XML path="xml"/>
</ApplicationSet>
You will need to alter the MySQL tag, at the least to set the encoded password of you choice.
The template creates a simple HTML form, with multiple submit buttons that, when clicked, set a hidden field to note what kind of action is required. Other than that, they submit to the same URL, embodying the application and page name as usual. The new elements in this example concern handling of XML data that maintains application state. The relevant parts are:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML
4.0 Transitional//EN">
<html>
<!-- Customary stuff here -->
<script
src="/dbops/simplexmlom.js">
</script>
<script>
var xmldata;
function OnLoad()
{
xmldata = new xml_om(document.f1.__retro_xml.value);
var msg = xmldata.getValue("error.msg");
xmldata.toform(document.f0);
if (msg.length)
alert(msg);
}
function OnSubmit()
{
//check values here
//if (nogood)
// return false;
xmldata.fromform(document.f0);
document.f1.__retro_xml.value = xmldata.xml();
return true;
}
</script>
</head>
<body onLoad="OnLoad()">
<center>
<h1><%=AS::organization%>
<%=AP::title%> Example - <%=PG::element%></h1>
<form name=f0 action=""
method=get>
<!-- HTML form here -->
</form>
<form onSubmit="return OnSubmit()"
name=f1 action="http://psyche/dbops/bin/dbops.fcgi/members/admin"
method=post>
<input type=hidden name=__retro_xml
value='<%=__retro_xml%>'>
<input type=hidden name=action
value="u">
<input
onClick='document.f1.action.value="q"' type=submit value=" Query ">
<input
onClick='document.f1.action.value="u"' type=submit
value=" Update ">
<input
onClick='document.f1.action.value="i"' type=submit
value=" Insert ">
<input
onClick='document.f1.action.value="x"' type=submit
value="
Quit ">
</form>
<i>Page generated at:
<%=timenow%></i>
</body>
</html>
The first thing to note is that there are two script elements. The first downloads some Javascript from the server. This file, a Retro component – simplexmlom.js – implements a Javascript object that provides a simple XML parser and object model. This can be used with any browser that supports reasonably current Javascript to ensure portability of the XML data islands approach. The second script block uses methods of the object to create the XML object model from XML in the hidden variable __retro_so in form f1, populate the fields of form f0 with data, and later, to recover the data from form f0, and place it back in the hidden field in form f1. Another minor feature is that the OnLoad() function checks the value of one of the XML attributes, and if it is non-blank, will display an error message as soon as the page has been loaded.
Note the dual occurrence of “__retro_xml” in the line defining the hidden field:
<input type=hidden name=__retro_xml
value='<%=__retro_xml%>'>
This is essential to the working of Retro’s application-state mechanism. The other hidden field records which submit button was pressed, so that we know what is to be done for any particular submission in the PIO.
The XML used by this application is as follows:
<?xml version="1.0"?>
<member_data>
<meta author="Steve Teale" date="2003/7/6"/>
<address last=""
first="" mi="" address1="" address2=""
city=""
state="" zip=""/>
<info email="" visits="0"
comment=""/>
<error msg=""/>
</member_data>
In the PIO, the operations of interest are as follows. First we must pick up the database connection that Retro has created for us as specified in the ASD. Then we get a pointer to the SimpleXMLDOMDocument object also created for us by Retro from the contents of the __retro_xml variable. We use the information from the DOM to build an SQL query:
MySQLConnection *pcon = pr->getDBCon();
SimpleXMLDOMDocument *pxml = pr->XMLDoc();
SimpleXMLDOMElement *pm = pxml->getChild("address");
if (!pm)
return "Bad XML data - no
address";
SimpleXMLDOMElement *pinfo = pxml->getChild("info");
if (!pinfo)
return "Bad XML data - no
info";
pxml->setValue("error.msg", "");
std::stringstream ss;
if (op == 'u') {
ss << "update members set
first=\'"
<<
sqlEncode(pm->attrib("first")) << "\', mi=\'"
<<
sqlEncode(pm->attrib("mi")) << "\', address1=\'"
The sqlEncode() method performs the customary doubling of any single quotes in the attribute values, so that they are properly interpteted in the generated SQL. Then we perform the query, and poke any error message resulting from this process into the XML. Note that the MySQL API wrapper object methods throw a const char * exception if they encounter a problem.
try {
Result res =
pcon->Query(ss);
if
(pcon->GetAffected() != 1) {
// Report the outcome in the XML
pxml->setValue("error.msg",
"Failed to update member record in database");
}
}
catch (const char *err) {
pxml->setValue("error.msg",
err);
}
The Javascript object implemented by simplexmlom.js provides the following methods:
|
Method |
Description |
|
xml_om(src) |
Constructor - creates an xml_om object from a string containing XML, as in var om = new xml_om(); |
|
om.xml() |
Returns a string comprising the XML currently represented by the object. |
|
om.getValue(path) |
Returns a string, which is the value of the attribute or tag represented by the string path, or null on failure. Path is a jointed string of the form: “Tag.tagchild1.tagchild2.attribute” Or: “Tag.tagchild1.tagchild2.” In the former case, getValue will return the value of an attribute, in the latter case, the text within the most deeply nested tag, less leading and trailing whitespace. See notes about arrays below. |
|
om.getAttributes(path) |
Returns an array of strings comprising the attribute values of the element represented by the path, as in “tagtop.tagchild1.tagchild2”. The strings are in the same order as the attributes were defined in the XML. Returns null on failure. |
|
om.setValue(path, v) |
Set the value of the attribute or tag represented by path (as in getValue()) to the string corresponding to v. Returns true on success, false otherwise. |
|
om.setAttributes(path, a) |
Sets the existing attributes of the element represented by path to the first n values of array a, converted to strings, where n is the number of attributes on the element. Returns true on success, false otherwise. |
|
om.cloneArrayElement(path) |
Creates a new element identical to the one specified by path. The order of insertion is not specified or implied, since the resulting elements are identical. Returns true on success, false otherwise. |
|
om.deleteArrayElement(path) |
Deletes the element (part of an array) designated by path. Returns true on success, false otherwise. |
|
om.listLength(path) |
Returns the number of elements that are the immediate children of the element represented by path, or –1 if the path is not found. |
|
om.addElement(path, name, after) |
Adds a new element to the model within the element represented by path, and following the child named by after. If after is null or blank, the new element is inserted before any existing elements. If path is blank or “.” This method inserts an element into the top-level element. Returns true on success, false otherwise. |
|
om.addAttribute(path, name, value) |
Adds an attribute to the element corresponding to path, at the end of the current list of attributes. If the path is blank or “.” The attribute is added to the outermost element. If the attribute name is null or blank, adds element text to the specified element. Returns true on success, false otherwise. |
|
om.xmlName() |
Returns the tag name of the outermost tag. |
|
om.ok() |
Returns true if the model is in a good state, false otherwise. |
|
om.toForm(f, ep) |
Populates the fields of the HTML form represented by f. The field names should represent paths as described for setValue(). The optional argument ep specifies an exclusion prefix. Fields with names prefixed by ep are ignored in this process. The default exclusion prefix is “__” (double underscore). |
|
om.fromForm(f, ep) |
Takes values from the HTML form represented by f, and populates them into the model. The field names should represent paths as described for setValue().The optional argument ep specifies an exclusion prefix. Fields with names prefixed by ep are ignored in this process. The default exclusion prefix is “__” (double underscore). |
The XML Object model is simple in the sense that it knows nothing about CDATA, namespaces, entity references, processing instructions, and so on. It understands elements and attributes, and simple text as the body of an element. It does however recognize the special case of elements that contain a number of children which all have the same tag name, i.e. elements that represent arrays. However, there must be no elements of different name within the same containing element. To understand this, let’s use an example:
<?xml version="1.0"?>
<outer attr1="value1"
attr2="value2">
<child1
attr1="value1" attr2="value2">
The grass is greener
</child1>
<child2 attr1="Is an array
holder">
<elem
attr1="value1" attr2="value2"/>
<elem
attr1="value1" attr2="value2"/>
</child2>
<child3 attr1="value1" attr2="value2"/>
<child4 attr1=”value1”>
<inner1
attr1=”value1” attr2=”value2”/>
<inner2
attr1=”value1” attr2=”value2”/>
</child4>
</outer>
Here child1 is an element
with some body text, and some attributes. To get the value of attr1, we would
use a path “child1.attr1”, to get the value of attr2 we would use path
“child1.attr2”. To get the value of the body text (less the extraneous
whitespace that come before and after), we would use the path “child1.” - essentially signifying the nameless
attribute. The jointed paths consist of a number of tag names followed by one
attribute name. Methods that relate only to elements take a path that consists
only of tag names. The outermost tag name (“outer” in this case) is never used
in paths.
Child2 is a two-element
array holder. It can still have attributes, and these are accessed in the same
way as those of child1. To refer to attributes of the array elements, we would
use path “child2.elem[0].attr1” for example. The child elements can have structures
nested under them, and the model doesn’t care if they have identical
structures, so you can have arrays of dissimilar objects, as long as their
outer name is the same, and as long as you’re certain you know what you’re
doing (it’s not recommended). To preserve simplicity in the Javascript object,
arrays must have an array holder. (This can be the XML outer element, but then
your data structure at the top level can only be an array, with no elements of
other names.)
Child3 is similar to
child1, except that it doesn’t have any body text. Child4 has contained
elements (with paths “child4.inner1”, and child4.inner2”). The attributes of
inner1 are reached via paths such as “child4.inner1.attr1”.
If the name of the outer
tag is of interest, the object’s xmlName() method gives access to it.
There is a test script –
jsomtest.js – in the retro2 top-level directory that illustrates the use of the
methods except for toForm() and fromForm(). These are illustrated on the
dbops.members example.
It may be useful to know
that the XML generated by Retro encodes attribute values so that for instance,
the following transformations occur:
O’Flannagan => O'Flannagan
This is necessary because
the XML can contain no instances of the single quote character. Remember we
include the XML in the hidden field using single quotes:
Value=’<%=__retro_xml%>’
The Javascript object
decodes attributes automatically. However there is no corresponding encoding
when XML is generated from the object model. This isn’t necessary because we
don’t have to express that value in the HTML.
The SimpleXMLDOMElement
class provided by the Retro API provides the following methods (the header
files in retro_so are of course definitive):
|
Method |
Description |
|
void setText(const string
&s); |
Sets the body text of
the element. |
|
void streamout(ostream &,
int indent = 0) const; |
Formats the XML
represented by the element into an ostream. |
|
void packXML(ostream &); |
Formats the XML
represented by the element into packed XML (no extraneous whitespace). |
|
const string &tagName()
const; |
Returns the tag name of
the element. |
|
int attribCount() const; |
Returns the number of
attributes on the element. |
|
const anvpair *attributes()
const; |
Returns an array of
attribute name value pairs – Struct anvpair { string
m_name; string m_value; }; |
|
const string &attrib( const string &atname,
bool *pb = 0) const; |
Returns the value of
attribute atname. Returns the empty string if atname doesn’t exist. If pb
points to a bool value, this will receive false if the attribute doesn’t
exist. |
|
int childCount() const |
Returns the number of
elements that are immediate children of this element. |
|
SimpleXMLDOMElement ** children() const; |
Returns an array of
pointers to the children of this element, or null if there are no children. |
|
SimpleXMLDOMElement * getChild(const string &path)
const; |
Returns a pointer to a
single child element by jointed name. |
The SimpleXMLDOMDocument
class is derived from SimpleXMLDOMElement, and provides the following methods:
|
Method |
Description |
|
bool good() const; |
Returns true if the document object is valid. |
|
Bool insertElement( const string &path, const string &name, const string &after, const char **attributes); |
Inserts a child element into the element specified by the jointed name path (“” or “.” For the top level). The inserted element will have tag name “name”, and will be inserted after the element by “after”. If after is blank, the new element will become the first element. The list off attributes consists of alternating name and value strings, terminated by a null. |
|
bool deleteElement( const string &path); |
Deletes the element specified by the jointed name path. |
|
bool duplicateElement( const string &path); |
Duplicates the element specified by the jointed name path. |
|
bool addAttributes( const string &path, const char ** attribs); |
Adds attributes to the element specified by the jointed name path. The list off attributes consists of alternating name and value strings, terminated by a null. |
|
bool setValue( const string &path, const string &value); |
Sets the value of an attribute described by the jointed name path to value. If there is an empty last joint, as in “a.b.c.” the element text is set. |
|
bool setValue( const char *path, const char *value); |
Sets the value of an attribute described by the jointed name path to value. If there is an empty last joint, as in “a.b.c.” the element text is set. |
|
const string &getValue( const string &path, bool *pbfound = 0); |
Gets the value of an attribute described by the jointed name path. If there is an empty last joint, as in “a.b.c.” the element text is retrieved. If the attribute doesn’t exist, an empty string is returned. You can find out by passing a valid bool pointer. |
|
const string &getValue( const char *path, bool *pbfound); |
Gets the value of an attribute described by the jointed name path. If there is an empty last joint, as in “a.b.c.” the element text is retrieved. If the attribute doesn’t exist, an empty string is returned. You can find out by passing a valid bool pointer. |
|
string packedXML(); |
Returns a string comprising the XML that represents the current state of the document, with encoding suitable for use in HTML. |
|
bool readXML(const char *file); |
Reads an XML file and populates the document. |
|
bool loadXML(const char *src); |
Loads XML from a string and populates the document |
The methods provided by these classes are close to those provided by the Javascript model. Where there are differences these are generally aimed at better performance, e.g. setting of multiple attributes when an element is inserted.
The use of some of these methods is illustrated by the trivia/makexml example. This example also shows shows how to use Retro to produce web content that is of mime type other than text/html.
The example adds another application, and an XML path to the trivia ASD:
<?xml version="1.0"?>
<ApplicationSet name=”trivia”
verbosity="debug" logpath="/dev/pts/1">
...
<Application name="makexml" state="hxml">
<Page name="makexml"
options="sig" next="" xheads="c"
mimetype="text/xml"/>
</Application>
<XML path="xml"/>
</ApplicationSet>
The single page of this application specifies that it is the start page, it is to initialize “hxml” state (though we won’t actually use this), and that it will generate everything except the headers. It also stipulates that a content-length header is to be emitted, and that the type of the generated response is to be “text/xml”. I’m assuming you’ll look at the response using a browser that understands XML. Because we are to generate the output (the XML we used as an example for the Javascript XML model), we won’t need a template. However, a PIO implementing the generate() entry point is required. Here’s its definition:
const char *generate(Request *pr, Response *prs,
ostream &out, void *appdata)
{
const char *al[] = { "attr1", "value1",
"attr2", "value2", 0 };
SimpleXMLDOMDocument *pxml = pr->XMLDoc();
pxml->addAttributes("", al);
pxml->insertElement("", "child3", "",
al);
pxml->insertElement("", "child2", "",
al);
pxml->insertElement("", "child1", "",
al);
pxml->insertElement("", "child4",
"child3", al);
pxml->insertElement("child2", "elem",
"", al);
pxml->insertElement("child2", "elem",
"", al);
pxml->insertElement("child4", "inner2",
"", al);
pxml->insertElement("child4", "inner1",
"", al);
SimpleXMLDOMElement *pe = pxml->getChild("child3");
pe->setText("The grass is greener");
pxml->setValue(“child4.inner2.attr2”, “something else”);
out << pxml->packedXML() << '\n';
return 0;
}
The XML provided for initialization of state is:
<?xml version="1.0"?>
<outer/>
The example cheats for the sake of simplicity by having identical attribute names and values for all the element. It will be left as an exercise for the reader to produce a more interesting example.
First we add attributes to the “outer” tag, which we can address by the path “” or “.”.
Then we insert three elements into the outer tag, with no ‘after’ specification, so they each become the first element in turn, hence we add them in reverse order. We then insert child4 after child3.
The remaining insertions add children to the elements we just added. Then we get a pointer to child3 by name, and set its body text, and we change the value of an attribute on one of the innermost elements.
In the simplest use of Retro, the only response header generated is:
Content-type: text/html
This is not to say that this header is the only one the client will receive. The web server may choose to add headers to what retro has provided.
Retro provides two methods for adding further response headers. A page specification in the ASD can specify xheads options:
c - add a content-length header,
k - kill the connection (as opposed to keeping it alive which is the default in HTTP1.1),
n – no-cache, actually several headers.
Adding a content-length header involves more than just emitting the header. All of the response must be buffered before the header can be generated, so that the content length can be written into the header. You should allow Retro to do this one for you.
The no-cache option actually generates the following headers:
Cache-control: no-cache
Pragma: no-cache
Expires: Sat, 01 Jan 2000 00:00:00 GMT
Hopefully this cocktail will be sufficient to dissuade most browsers and proxies from using a cached copy.
You can also include extra headers explicitly if you specify the ‘h’ flag in the phases for the page, and provide the headers() entry point in your PIO. Remember to send a ‘\n’ after each header (though you can get away with the last one, Retro will check) – for example:
const char *headers(Request *pr, Response *prs,
ostream &html, void *appdata)
{
html << "Date: Tue, 15
Jul 2003 16:00:00 GMT\n";
html << "Message-ID: 23ab-ff87-ca67-22ee\n";
html << "ETag: fuzzy\n";
return 0;
}
If you use this facility, you had better understand what you are doing.
A page specification in the ASD also allows you to specify the mime type of the response. The trivia/makexml example shows how this is used. Because Retro lets you generate response from compiled code down a fast connection to the client, there are many mime types that could be of interest. For example you could generate graphical material into a pixel matrix, then emit the matrix as a PNG or other bitmapped graphic file. There’s an example in application-set imggen. The ASD is as follows: <?xml version="1.0"?><ApplicationSet verbosity="debug" logpath="/dev/pts/2"> <Application name="makepng" state="hxml"> <Page name="makepng" options="sg" next="" xheads="cn" mimetype="image/png"/> </Application></ApplicationSet> There’s no template. The PIO uses libgd (which in turn requires libpng and libz), and is coded as follows: #include <math.h>#include <request.h>#include <response.h>#include <retrostring.h>#define LOG 1#include <util.h>#include <pio.h>#include "gd.h"#include "gdhelpers.h" // libgd requires a context object to do output to something other// than a file, so we provide a thin wrapper around the out ostreamstruct RetroIOCtx{ RetroIOCtx(ostream &os); gdIOCtx m_ctx; ostream &m_os;}; // We only need the output methodsstatic int __putBuf(gdIOCtx *pctx, const void *src, int sz){ RetroIOCtx *ctx = (RetroIOCtx *) pctx; ctx->m_os.write((const char *)src, sz); return sz;} static void __putChar (gdIOCtx *pctx, int c){ RetroIOCtx *ctx = (RetroIOCtx *) pctx; unsigned char uc = (unsigned char) (c & 0xff); ctx->m_os << uc;} // Cover the free operation just in casevoid __free(gdIOCtx *){} RetroIOCtx::RetroIOCtx(ostream &os) : m_os(os){ memset(&m_ctx, 0, sizeof(gdIOCtx)); m_ctx.putC = __putChar; m_ctx.putBuf = __putBuf; m_ctx.gd_free = __free;} const char *generate(Request *pr, Response *prs, ostream &out, void *appdata){ RetroIOCtx ctx(out); // First sort out the input int size = atoi(pr->value("size").c_str()); if (size <= 0) size = 100; int npoints = atoi(pr->value("points").c_str()); if (npoints <= 0) npoints = 10; int rotn = atoi(pr->value("rotn").c_str()); rotn %= 360; int numerator = atoi(pr->value("num").c_str()); if (numerator <= 0) numerator = 1; int denominator = atoi(pr->value("den").c_str()); if (denominator <= 0) denominator = 2; int red = atoi(pr->value("red").c_str()); if (red < 0) red = 0; if (red > 255) red = 255; int green = atoi(pr->value("green").c_str()); if (green < 0) green = 0; if (green > 255) green = 255; int blue = atoi(pr->value("blue").c_str()); if (blue < 0) blue = 0; if (blue > 255) blue = 255; gdImagePtr im; im = gdImageCreate(size, size); int white = gdImageColorAllocate(im, 255, 255, 255); // background color int fill = gdImageColorAllocate(im, red, green, blue); gdImageColorTransparent(im, white); int i; double center = (double)size/2; double ratio = (double) numerator/denominator; gdPoint *tpp = (gdPoint *) alloca(npoints*2*sizeof(gdPoint)); if (!tpp) throw "Out of memory"; double rot = rotn; rot =(rot*M_PI*2)/360.0; double theta = rot; double subtend = M_PI/npoints; for (i = 0; i < npoints; i++) { tpp[2*i].x = (int) (center+center*cos(theta)); tpp[2*i].y = (int) (center-center*sin(theta)); theta += 2*subtend; } theta = subtend+rot; double ir = ratio*center; for (i = 0; i < npoints; i++) { tpp[2*i+1].x = (int) (center+ir*cos(theta)); tpp[2*i+1].y = (int) (center-ir*sin(theta)); theta += 2*subtend; } gdImageFilledPolygon(im, tpp, npoints*2, fill); // Output the image in PNG format gdImagePngCtx(im, (gdIOCtx *) &ctx); gdImageDestroy(im); return 0;} This creates a PNG image of a star using the parameters provided in the query string. The query should be of the form
?size=300&points=15&rotn=0&num=1&den=2&red=255&green=0&blue=0
Here, size is the side of the bounding square, points is the number of points on the star, rotn is the initial rotation angle, num and den specify a ratio – e.g. 1/2 - that specifies the diameter of the inner circle of the star relative to the outer circle, and red, green, and blue represent the required color. All have some default value.
The background is white and white is specified as being the transparent color.
You might find it convenient to build your Linux Retro applications in the same environment as the examples.
To facilitate this, Retro provides a command line program to create the correct directory structure, create the required makefiles, create a link to raf.fcgi, and to set up skeletons for the ASD, an application error file, and a skeleton page PIO source and template.
To create a new application-set, in the top-level Retro directory invoke the executable as follows:
raf/pioprgen
foo app1 page1
where foo is the name of your new application-set, foo1 is its first application, and foo1page1 is the first page of that application. This should set up a directory structure and files as follows:
/foo
/foo/bin
foo.fcgi (symlink)
/foo/app11
error.html
/foo/app1/pio
page1.cc
page1.d
page1.o
page1.so
Makefile
Makefile.in
pioimple.h
/foo/app1/template
foo1page1.csp
/foo/xml
foo.retro
Makefile
Makefile.in
The program does not modify your httpd.conf file. You will need to add an alias and a directory entry at this point before the vestigial application will run. Something like
Alias /foo /home/you/retro2/foo
<Directory /home/you/retro2/foo/bin>
Options ExecCGI FollowSymLinks
</Directory>
If you are using a console for debug output, check which terminal it is (tty) and make sure it’s writeable (e.g. chmod 0666 /dev/pts/1). Modify the foo.retro file to reflect the correct console or log file. You should also bounce Apache – apachectl restart.
The same program can be used with a similar command line to add an application to the application-set, or add a new page to an application.
You might find it convenient to build your Windows Retro applications in the same environment as the examples.
To facilitate this, Retro provides a command line program to create the correct directory structure, create the required project and workspace files, and to set up skeletons for the ASD, an application error file, and a skeleton page PIO source and template.
To create a new application-set, in the top-level Retro directory invoke the executable as follows:
Bin\pioprgen
foo foo1 page1
where foo is the name of your new application-set, foo1 is its first application, and foo1page1 is the first page of that application. This should set up a directory structure and files as follows:
\foo
\foo\foo1
error.html
\foo\foo1\pio
page1.cpp
page1.obj
foo_app1_page1.dsp
foo_app1_page1.dsw
pioimple.h
\foo\foo1\template
page1.csp
\foo\xml
foo.retro
The program does not modify your IIS configuration. You will need to use the Internet Services Manager to add a virtual directory for foo, and to provide that with an association for the extension “.retro” to retroapi.dll. (See installing on Windows.) You will probably find that you also need to bounce IIS before the new application will work.
The same program can be used with a similar command line to add an application to the application-set, or add a new page to an application.
When you’re happy with what you have created, you can add it to the retro.dsw workspace if you find it convenient to have everything in the same workspace.
OpenTS is a web application designed to allow the employees of service companies/organizations to fill in their weekly time sheet from anywhere where they can access the web. The author originally wrote an earlier CGI version of OpenTS in a fit of peek about the time sheet system used by the organization where he had his day job. It has subsequently been rewritten to investigate the ease with which 'real' applications can be built using Retro.
The outcome was some changes to Retro, (Late loading of page-implementation objects, extension of the next attribute options in application definitions, improved cookie setting provisions, some support for digest and encryption algorithms), and some degree of comfort about the ease of use of Retro. It's about the same as ASP or JSP programming, as you'd expect, though the ability to describe the application-set at high level in the ASD is a distinct advantage.
OpenTS is designed to give a reasonable level of security without the need to set up a secure server. It doesn't send plain text passwords over the wire except at initial setup of an account. Instead it uses digests of passwords (account administration), or passwords encrypted with the previous password (user password changes). For these activities it uses Javascript implementations of SHA1 and TEA (Tiny Encryption Algorithm) in client-side scripts. Corresponding facilities are provided in the Retro shared library for the server side.
For example in the account administration template script, the user must enter her password on the main menu page. A hash is then composed of a timestamp value sent by the server, and the entered password, and this is sent:
var s = document.form0.lts.value+document.form0.clientpwd.value;
document.form0.token.value = calcSHA1(s);
document.form0.clientpwd.value =
"********";
The server can look up the password in the database, and recalculate the hash to validate the user. Similarly when a user changes her password, she must enter her current password, and a hash is calculated. This is then used to encrypt the new password:
var s = form.lts.value+form.pwd.value;
form.token.value = calcSHA1(s);
s = doEncipher(form.newpwd.value, form.pwd.value);
form.pwd.value = "";
The code on the sever side can then calculate the same hash, and decrypt the new password.
OpenTS makes some use of XML data objects, particularly for the actual time sheet data entry screen, but also to pass data structures to populate drop-lists of customers and their associated projects. However it doesn’t use the built-in “hxml” state mechanism – that’s how it was, and if it’s not broken, you don’t mend it.
If anyone ever uses it for real data, please let us know!