Helma Logo
main list history

Version 1 by zumbrunn on 20. February 2009, 16:17

1This tutorial walks you through setting up Helma and implementing a simple addressbook application that stores its data inside a relational database.
2
3
4=== Preparing Helma
5
6I assume you already managed to install a <% shortcut name="download" text="recent version of Helma" %> and that it is now <a href="/download/installation/">up and running</a>.
7
8First of all, you have to tell Helma about your new application. Add a line with a more or less meaningful name (e.g. <tt>addressbook</tt>) to the file <tt>apps.properties</tt> in the Helma installation directory (you might find some lines containing other application names already):
9
10<tt>apps.properties:</tt>
11<pre># List of apps to start.
12
13base
14base.mountpoint = /
15
16manage
17
18<b>addressbook</b></pre>
19
20Helma will now create a new folder in the <tt>apps</tt> directory called <tt>addressbook</tt>, inside of which you will later place the Javascript files and skins that define your application.
21
22The application's name is also the first part of the URL path if you want to access the application from a browser. If Helma is running with the default settings, you should be able to request the URL <a href="http://localhost:8080/addressbook/">http://localhost:8080/addressbook/</a>.
23
24However, there is nothing there yet and you only will get an error message stating "Error in application 'addressbook': Action not found". Since the request didn't reference a particular HopObject and/or specify a specific action, Helma was looking for the "main" action of the "root" HopObject, which you have not yet specified.
25
26This main action will later contain the code that generates a page listing your address book entries. For now, you may just create the following main action, in order to verify that your otherwise empty addressbook application is configured correctly.
27
28apps/addressbook/Root/main.hac:
29<pre>res.data.title = "Helma Address Book";
30renderSkin("html");
31</pre>
32
33This action attempts to render a skin named "html", which you  create inside the "Global" directory:
34
35<tt>apps/addressbook/Global/html.skin:</tt>
36<pre>&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"&gt;
37&lt;head&gt;
38&nbsp;&nbsp;&nbsp;&lt;title&gt;&lt;% response.title %&gt;&lt;/title&gt;
39&nbsp;&nbsp;&nbsp;&lt;meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"&gt;
40&lt;/head&gt;
41&lt;body bgcolor="white"&gt;
42
43Under development: &lt;% response.title %&gt;
44
45&lt;/body&gt;
46&lt;/html&gt;</pre>
47
48Instead of the above mentioned error message, you should now receive the following response.
49
50<tt>Under development: Helma Address Book</tt>
51
52
53=== Database Connection
54
55While Helma features an embedded object-oriented database, where HopObjects are persisted when they are not mapped to an external database, Helma is able to connect to almost any database system in the world. Well, at least if there is a so-called JDBC driver available for it. See the short guide about <a href="/docs/howtos/mysql/">using MySQL</a>, which should help you preparing the freely available MySQL server for a connection with Helma.
56
57To wire the MySQL database to your Helma application, you need a file called <tt>db.properties</tt> in your application root (ie. the folder <tt>addressbook</tt> according to my example). Use your favorite text editor and enter the following lines:
58
59<tt>apps/addressbook/db.properties:</tt>
60<pre>myDataSource.url      = jdbc:mysql://localhost/addressbook
61myDataSource.driver  = org.gjt.mm.mysql.Driver
62myDataSource.user    = helma
63myDataSource.password = password</pre>
64
65This defines <tt>myDataSource</tt> as a connector to the MySQL database called <tt>addressbook</tt> for the user <tt>helma</tt> identified by the password <tt>password</tt> &#150; which you should replace with your password as chosen when setting up the MySQL installation (you certainly can use a different user/password combination as well, as long as it allows to connect to the MySQL server).
66
67If you want to use a different database system or if you gave a different name to your database, that's no problem for Helma at all. Simply replace your JDBC driver or fill in the appropriate name. And certainly, you also can connect to a database server on another machine by arranging the URL, though you might have to set the database access privileges differently.
68
69If you have done everything alright, you should restart either Helma or at least the <tt>addressbook</tt> application.
70
71To restart an application visit the manage-interface at <a href="http://localhost:8080/manage/">http://localhost:8080/manage/</a>. If you haven't visited it up to now you'll get asked to set an initial password. Unless you are experimenting with a local Helma installation, you will probably have to edit the server.properties file and add your (local) IP address to the allowAdmin property.
72
73
74=== Object-Relational Mapping
75
76The goal is now, to "wire" the relational tables in the MySQL database to the object-oriented structure of Helma. While MySQL (and any other relational database system) structures data in tabular form, each row representing a data set, in Helma each table row becomes a HopObject using the columns as named subnodes, also called properties.
77
78To achieve this, you will now create a prototype such HopObjects can be adapted from. While this sounds complex in theory, in practice you do this simply by adding a new directory to your application (e.g. by issueing <tt>mkdir apps/addressbook/Person</tt>).
79<pre>
80+----+-----------+-----------+---------------------+
81| id | firstname | lastname  | email              |
82+====+===========+===========+=====================+
83|  1 | Hannes    | Wallnoefer | hannes@helma.org    |
84+----+-----------+-----------+---------------------+
85|  2 | Robert    | Gaggl    | robert.gaggl@orf.at |
86+----+-----------+-----------+---------------------+
87|  3 | Tobi      | Schaefer  | tobi@helma.org      |
88+----+-----------+-----------+---------------------+
89fig.1 A relational database table.
90
91+-------------+
92| HopObject 1 |
93+===========+=+----------------+
94| FIRSTNAME | Hannes          |
95+-----------+------------------+
96| LASTNAME  | Wallnoefer        |
97+-----------+------------------+
98| EMAIL    | hannes@helma.org |
99+-----------+------------------+
100
101+-------------+
102| HopObject 2 |
103+===========+=+-------------------+
104| FIRSTNAME | Robert              |
105+-----------+---------------------+
106| LASTNAME  | Gaggl              |
107+-----------+---------------------+
108| EMAIL    | robert.gaggl@orf.at |
109+-----------+---------------------+
110
111+-------------+
112| HopObject 3 |
113+===========+=+--------------+
114| FIRSTNAME | Tobi          |
115+-----------+----------------+
116| LASTNAME  | Schaefer        |
117+-----------+----------------+
118| EMAIL    | tobi@helma.org |
119+-----------+----------------+
120fig.2 Three HopObjects representing the three
121rows of the table.</pre>
122These so-called type mappings are set-up in the file <tt>type.properties</tt> for each HopObject. We create such a file in the next step.
123
124
125=== "Wiring" The Prototype
126
127A <tt>type.properties</tt> file that fits with the example MySQL database <tt>addressbook</tt> looks like this:
128
129<tt>apps/addressbook/Person/type.properties:</tt>
130<pre>_db        = myDataSource
131_table      = PERSON
132_id        = ID
133
134firstname  = FIRSTNAME
135lastname    = LASTNAME
136email      = EMAIL
137</pre>
138
139What exactly happens here? The two lines at the beginning tell Helma to map the prototype <tt>Person</tt> and all its HopObjects derived from it to the table <tt>PERSON</tt> (please note the different case!) found via the database connection established with <tt>myDataSource</tt> &#150; the MySQL connector as defined before.
140
141So, <tt>_db</tt> defines the database connection (or: data source) and <tt>_table</tt> the database table to be used. What's missing are the details of how the relational table columns should be mapped to the object-oriented database.
142
143Most important is the mapping of the table's primary key index to the object-oriented database index: this key is necessary to provide a unique identifier for both, each entry in the relational database and each HopObject. And in consequence, <tt>_id</tt> maps the two to each other.
144
145Btw. <tt>_db</tt>, <tt>_table</tt> and <tt>_id</tt> are internal Helma properties. That's why they start with an underscore for not getting in the way of your custom properties. However, they are mandatory for each <tt>type.properties</tt> file.
146
147The five following lines assign each column of the relational table to a property of the HopObject. E.g. the data that is contained in the column <tt>FIRSTNAME</tt> of the table, becomes the value of the property <tt>firstname</tt> of the HopObject, the column <tt>LASTNAME</tt> is being mapped to the property <tt>lastname</tt> and so on.
148
149A recommended naming convention is to use lowercase (resp. mixed case) letters for HopObject properties and uppercase letters for database columns resp. table names.
150
151So, you achieved to represent the figure of HopObjects from the prior step with a few lines in <tt>type.properties</tt>. Save the <tt>type.properties</tt> in the folder of the corresponding HopObject - in this case in the <tt>Person</tt> folder.
152
153To generate the corresponding table in your database, you should be able to use an SQL statement such as the following.
154
155<pre>
156CREATE TABLE `PERSON` (
157`ID` int(11) NOT NULL default '0',
158`FIRSTNAME` varchar(128) NOT NULL default '',
159`LASTNAME` varchar(128) NOT NULL default '',
160`EMAIL` varchar(128) NOT NULL default '',
161PRIMARY KEY (`ID`)
162);
163</pre>
164
165As a side remark, be aware of what names to choose for your HopObject's properties. Just as the database columns can be made up of any name as long as they do not conflict with MySQL keywords, you are restricted to use property names that do not represent reserved HopObject or ECMAScript keywords. A look in this documentation's <a href="http://helma.server-side-javascript.org/reference/">reference</a> section might help if you are in doubt.
166
167
168=== Attaching HopObjects
169
170We now successfully established a database connection from Helma to the MySQL server and mapped the relational table data to HopObject properties. But still, these derived HopObjects are freely floating around in Helma space. There is no possibility to access  HopObjects, yet. Helma even does not know about them. We need a HopObject that already is accessible, that is known by Helma. That's what the <tt>Root</tt> HopObject of an application  is for.
171
172One could say the <tt>Root</tt> HopObject is the default place for Helma to go: "go for the root", that's what Helma can do very good. When you enter the base URL of any application, Helma is going for <tt>Root</tt>, is retrieving data from the <tt>Root</tt> HopObject first.
173
174And as any HopObject has its prototype, there is also one for <tt>Root</tt>, ie. the directory called &#150; you guessed it &#150; <tt>Root</tt>.
175
176To attach our <tt>Person</tt> HopObjects onto <tt>Root</tt>, we have to create yet another file called <tt>type.properties</tt>, this time for the <tt>Root</tt> prototype. It contains the following lines and goes into the &#150; you guessed it again &#150; <tt>Root</tt> folder.
177
178<tt>apps/addressbook/Root/type.properties:</tt>
179<pre>_children            = collection(Person)
180_children.accessname = ID
181_children.order      = LASTNAME asc</pre>
182
183You can read this assignment the way "attach all HopObjects of the prototype <tt>Person</tt> as subnodes, indexed by the column <tt>ID</tt> of the corresponding relational table PERSON, and sorted ascending by the values in column LASTNAME".
184
185And because we save this document in <tt>Root</tt> they will be attached to the HopObject <tt>Root</tt> (you are right when you assume that you could do this with any other HopObject simply by creating such <tt>type.properties</tt> for its prototype &#150; in fact, we have done it before with the <tt>Person</tt> prototype).
186
187
188=== Shaping The Output
189
190Because the <tt>Person</tt> HopObjects are now attached to <tt>Root</tt>, we just need a simple interface to access them and make them display in the browser.
191
192Here's a simple XHTML layout we can use:
193
194<tt>apps/addressbook/Global/html.skin:</tt>
195<pre>&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"&gt;
196&lt;head&gt;
197&nbsp;&nbsp;&nbsp;&lt;title&gt;&lt;% response.title %&gt;&lt;/title&gt;
198&nbsp;&nbsp;&nbsp;&lt;meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"&gt;
199&lt;/head&gt;
200&lt;body bgcolor="white"&gt;
201
202&lt;% response.body %&gt;
203
204&lt;/body&gt;
205&lt;/html&gt;</pre>
206
207Instead of defining a generic title, I am using a placeholder <tt>&lt;% response.title %&gt;</tt>, just as the whole text body is substituted by <tt>&lt;% response.body %&gt;</tt>. Both placeholders will be replaced by "real" content, later.
208
209Helma calls such placeholder structures "macros", the files fitted with such macros are called "skins", resp. "skin files". Skin files are behaving like templates, except that you absolutely cannot evaluate any scripting code in those. And that's again what macros are for. Macros are replaced by either the result of a function call or, like in this case, by the value of an object's property.
210
211This is one of Helma's big virtues: the separation of data and presentation. Because it is a good idea to avoid putting layout elements into your code just as it is a good idea to avoid including scripting code in your layout. Helma's macro and skin concepts help you to prevent these big no-nos and if you follow this path your web applications will become more flexible and easier to deploy, even more beautiful.
212
213To see how the two macros will work here, save this skin as <tt>html.skin</tt> in the directory <tt>addressbook/Global</tt>.
214
215
216=== Improvements
217
218Helma's skin and macro features allow you to easily create layouts following the same hierarchical structure the represented data was built of.
219
220We will now create another skin for the <tt>Person</tt> prototype to be used by all <tt>Person</tt> HopObjects in consequence:
221
222<tt>apps/addressbook/Person/link.skin:</tt>
223<pre>&lt;a href="mailto:&lt;% this.email %&gt;"&gt;&lt;% this.firstname %&gt; &lt;% this.lastname %&gt;&lt;/a&gt;&lt;br /&gt;</pre>
224
225I think the three macros in this skin are pretty self-explanatory. They all refer to the current HopObject by <tt>this</tt> (ie. <tt>this</tt> will be evaluated to a <tt>Person</tt> HopObject at runtime) and three of the five custom properties we defined for it. Each of them will be replaced by the corresponding HopObject's property value.
226
227Save this skin as <tt>link.skin</tt> in the <tt>Person</tt> directory.
228
229The code for actions performed on an object is stored in <tt>hac</tt> files which are just text-files with a senseful name and the file-extension <tt>hac</tt> - for instance <tt>edit.hac</tt>.
230
231The default action for an object is stored in <tt>main.hac</tt>
232
233We will now define the default action for the application's <tt>Root</tt> object and change the file called <tt>main.hac</tt> in the <tt>Root</tt> folder to contain code that looks like this:
234
235<tt>apps/addressbook/Root/main.hac:</tt>
236<pre>
237var str = "";
238for (var i=0; i&lt; root.size(); i++) {
239&nbsp;&nbsp;&nbsp;var person = root.get(i);
240&nbsp;&nbsp;&nbsp;str += person.renderSkinAsString("link");
241}
242
243res.data.title = "Helma Address Book";
244res.data.body = str;
245renderSkin("html");
246</pre>
247
248If everything is done right, Helma will now render the skin <tt>Person/link.skin</tt> for each <tt>person</tt> HopObject using the corresponding object data. And so your <a href="http://localhost:8080/addressbook/">browser display</a> should look much more beautiful:
249
250<tt><a href="mailto:xxxATxxxDOTorg">Hannes Wallnoefer</a>
251<a href="mailto:xxxATxxxDOTorg">Robert Gaggl</a>
252<a href="mailto:xxxATxxxDOTorg">Tobi Schaefer</a></tt>
253
254See how easy Helma skins and macro work? And in effect, you create very flexible and modular web applications. And you even can use your own custom macros &#150; just continue reading...
255
256
257=== Custom Macros
258
259The way we filled the MySQL database with some data is quite unefficient. Why not do it with Helma?
260
261As a first step, we enable editing the database entries and add an appropriate link to the skin file <tt>Person/link.skin</tt> (in a later step we also will make it possible to create new entries):
262
263<pre>&lt;a href="mailto:&lt;% this.email %&gt;"&gt;&lt;% this.firstname %&gt; &lt;% this.lastname %&gt;&lt;/a&gt;
264&lt;small&gt;&lt;a href="&lt;% this.href action="edit" %&gt;"&gt;edit&lt;/a&gt;&lt;/small&gt;&lt;br /&gt;</pre>
265
266Other than the first three macros, <tt>this.href</tt> does not refer to a property of a <tt>person</tt> HopObject. It is a custom macro that needs a macro handler, ie. a function to work:
267
268<tt>apps/addressbook/Person/macros.js:</tt>
269<pre>function href_macro(param) {
270&nbsp;&nbsp;return(this.href(param.action));
271}</pre>
272
273Enter the above lines in a new file and save this as <tt>macros.js</tt> in the <tt>Person</tt> directory.
274
275Now let's put it altogether:
276
277Helma transforms the macro structure <tt>&lt;% this.href action="edit" %&gt;</tt> into a function call of <tt>this.href_macro(param)</tt> with <tt>param</tt> being a generic object containing the property <tt>action</tt> (along with its value).
278
279That means <tt>param.action</tt> contains the string <tt>"edit"</tt> as it was assigned in the macro structure. The function <tt>href_macro(param)</tt> then returns the URL of the actual <tt>person</tt> HopObject plus <tt>"edit"</tt>.
280
281A look at the browser will proove if these considerations are right:
282
283<tt><a href="xxxATxxxDOTorg">Hannes Wallnöfer</a> <small><a href="http://localhost:8080/addressbook/1/edit">edit</a></small>
284<a href="mailto:xxxATxxxDOTorg">Robert Gaggl</a> <small><a href="http://localhost:8080/addressbook/2/edit">edit</a></small>
285<a href="mailto:xxxATxxxDOTorg">Tobi Schäfer</a> <small><a href="http://localhost:8080/addressbook/3/edit">edit</a></small></tt>
286
287Great! It works. But yet the links lead into Helmatic nirvana. We have to enable the edit form first.
288
289
290=== Handling User Input
291
292To enter data into Helma from a browser we first create an adequate layout for an HTML form:
293
294<tt>apps/addressbook/HopObject/edit.skin:</tt>
295<pre>&lt;form action="edit" method="post"&gt;
296First Name: &lt;input type="text" name="firstname"
297    value="&lt;% this.firstname encoding="form" %&gt;" /&gt;&lt;br /&gt;
298Last Name: &lt;input type="text" name="lastname"
299    value="&lt;% this.lastname encoding="form" %&gt;" /&gt;&lt;br /&gt;
300e-mail: &lt;input type="text" name="email"
301    value="&lt;% this.email encoding="form" %&gt;" /&gt;
302&lt;p /&gt;
303&lt;input type="submit" name="submit" value=" Save " /&gt;
304&lt;/form&gt;</pre>
305
306Save this skin as <tt>edit.skin</tt> but this time in the <tt>HopObject</tt> directory (we will reveal the reason for this later).
307
308The corresponding action <tt>edit.hac</tt> looks like this &#150; it needs to be saved in the <tt>Person</tt> directory:
309
310<tt>apps/addressbook/Person/edit.hac:</tt>
311<pre>res.data.title = "Edit Helma Address Book Entry";
312res.data.body = this.renderSkinAsString("edit");
313renderSkin("html");</pre>
314
315Now the <tt>edit</tt> links from the main resource of our address book application should work and you should see the HTML form containing the chosen HopObject's data.
316
317<% image name="addressbook" %>
318
319The <tt>edit.hac</tt> / <tt>edit.skin</tt> combo works analogously to <tt>main.hac</tt> and <tt>link.skin</tt> in the previous sections. So nothing really new here except the <tt>encoding</tt> parameters in the skin markup.
320
321They avoid HTML markup entered in the database from messing up the HTML layout. Each value returned from the macro handler is encoded like it was wrapped into a <a href="http://helma.server-side-javascript.org/reference/global.html#encodeForm"><tt>encodeForm()</tt></a> function.
322
323Although the <tt>Save</tt> button already works, the data remains yet unchanged. The necessary code has to be added before the first line of <tt>edit.hac</tt>:
324
325<pre>if (req.data.submit) {
326&nbsp;&nbsp;this.firstname = req.data.firstname;
327&nbsp;&nbsp;this.lastname = req.data.lastname;
328&nbsp;&nbsp;this.email = req.data.email;
329&nbsp;&nbsp;this.modifytime = new Date();
330&nbsp;&nbsp;res.redirect(root.href());
331}</pre>
332
333Try out to change some values and click <tt>Save</tt> afterwards &#150; you should notice the changes immediately in the list view.
334
335
336=== Creating A HopObject
337
338We are only a few more steps away from successfully having built a simple application with the basic and most important Helma features.
339
340One thing that's still missing is to create a new <tt>person</tt> HopObject. Let's do this now.
341
342Because the create form almost exactly looks like the edit form in <tt>HopObject/edit.skin</tt> from the previous step, we will recycle it for this purpose.
343
344Therefore some slight adjustments are necessary:
345
346Change the first line of <tt>HopObject/edit.skin</tt> from
347
348<pre>&lt;form action="edit" method="post"&gt;</pre>
349
350to
351
352<pre>&lt;form action="&lt;% response.action %&gt;" method="post"&gt;</pre>
353
354Then add the line
355
356<pre>res.data.action = "edit";</pre>
357
358just before the last line containing <tt>renderSkin("html");</tt> in <tt>Person/edit.hac</tt> (this is just to keep this action file compatible with our changes).
359
360Create the action file <tt>Root/create.hac</tt> as follows:
361
362<tt>apps/addressbook/Root/create.hac:</tt>
363<pre>if (req.data.submit) {
364&nbsp;&nbsp;var p = new Person();
365&nbsp;&nbsp;p.firstname = req.data.firstname;
366&nbsp;&nbsp;p.lastname = req.data.lastname;
367&nbsp;&nbsp;p.email = req.data.email;
368&nbsp;&nbsp;p.createtime = new Date();
369&nbsp;&nbsp;p.modifytime = new Date();
370&nbsp;&nbsp;root.add(p);
371&nbsp;&nbsp;res.redirect(root.href());
372}
373
374res.data.title = "Create Helma Address Book Entry";
375res.data.body = this.renderSkinAsString("edit");
376res.data.action = "create";
377renderSkin("html");</pre>
378
379Finally, add these three macro functions to a new file called <tt>macros.js</tt> (don't use <tt>Person/macros.js</tt>):
380
381<tt>apps/addressbook/Root/macros.js:</tt>
382<pre>function firstname_macro() {
383&nbsp;&nbsp;return("");
384}
385
386function lastname_macro() {
387&nbsp;&nbsp;return("");
388}
389
390function email_macro() {
391&nbsp;&nbsp;return("");
392}</pre>
393
394Before we look at the result of these additions and modifications, let me explain some details.
395
396The introduction of <tt>res.data.action</tt> and its corresponding macro <tt>&lt;% response.action %&gt;</tt> gives the skin file <tt>HopObject/edit.skin</tt> more flexibility since the action file that is invoking it determines the form action for it.
397
398The reason why <tt>edit.skin</tt> is stored in the <tt>HopObject</tt> directory is to make it available for both, the <tt>Root</tt> and the <tt>Person</tt> prototypes. Everything that's belonging to the <tt>HopObject</tt> prototype also belongs to any other custom HopObject prototype. This way the <tt>Root</tt> and <tt>Person</tt> prototypes can share the same skin file.
399
400Moreover, and this is a big difference to when we would have saved <tt>edit.skin</tt> as global skin file, we can use the scope variable <tt>this</tt>. It always refers to the actual HopObject, either the <tt>root</tt> object or a particular <tt>person</tt> object.
401
402This is very convenient, since that way we can retrieve any HopObjects properties directly via <tt>this.firstname</tt>, <tt>this.lastname</tt> and <tt>this.email</tt>.
403
404Because <tt>root</tt> does not have any of these properties, we have to avoid the output of error messages by adding corresponding macro functions.
405
406So while the <tt>&lt;% this.firstname %&gt;</tt> macro for any <tt>person</tt> HopObject refers to its property stored in the database, <tt>&lt;% this.firstname %&gt;</tt> for the <tt>root</tt> HopObject refers to the macro function <tt>root.firstname_macro()</tt>, and respectively do the other two macros.
407
408Now point your browser to the URL <a href="http://localhost:8080/addressbook/create">http://localhost:8080/<wbr>addressbook/<wbr>create</a> and you will get an empty form. Fill in some data and press <tt>Save</tt>. Et voilà! You immediately should see a new item at the end of the list.
409
410Here is the file structure you should have ended up with:
411
412&nbsp;- apps
413&nbsp;&nbsp;&nbsp;- addressbook
414&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; db.properties
415&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- Global
416&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; html.skin
417&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- HopObject
418&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; edit.hac
419&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; edit.skin
420&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- Person
421&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; link.skin
422&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; macros.js
423&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; type.properties
424&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- Root
425&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; create.hac
426&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; macros.js
427&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; main.hac
428&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; type.properties
429
430