diff --git a/releases/index.html b/releases/index.html index 83446c2..0163b87 100644 --- a/releases/index.html +++ b/releases/index.html @@ -1780,9 +1780,9 @@
Download the latest release of the WebDSL Plugin for Eclipse bundle for your platform:
-WebDSL in Eclipse bundle (Linux) - WebDSL in Eclipse bundle (MacOS) - WebDSL in Eclipse bundle (Windows)
+WebDSL in Eclipse bundle (Linux)
+WebDSL in Eclipse bundle (MacOS)
+WebDSL in Eclipse bundle (Windows)
diff --git a/search/search_index.json b/search/search_index.json index 6cd5197..e9aacc0 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"WebDSL","text":"WebDSL is a domain-specific language for developing dynamic web applications with a rich data model, developed and maintained by the Programming Language Group at Delft University of Technology.
"},{"location":"background/","title":"Background","text":"This section contains information on the ideas, architecture, and design decisions behind WebDSL. For the WebDSL language reference, see the Reference section.
"},{"location":"background/about/","title":"About WebDSL","text":"WebDSL is a domain-specific language for developing dynamic web applications with a rich data model.
"},{"location":"background/about/#features","title":"Features","text":"WebDSL is being developed by Eelco Visser and Ph.D./M.Sc. students in the context of the Model-Driven Software Evolution project at Delft University of Technology.
"},{"location":"background/about/#active-developers","title":"Active developers","text":"Meeting notes of past WebDSL meetings are documented here.
"},{"location":"background/meeting-notes/#webdsl-improvement-meeting-may-2021","title":"WebDSL Improvement Meeting May 2021","text":"The following points were discussed in a WebDSL improvement meeting.
"},{"location":"background/meeting-notes/#documentation","title":"Documentation","text":".length
and .length()
are used for different types.inputajax
for Secret
and WikiText
types.application.ini
. Certain database modes would not work on certain operating systems, the default one has to work on all platforms.All WebDSL publications on researchr.org
"},{"location":"howtos/","title":"How-To's","text":"These are some How-To's that help you to get to a specific goal or result with WebDSL. For hands-on tutorials on learning WebDSL, see the Tutorials section. For the WebDSL language reference, see the Reference section.
"},{"location":"howtos/#installation-and-build","title":"Installation and Build","text":"Download the WebDSL CLI for your platform:
WebDSL CLI
"},{"location":"howtos/install-cli/#installation","title":"Installation","text":"An installation of Java 8 or newer and Apache Ant are required.
webdsl/bin
directory to your $PATH
.Go to the directory of your application and execute:
webdsl run appname.app\n
This will override any existing application.ini
file.
This generates an application.ini
file configured for testing with an H2 in-memory database.
If there is already an application.ini
file with configuration (see Application Configuration options), use instead:
webdsl run\n
Pressing Ctrl+C stops the application.
Faster compilation on command-lineYou can avoid JVM startup overhead by keeping the WebDSL compiler process running.
This requires having the Nailgun client installed. For example, on macOS you can install nailgun
using brew install nailgun
.
Add to the application.ini
configuration file of your project the following line to use the compile server for all the commands like webdsl run
:
usecompileserver=true\n
To start the WebDSL nailgun server process, invoke:
webdsl start\n
"},{"location":"howtos/install-eclipse-bundle/","title":"Install the Eclipse with WebDSL Plugin Bundle","text":"Install an Eclipse instance with the WebDSL plugin pre-installed for your platform:
WebDSL in Eclipse bundle
"},{"location":"howtos/install-eclipse-bundle/#troubleshooting","title":"Troubleshooting","text":""},{"location":"howtos/install-eclipse-bundle/#macos-eclipse-cannot-be-opened-because-the-developer-could-not-be-verified","title":"macOS: \"Eclipse\" cannot be opened because the developer could not be verified","text":"macOS puts unverified binaries in 'quarantine' and disallows their execution. To remove the com.apple.quarantine
attribute, do:
xattr -rc Eclipse.app\n
"},{"location":"howtos/install-eclipse-bundle/#eclipse-does-not-start-or-complains-about-missing-java","title":"Eclipse does not start, or complains about missing Java","text":"Ensure you have a distribution of Java installed. Then in eclipse.ini
, add a -vm
line at the top of the file, followed by the path to the Java installation. For example, with SDKMan! on macOS:
-vm\n/Users/myusername/.sdkman/candidates/java/current/jre/lib/jli/libjli.dylib\n
"},{"location":"howtos/install-eclipse-plugin-manually/","title":"Install the WebDSL Eclipse Plugin","text":"Perform a manual installation of the WebDSL plugin in Eclipse 3.5 or newer.
In the Work with: text area, type:
https://update.webdsl.org/update\n
Uncheck Group items by category to make the plugin visible.
It is recommended that the eclipse.ini
of Eclipse is updated to give WebDSL enough stack space and memory to function correctly. Include the following options in eclipse.ini
, below the line that starts with -vmargs
.
-Xss8m -Xms256m -Xmx1024m -XX:MaxPermSize=256m -server\n
On macOS this file can be found at Eclipse.app/Contents/MacOS/eclipse.ini
.
To get started with WebDSL, install the WebDSL editor and the WebDSL Command-Line interface.
"},{"location":"howtos/install/#webdsl-editor","title":"WebDSL Editor","text":"We provide an Eclipse bundle in which you can create and edit WebDSL applications with the assistance of syntax highlighting, static analysis and code completion. Alternatively, you can download the WebDSL plugin in your own Eclipse instance.
Recommended: Eclipse BundleDownload an Eclipse instance with the latest WebDSL plugin pre-installed for your platform:
WebDSL in Eclipse bundle
Installation instructions.
Alternative: Eclipse PluginPerform a manual installation of the WebDSL plugin in Eclipse 3.5 or newer through the update site:
https://update.webdsl.org/update\n
Installation instructions.
"},{"location":"howtos/install/#command-line-interface","title":"Command-Line Interface","text":"The WebDSL CLI transforms your WebDSL code to web applications.
Recommended: Download Latest BuildDownload the WebDSL CLI for your platform:
WebDSL CLI
Installation instructions.
Alternative: Build WebDSL YourselfClone the WebDSL GitHub repository and follow the instructions listed there.
:fontawesome-solid-external-link-alt: WebDSL GitHub Repository
"},{"location":"howtos/install/#start-creating-web-applications","title":"Start Creating Web Applications","text":"Once installed and started, you are ready to create web applications with WebDSL. Learn WebDSL by example through our tutorials.
"},{"location":"reference/","title":"Reference","text":"This is the WebDSL language reference. For more background information on the ideas, architecture, and design decisions behind WebDSL, see the Background section.
"},{"location":"reference/access-control/","title":"Access Control","text":""},{"location":"reference/access-control/#configuration-of-the-principal","title":"Configuration of the Principal","text":"The Access Control sublanguage is supported by a session entity that holds information regarding the currently logged in user. This session entity is called the securityContext and is configured as follows:
principal is User with credentials name, password\n
This states that the User entity will be the entity representing a logged in user. The credentials are not used in the current implementation (the idea is to derive a default login template). The resulting generated session entity will be:
session securityContext\n{\n principal : User\n loggedIn : Bool := this.principal != null\n}\n
Note that this principal declaration is used to enable access control in the application.
It will also generate authentication()
(both login and logout), login()
, and logout()
templates, and an authenticate
function that takes the credentials as arguments and, if they are correct, returns true and sets the principal (only String/Email/Secret-type credential properties are allowed).
Authentication can be added manually, instead of using the generated authentication templates. Here is a small example application with custom authentication:
principal is User with credentials name, password\n\nentity User {\n name : String\n password : Secret\n}\n\ntemplate login {\n var username := \"\"\n var password : Secret := \"\"\n\n form { \n label(\"Name: \"){ input(username) }\n label(\"Password: \"){ input(password) }\n captcha()\n submit login() { \"Log In\" }\n }\n\n action login(){\n validate(authenticate(username,password), \n \"The login credentials are not valid.\");\n message(\"You are now logged in.\");\n }\n}\n\ntemplate logout {\n \"Logged in as \" output(securityContext.principal)\n form{\n submitlink logout() {\"Log Out\"}\n }\n action logout(){\n securityContext.principal := null;\n }\n}\n\npage root {\n login()\n \" \"\n logout()\n}\n\ninit {\n var u1 : User := \n User{ name := \"test\" password := (\"test\" as Secret).digest() };\n u1.save();\n}\n\naccess control rules\n\n rule page root(){\n true\n }\n
When storing a secret property you need to create a digest of it:
newUser.password := newUser.password.digest();\n
This makes sure the secret property is stored encrypted. A digest can be compared with an entered string using the check method: us.password.check(enteredpassword)\n
"},{"location":"reference/access-control/#protecting-resources","title":"Protecting Resources","text":"The default policy is to deny access to all pages and ajax templates, the rules determine what the conditions for allowing access are. Regular templates are accessible by default, however, you can add additional access control rules on templates to limit their accessibility.
A simple rule protecting the editUser page to be only accessable by the user being edited looks like this:
access control rules\n\n rule page editUser(u:User){\n u == principal \n }\n
An analysis of this rule: section some description
.Matching can be done a bit more freely using a trailing * as wildcard character, both in resource name and arguments:
rule page viewUs*(*){\n true\n}\n
When more fine-grained control is needed for rules, it is possible to specify nested rules. This implies that the nested rule is only valid for usage of that resource inside the parent resource. The allowed combinations are page - action, template - action, page - template. The next example shows nested rules for actions in a page:
rule page editDocument(d:Document){\n d.author == principal\n rule action save(){\n d.author == principal\n }\n rule action cancel(){\n d.author == principal\n } \n }\n
This flexibility is often not necessary, and it is also inconvenient having to explicitly allow all the actions on the page, for these reasons some extra desugaring rules were added. When specifying a check on a page or template without nested checks, a generic action rules block with the same check is added to it by default. For example:
rule page editDocument(d:Document){\n d.author == principal \n }\n
becomes rule page editDocument(d:Document){\n d.author == principal\n rule action *(*)\n {\n d.author == principal\n } \n }\n
"},{"location":"reference/access-control/#reuse-in-access-control-rules","title":"Reuse in Access Control rules","text":"Predicates are functions consisting of one boolean expression, which allows reusing complicated expressions, or simply giving better structure to the policy implementation. An example of a predicate:
predicate mayViewDocument (u:User, d:Document){\n d.author == principal\n || u in d.allowedUsers\n }\n rule page viewDocument(d:Document){\n mayViewDocument(principal,d)\n }\n rule page showDocument(d:Document){\n mayViewDocument(principal,d)\n }\n
"},{"location":"reference/access-control/#inferring-visibility","title":"Inferring Visibility","text":"A disabled page or action redirects to a very simple page stating access denied. Since this is not very user friendly, the visibility of navigate links and action buttons/links are automatically made conditional using the same check as the corresponding resource. An example conditional navigate:
if(mayViewDocument(securityContext.principal,d)){\n navigate(viewDocument(d)){ \"view \" output(d.title) } \n}\n
When using conditional forms it is often more convenient to put the form in a template, and control the visibility by a rule on the template.
"},{"location":"reference/access-control/#using-entities","title":"Using Entities","text":"Access Control policies that rely on extra data can create new or extend existing properties. An example of extending an entity is adding a set of users property to a document representing the users allowed access to that document:
extend entity Document{\n allowedUsers : {User}\n }\n
"},{"location":"reference/access-control/#administration-of-access-control","title":"Administration of Access Control","text":"Administration of Access Control in WebDSL is done by the normal WebDSL page definitions. All the data of the Access Control policy is integrated into the WebDSL application. An option is to incorporate the administration into an existing page with a template. This example illustrates the use of a template for administration:
template allowedUsersRow(document:Document){\n row{ \"Allowed Users:\" input(document.allowedUsers) }\n }\n
The template call for this template is added to the editDocument page: table{\n row{ \"Title:\" input(document.title) }\n row{ \"Text:\" input(document.text) }\n row{ \"Author:\" input(document.author) } \n allowedUsersRow(document)\n }\n
By using a template the Access Control can be disabled easily by not including the access control definitions and the template. The unresolved template definitions will give a warning but the page will generate normally and ignore the template call."},{"location":"reference/access-control/#minimal-access-control-example","title":"Minimal Access Control Example","text":"application minimalac\n\n entity User {\n name : String\n password : Secret\n }\n\n init {\n var u := User{ name := \"1\" password := (\"1\" as Secret).digest() };\n u.save();\n }\n\n page root {\n authentication()\n \" \"\n navigate protectedPage() { \"go\" }\n }\n\n page protectedPage { \"access granted\" }\n\n principal is User with credentials name, password\n\n access control rules\n\n rule page root(){true}\n rule page protectedPage(){loggedIn()}\n
"},{"location":"reference/action-code/","title":"Action Code","text":"This section describes the expressions and statements available in WebDSL.
"},{"location":"reference/action-code/#expressions","title":"Expressions","text":"literals A number of literals are supported:
\"This is a string\"
22
8.3
true
/false
[<expression>, <expression>, ...]
List<Int>()
{<expression>, <expression>, ...}
Set<Int>()
null
operators The following operators are supported:
+
-
*
/
%
as
(example: 8 as Float
)binary operators
==
!=
>
>=
<
<=
is a
(checks if a certain expression is of a certain runtime type)in
(checks if a certain expression is contained in a collection)&&
||
!
Example:
if(!(b is a String) && (b in [8, 5] || b + 3 = 7)) {\n // ...\n}\n
variables Variables can be accessed by use of their identifiers and their properties using the . notation. Example: person.lastName
indexed access List elements can be retrieved and assigned using index access syntax:
var a := list[0]; \nlist[2] := \"test\";\n
"},{"location":"reference/action-code/#functions","title":"Functions","text":"Functions can be defined globally and as methods in entities:
function sayHello(to : String) : String {\n return \"Hello, \" + to;\n}\n\nentity User {\n name : String\n\n function showName() : String {\n return sayHello(name + \"!\");\n }\n}\n
As of August 2016, entity functions without arguments can also be preceded with the cache
keyword. This cache operates at the request level, i.e. it only calculates its value once per request. This is useful for cases where a more expensive function is repeatedly invoked (e.g. for access control).
entity SubForum {\n name : String\n managers : [User]\n ...\n\n cached function isManager() : String {\n return loggedIn() \n && ( principal() in managers || parentForum.isManager() )\n }\n}\n
"},{"location":"reference/action-code/#variable-declaration","title":"Variable Declaration","text":"Variables can be defined globally, in pages (see Page Variables, and in code blocks.
Syntax:
var <identifier> : <Sort>;\n
This defines a variable within the current scope with name identifier and type Sort.
Variable declarations can also have an expression that initializes the value:
var <identifier> [: <Sort>] := expression;\n
The sort is optional in this case, if the Sort is not declared, the var will receive the type resulting from the expression (also known as local type inference).
Global variables always need an expression for initialization, they are added to the database once (when the first page is loaded, the database is checked to see whether all global vars have been created already). Global variables can be edited, but removing them can cause problems when there are explicit references to those variables.
Global variables can be further initialized using a global init{} block, e.g.
var defaultUser := User{ name := \"default\" }\ninit {\n defaultUser.someInitializeFunction();\n}\npage root {\n output(defaultUser.name)\n}\n
Global inits are also performed only once after database creation (if the dbmode is create-drop each new deploy will recreate the globals and execute inits, see App Configuration.
The ; is optional for global and page variable declarations.
"},{"location":"reference/action-code/#assignment","title":"Assignment","text":"The syntax of an assignment:
<variable> := <value expression>;\n
Example:
p.lastName := \"Doe\";\n
"},{"location":"reference/action-code/#return","title":"Return","text":"Syntax:
return <expression>;\n
Example:
function test() : String{\n return p.lastName;\n}\n
In the context of a entity function this returns the expression as the result of that function. In the context of an action or page init definition, it redirects the user to the page specified in the expression.
Example:
action done() { return root(); }\n
"},{"location":"reference/action-code/#for-loop","title":"For-loop","text":"Iterating a collection of entities or primitives can be done using a for loop. There are three types of for loop statements:
For
This type of for loop iterates the collection produced by expression e, which must contain elements of type t. The elements in the collection are accessible through identifier id.
The collection can be filtered:
for (id:t in e) { stat* }\n for (id:t in e filter) { stat* }\n
ForAll
This for loop iterates all the entities in the database of type t. These can also be filtered. Note that it is more efficient to retrieve the objects using a filtering query and use the regular for loop above for iteration.
for (id:t) { stat* }\n for (id:t filter) { stat* }\n
For Count
This for loop iterates the numbers from e1 to e2-1.
for (id:Int from e1 to e2) { stat* }\n
For-loop Filter
The filter part of a for loop can consist of four parts:
Where
where e1\n
e1 is a boolean expression which needs to evaluate to true for the element to be iterated.
Order By
order by e2 asc/desc\n
e2 is an expression that needs to produce a primitive type such as String or Int, which will be used to order the elements ascending or descending.
Limit
limit e3\n
e3 is an Int expression which will limit the number of elements that get iterated.
Offset
offset e4\n
e4 is an Int expression which will offset the starting element of the iteration.
Each of the four parts is optional, but they have to be specified in this order. The filtering is done in the application, so use queries instead of filters to optimize the WebDSL application.
"},{"location":"reference/action-code/#list-comprehension","title":"List Comprehension","text":"List comprehensions are a combination of mapping, filtering and sorting.
[ e1 | id : t in e2 ]\n
e2 produces a collection of elements with type t, e1 is an expression that allows transformation of the elements using identifier id.
Filters are also allowed:
[ e1 | id : t in e2 filter ]\n
Example:
[e.title | e : BlogEntry in b.entries \n where e.created > date \n order by e.created desc]\n
This expression returns all titles (e.title) from b.entries where the time created (e.created) is greater than a certain date, ordered by e.created in descending order. Both the where and order by clauses are optional. An ordering is either ascending (asc) or descending (desc).
Conjunction
And [ e1 | id : t in e2 ]\n
If e1 produces a boolean, the list comprehension can be preceded by \"And\" to create the conjunction of the elements produced by the list comprehension.
Disjunction
Or [ e1 | id : t in e2 ]\n
If e1 produces a boolean, the list comprehension can be preceded by \"Or\" to create the disjunction of the elements produced by the list comprehension.
"},{"location":"reference/action-code/#while-statement","title":"While Statement","text":"Besides for loops, iteration can also be performed using the while statement.
while (e) { stat* }\n
This will repeat stat* while e evaluates to true.
"},{"location":"reference/action-code/#switch-statement","title":"Switch Statement","text":"The case-statement has the following syntax:
case (<expression>) {\n [case <expr-1> {\n <block executed if true>\n }] *\n [default {\n <block executed if no cases match>\n }]\n}</verbatim>\n
Any number of cases and optionally one default case can be specified.
Example:
case (formatNumber) {\n 1 {\n // format is one\n }\n 2 {\n // format is two\n }\n default {\n // format is neither one nor two\n }\n}\n
"},{"location":"reference/action-code/#regular-expressions-regex","title":"Regular Expressions / Regex","text":"Examples in codefinder
"},{"location":"reference/action-code/#render-template-to-string","title":"Render Template to String","text":"The rendertemplate
function can be used to render template contents to a String.
rendertemplate(TemplateCall):String\n
Example:
template test(a:Int){ output(a) \"!\" }\nfunction showContent(i:Int){\n log(rendertemplate(test(i)));\n}\n
"},{"location":"reference/advanced-search/","title":"Advanced Search","text":"note: Some syntax changes and additional features are expected for WebDSL 1.3.0. The search language will become more structured. This manual will be updated/completed upon 1.3.0 release
WebDSL offers full text search engine capabilities based on Apache Lucene and Hibernate Search. Current implementation supports:
Using search in WebDSL starts by marking which entities need to be searchable. If one property is marked searchable, the entity can be searched. For each entity property one or more search fields can be specified. There are 2 ways to specify these: using search mappings or using searchable annotations. For simple search functionality, searchable annotations will suffice, but for cleaner code we recommend using search mappings.
"},{"location":"reference/advanced-search/#using-search-mappings-recommended","title":"Using search mappings (recommended)","text":"A search mapping starts with the name of the property to be indexed, optionally followed by mapping specifications:
"},{"location":"reference/advanced-search/#as-name","title":"asname
","text":"Override the default search field name
. Default: property name
analyzer
","text":"Indexed using analyzer analyzer
instead of the default analyzer.
Float
|^Float
","text":"Search field is boosted to Float
at index time (default 1.0).
Indicate that this search field can be used for spell checking/autocompletion.
"},{"location":"reference/advanced-search/#for-subclass-entity","title":"for subclassentity
","text":"In case marking an reference/composite property as searchable, you might want to make only a specific subclass of the property type searchable.
"},{"location":"reference/advanced-search/#depth-intwith-depth-int","title":"depthInt
|with depth Int
","text":"In case marking an reference/composite property as searchable, you can specify the depth of the 'embedded' path, 1 is the default.
"},{"location":"reference/advanced-search/#mapping-specification","title":"+mapping specification
","text":"Prefix a mapping specification
with the plus sign if you want this search field to be used by default at query time. If no default search field is specified, all search fields are used by default.
Search mappings belong to an entity and can be placed inside an entity declaration, or somewhere else by adding the entity name. Names of the search fields are scoped to entities, so different entities may share the same names for search fields.
// Embedded search mapping\nentity Message {\n subject : String\n text : Text\n category : String\n sender : User\n\n search mapping {\n +subject\n +text using snowballporter as textSnowBall\n text\n category\n +sender for subclass ForumUser\n }\n}\n
// External search mapping\nentity ForumUser : User {\n forumName : String\n forumPwd : Secret\n messages : {Message} (inverse=Message.sender)\n}\n...\nsearch mapping ForumUser {\n forumName using none\n}\n
"},{"location":"reference/advanced-search/#using-annotations","title":"Using annotations","text":"Search fields can also be specified using property annotations:
// Using searchable annotations\nentity Message {\n subject : String (searchable)\n text : Text (searchable, searchable(name=textSnowBall, analyzer=snowballporter)\n category : String (searchable)\n sender : ForumUser (searchable())\n}\n
The above code marks the entity Message
searchable, and it has 3 search fields: subject
, text
using the default analyzer, and textSnowball
, which uses the snowball porter analyzer. Searchable annotations have no restriction w.r.t. search mappings, and both can be used interchangeably (not recommended since it's less transparent). The following table shows the annotation equivalent of specifications in search mappings.
Properties of any type can be made searchable, although there are some notes to make.
"},{"location":"reference/advanced-search/#reference-and-composite-properties","title":"Reference and composite properties","text":"These properties don't contain any text or value by themselves, but hold references to other entities. Therefore, the properties themselves cannot be indexed, but the searchable properties of the referred entity/entities will be indexed in the scope of the current entity. For example if you want to be able to search for Message
entities by the name of the sender (in the above example), the property forumName
of ForumUser
needs to be indexed in the scope of Message
. This can be done by marking the sender
property as searchable. All search fields from ForumUser
will then be available for Message
, and searchfields are prefixed with 'propertyName.
' by default (or different name if specified using as
in search mappings). The search field from the example becomes : sender.forumName
.
Note: Searchable reference/composite properties need to be part of an inverse relation to keep the index of the owning entity updated with changes in its reference entity/entities. The mapping options available for reference properties are restricted to name
and subclass
.
In case no analyzer is specified for a numeric property search field, it will be indexed as numeric fields, which is a special type of field in Lucene. It enables efficient range queries and sorting on this field.
"},{"location":"reference/advanced-search/#derived-properties","title":"Derived properties","text":"Derived properties are currently only indexed when the entity owning this property is saved/changed.
"},{"location":"reference/advanced-search/#how-to-analyze-your-dataqueries","title":"How to analyze your data/queries","text":"By default, textual properties will use the default analyzer from Lucene, which is optimized for the English language. In the specification of a search field (in search mapping or searchable annotation), a different analyzer can be assigned to it like is done for the textSnowBall
search field. A custom analyzers can be declared, each containing:
The range of tokenizers and filters that are supported can be found here and here (with more information about specific analyzers). You don't need to use the factory keyword at the end. Useful analyzers definitions are already included in a new WebDSL project under ./search/searchconfiguration.app. The default analyzer can be overwritten by adding the default
keyword before analyzer
. More advanced analysis may require different behavior at search and query time. Using the index { ... }
and query { ... }
block, the analyzers may be specified different for indexing and query time (see the synonym analyzer).
For each indexed entity, search functions and a searcher class are automatically generated. For simple searches, the generated functions will suffice. For more advanced searches, the magic is in the generated entity searcher(s).
"},{"location":"reference/advanced-search/#search-data-using-generated-search-functions","title":"Search data using generated search functions","text":"For the example entity Message
, the following search functions are generated.
function searchMessage(query : String) : [Message]\nfunction searchMessage(query : String, limit : Int) : [Message]\nfunction searchMessage(query : String, limit : Int, offset : Int) : [Message]\n
The limit and offset parameters can be used for paginated results. It only loads at most the limit
number of results from the database (for efficiency/faster pageloading). These functions use the default search fields when searching, and the specified analyzers are applied for each search field."},{"location":"reference/advanced-search/#search-data-using-webdsl-search-language-for-full-text-search","title":"Search data using WebDSL search language for full text search","text":"More features are available when using WebDSL's search language designed to perform search operations. The language let you interact with the generated Searcher object for the targeted entity. A reference to (or initialization of) a searcher is followed by one or more constructs in which search criteria can be declared.
//matches Messages with \"tablet\", but without \"ipad\"\nvar msgSearcher := search Message matching +\"tablet\", -\"ipad\";\n\n//enable faceting on an existing searcher\nmsgSearcher := ~msgSearcher with facets sender.forumName(20), category(10)\n
List of search language constructs:
"},{"location":"reference/advanced-search/#retrieving-search-results","title":"Retrieving search results","text":"var searcher := search Book matching author: \"dahl\";\nvar results := searcher.results(); //returns [Book];\n
Calling .results() on a searcher returns the search results. Calling .count() on a searcher returns the total number of results."},{"location":"reference/advanced-search/#simple-and-boolean-queries-matching-field-qexp","title":"Simple and boolean queries: 'matching { [{field ,}:] {qExp ,} ,}'","text":"searcher := search Entity matching title: \"user interface\";\nsearcher := search Entity matching title, description: userQuery; \nsearcher := search Entity matching \"user interface\";\nsearcher := search Entity matching title: +userQuery, -\"case study\"; \nsearcher := search Entity matching ranking:4 to 5, title:-\"language\"; \n
Declares a searcher that matches a simple or boolean query. Fields are optional: if the query expression is not preceded by a field constraint, the default search fields are used (i.e. all search fields if no default fields are defined, see ...). qExp can be any String compatible WebDSL expression or a range expression optionally prefixed with a boolean operator (+ for must, - for mustnot, nothing for should)."},{"location":"reference/advanced-search/#range-queries","title":"Range queries","text":"searcher := search Entity matching rating: {1 to 3}\nsearcher := search Entity matching rating: [startDate to endDate]\nsearcher := search Entity matching rating: -[* to sinceDate]\n
Range expressions are in the form [minExp to maxExp] (including min and max value) or {minExp to maxExp} (exludes min and max, where both expressions can be any expression of a simple WebDSL builtin type. An open range is specified with an asterisk : [* to \"A\"} for example."},{"location":"reference/advanced-search/#pagination","title":"Pagination","text":"var searcher := search Book matching author: \"dahl\" start 20 limit 10\n
With the start and limit keywords, you can control which results to be retrieved."},{"location":"reference/advanced-search/#configuration-options-option","title":"Configuration options: '[ {option* ,} ]'","text":"searcher := search Entity on title: q [no lucene, strict matching];\n
Declare the searcher's options. Available options are: searcher := search Entity matching title: \"graph\" \n with filter hidden:false;\n
Specify a filter constraint. A filter constraint is a field-value expression. Be aware that when using a filter, a bitset is constructed and cached to accelerate future queries using the same filter. Filters are not considered in result ranking. Thus, only use field-value filters if you expect the same filtering to occur frequently."},{"location":"reference/advanced-search/#enabling-facets-with-facets-field1e1-field2e2","title":"Enabling facets: 'with facet(s) field1(e1), field2(e2)'","text":"Example:
searcher := search Entity matching title: \"graph\" with facet author(10);\nsearcher := search Entity matching title: \"graph\" with facets author(10), rating([* to 1],[2 to 3},[3 to 4},[4 to *]);\n
Specify enabled facets. These can be discrete or range facets"},{"location":"reference/advanced-search/#retrieving-facets-field-facets-from-searcherexp","title":"Retrieving facets: 'field facets from searcherExp'","text":"facets := author facets from s;\n
Returns a list: [Facet]
with the facets for the specified field. Facet objects have the following boolean functions available, for example to apply different styling on the variety of facet states: searcher := ~searcher with filter(s) selectedDateFacet.must(), selectedPriceFacet.must();\n
Previously returned facets can be used to narrow the search results. The behaviour of the facet (must, should, mustnot) can be set on the facet object itself (should by default)."},{"location":"reference/advanced-search/#namespace-scoping-in-namespace-e1","title":"Namespace scoping: 'in namespace e1'","text":"searcher := search Entity matching title: \"graph\" in namespace \"science\";\n
When using search namespaces, restricting a search to a single namespace is done using the in namespace construct followed by a String-compatible expression."},{"location":"reference/advanced-search/#search-data-using-native-java-instead-of-search-language-some-expert-features","title":"Search data using native java instead of search language (some expert features)","text":"The searcher class that is created for the example Message
entity is MessageSearcher
. The first advantage of using this searcher instead of the generated functions is the ability to interact with the searcher, for further refinements to the search query, or to get information like the total number of results, or time that was needed to perform the search.
page searchPage(query : String) {\n var searcher := MessageSearcher().query(query);\n var results := searcher.results();\n var searchTime := searcher.searchTime(); //String\n\n \"You searched for '\" output(searcher.query()) \"', \" output(searcher.count()) \" results found in \" output(searchTime) \".\"\n\n if(searcher.count() > 0) {\n showResults(results)\n }\n}\ntemplate showResults(results : [Message]) {\n //code to view results\n} \n
The available searcher functions generated for each searchable entity are:
"},{"location":"reference/advanced-search/#disallow-use-of-lucene-in-query-and-filter-values","title":"(Dis)Allow use of Lucene in query and filter values","text":"(see here)
allowLuceneSyntax(allow : Bool) : EntitySearcher\n
"},{"location":"reference/advanced-search/#orand-terms-in-user-queries-by-default","title":"OR/AND terms in user queries by default","text":"OR is the default.
defaultAnd() : EntitySearcher\ndefaultOr() : EntitySearcher\n
"},{"location":"reference/advanced-search/#filter-results-by-field-value-get-filter-value","title":"Filter results by field value, get filter value","text":"addFieldFilter(field : String, value : String) : EntitySearcher\ngetFieldFilterValue(field : String) : String\ngetFilteredFields() : [String]\nremoveFieldFilter(field : String)\nclearFieldFilters()\n
"},{"location":"reference/advanced-search/#get-spellautocomplete-suggestions","title":"Get spell/autocomplete suggestions","text":"The field(s) parameters specify which search field(s) to use for suggestions. 'limit' controls the max number of suggestions to retrieve. Additionally the namespace can be specified, if used. For spell suggestions the accuracy [0..1] can be set
static autoCompleteSuggest(toComplete : String, field : String, limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, namespace : String, field : String, limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, fields : [String], limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, namespace : String, fields : [String], limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, fields : [String], accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, namespace : String, fields : [String], accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, field : String, accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, namespace : String, field : String, accuracy : Float, limit : Int) : [String]\n
"},{"location":"reference/advanced-search/#indecrease-the-impact-of-a-search-field-in-ranking-of-results-by-boosting-at-query-time","title":"In/Decrease the impact of a search field in ranking of results by boosting at query-time","text":"boost(field : String, boost : Float) : EntitySearcher\n
"},{"location":"reference/advanced-search/#faceting-on-a-search-field","title":"Faceting on a search field","text":"The max
parameter defines the maximum facets to collect for that field. For range facets, the ranges are encoded as String in the same format as range queries. Multiple ranges can be specified concatenated, optionally seperated with a symbol like white space or comma but that's not required.\"
enableFaceting(field : String, max : Int) : EntitySearcher\nenableFaceting(field : String, rangesAsString : String) : EntitySearcher\ngetFacets(field : String) : [Facet]\naddFacetSelection(facet : Facet) : EntitySearcher\naddFacetSelection(facets : [Facet]) : EntitySearcher\ngetFacetSelection() : [Facet]\ngetFacetSelection(field : String) : [Facet]\nremoveFacetSelection(facet : Facet) : EntitySearcher\nclearFacetSelection() : EntitySearcher\nclearFacetSelection(field : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#specify-search-fields-to-use-for-query-or-range","title":"Specify search field(s) to use for query or range","text":"field(field : String) : EntitySearcher\nfields(fields : [String]) : EntitySearcher\n
"},{"location":"reference/advanced-search/#specify-offset-and-number-of-results-for-pagination","title":"Specify offset and number of results (for pagination)","text":"setOffset(offset : Int) : EntitySearcher\nsetLimit(limit : Int) : EntitySearcher\n
"},{"location":"reference/advanced-search/#hit-highlighting","title":"Hit highlighting","text":"Highlight matched tokens using the analyzer from the specified search field in a given text, optionally specifying a pre- and posttag (bold by default), number of fragments, fragment length and fragment separator. There are 4 types of highlight methods. Replace highlight with the version that is suitable for you:
highlight(field : String, toHighlight : String) : String\nhighlight(field : String, toHighlight : String, preTag : String, postTag : String) : String\nhighlight(field : String, toHighlight : String, preTag : String, postTag : String, nOfFrgmts : Int, frgmtLength : Int, frgmtSeparator : String) : String\n
"},{"location":"reference/advanced-search/#find-similar-entities-based-on-text-fragment","title":"Find similar entities based on text fragment","text":"Just like an ordinary query, first specify the fields using the field(s)
function
moreLikeThis(text : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#setget-the-current-text-query","title":"Set/get the current text query","text":"Note: Query text from the first specified query is returned in case multiple queries are combined using boolean queries.
getQuery() : String\nquery(queryText : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#sort-results-by-field-ascending-or-descending","title":"Sort results by field ascending or descending","text":"sortDesc(field : String) : EntitySearcher\nsortAsc(field : String) : EntitySearcher\nclearSorting() : EntitySearcher\n
"},{"location":"reference/advanced-search/#range-query-start-and-end-can-be-type-of-string-int-float-and-datedatetimetime-start-and-end-are-included-by-default","title":"Range query, start and end can be type of String, Int, Float and Date/DateTime/Time. start and end are included by default","text":"range(start, end) : EntitySearcher\nrange(start, end, includeMin : Bool, includeMax : Bool) : EntitySearcher\n
"},{"location":"reference/advanced-search/#setget-namespace","title":"Set/get namespace","text":"setNamespace(ns : String) : EntitySearcher\ngetNamespace() : String\nremoveNamespace() : EntitySearcher\n
"},{"location":"reference/advanced-search/#get-the-list-of-results","title":"Get the list of results","text":"results() : [Entity]\n
"},{"location":"reference/advanced-search/#get-the-number-of-results","title":"Get the number of results","text":"count() : Int\n
"},{"location":"reference/advanced-search/#get-the-search-time","title":"Get the search time","text":"searchTime() : String\nsearchTimeMillis() : Int\nsearchTimeSeconds() : Float\n
"},{"location":"reference/advanced-search/#filters","title":"Filters","text":"Filters are an efficient way to filter search results, because they are cached. If you expect to perform many queries using the same filter (like only showing Message
s in a specific category), using a filter is the way to go:
MessageSearcher.query(userQuery).addFieldFilter(\"category\",\"humor\")\n
or
search Message matching userQuery with filter category:\"humor\"\n
To get the value of a previously added field filter, use the getFieldFilterValue(field : String)
method.
Search namespaces become usefull if you want to allow searches on entities with some specific property value. For example searching Message
s by category in the above example. Namespaces have some advantages over using field filters. An index is created for each namespace separately, instead of one for all entities of that type. Since the indexes are used as input for auto completion and spell checking, the use of namespaces enables suggestion services scoped to one, or all, namespace(s).
Facets can be displayed in many contexts. For example, when displaying a list of products, you want the product categories to be displayed as facets. Any searchable property can be used for faceting. The values, as they appear in the search index, are used for faceting. So if you use the default analyzer for the category property of Product, categories containing white spaces are not treated as single facet value. For this to work you need to define an additional field which doesn't tokenize the value of the property, for example by indexing this property untokenized:
entity Product{\n name : String\n categories : {Category} (inverse=Category.products)\n\n search mapping{\n name\n categories\n }\n}\nentity Category {\n name : String\n products : {Product}\n\n search mapping{\n name using none //or 'name using no' in v1.2.9.0\n }\n}\n
Facets can be retrieved through the use of a searcher. You first need to specify the facets you want to use by enabling them in the searcher. A typical example is to display facets in the search results:
(updated April 5th)
template searchbar(){\n var query := \"\";\n form {\n input(query)\n submit action{\n //construct a searcher and enable faceting on tags.name, limited to 20 top categories\n //more facets can be enabled by separating the field(topN) facet definitions by a comma\n var searcher := search Product matching query with facets categories.name(20);\n return search(searcher);} {\"search\"}\n }\n}\n\npage search(searcher : ProductSearcher){\n var results : [Product] := results from searcher;\n var facets : [Facet] := categories.name facets from searcher;\n\n header{\"Filter by product category:\"}\n for(f : Facet in facets){\n facetLink(f, searcher)\n }separated-by{\" \"}\n\n showResults(results)\n}\ntemplate facetLink(facet: Facet, searcher: ProductSearcher){\n submitlink narrow(facet){ if(facet.isSelected()){\"+\"} output(facet.getValue()) }\"(\" output(facet.getCount()) \")\"\n\n action narrow(facet : Facet){\n if (facet.isSelected()) { searcher.removeFacetSelection(facet); } else { ~searcher matching facet.must(); }\n goto search(searcher);\n }\n}\n
"},{"location":"reference/ajax/","title":"Ajax","text":"WebDSL provides ajax operations which allow you to easily replace an element or group of elements in a page without reloading the entire page. These operations can be used as statements inside actions or functions.
"},{"location":"reference/ajax/#ajax-operations","title":"Ajax Operations","text":"replace(target, templatecall);\nappend (target, templatecall);\nclear (target);\n\nrestyle (target, String);\nrelocate(pagecall);\nrefresh();\n\nvisibility(target, show/hide/toggle);\n\nrunscript(String);\n
The most commonly used operation is replace, for example:
action someaction() {\n replace(body, showUser(user)); \n}\n
This will replace the contents of an element or placeholder (see ajax targets section below) that has id 'body' with the output of the templatecall showUser(user)
.
runscript
provides a way to interface with arbitrary Javascript code. Put .js files in a javascript directory in the root of your project and include it using includeJS
in a template, e.g. includeJS(\"sdmenu.js\")
.
Example: Moving a div around using JQuery animate:
runscript(\"$('\"+id+\"').animate({left:'\"+x+\"', top:'\"+y+\"'},1000);\");\n
The refresh action is the default action; when no other interface changing operations are executed the browser will just refresh the current page. For example an input form which submits to a completely empty action results in the data being saved (default behavior) and the page being refreshed (default behavior). If good way to avoid refreshing (if this is really what you want), is to use runscript(\"\")
. Note that it's good practice to return a confirmation message instead of doing nothing.
There are three ways to target an ajax operation. target can either be
Placeholder with non-ajax default content:
placeholder leftbar { \"default content\" /* non-ajax default elements */ }\n
Placeholder with ajax default content:
placeholder leftbar ajaxTemplateCall() /* call to ajax template*/\n
Id attribute example:
table[id := myfirsttable] { /* non-ajax default elements */ }\n
When a template is used as target in an ajax operation, it must be declared with the ajax
modifier.
Example:
template testtemplate(p:Person){\n placeholder testph{ \"no details shown\" }\n submit(\"show details\",show())[ajax]\n action show(){\n replace(testph,showDetails(p));\n }\n}\najax template showDetails(person:Person){\n \" name: \" output(person.name)\n}\n
Since an ajaxtemplate results in an extra entry point at the server, it must be explicitly opened when access control is enabled:
rule ajaxtemplate showDetails(p:Person){true}\n
"},{"location":"reference/ajax/#dom-event-handling","title":"DOM Event Handling","text":"To invoke actions when an HTML event is fired, for example when pressing a key, event attributes can be defined on elements. The syntax of such an attribute is:
<event name> := <action call>\n
W3schools.com provides an overview of many available events. Note that onmouseenter
and onmouseleave
are also available, but not documented by W3Schools. Example:
\"quicksearch: \" \ninput(search)[onkeyup := updatesearch(search)]\n
The result is that the updatesearch
action is invoked on the server.
Typically you should not make a form cross an ajax placeholder. The server considers ajax templates as self-contained components similar to pages.
Example of proper usage:
template demo(){\n placeholder test()\n}\najax template test(){\n form{ input(someGlobal.name) submit action{} {\"save\"} }\n}\n
Example of incorrect usage (the submit will be contained in a form on the client but not on the server):
template demo(){\n form{\n placeholder test()\n }\n}\najax template test(){\n input(someGlobal.name) submit action{} {\"save\"}\n}\n
In some cases interaction between a regular form and ajax operations is not an issue, e.g. when the ajax template does not contain any input elements. The most common case is rendering validation messages in the form, this behavior is provided in the WebDSL library, see next section.
"},{"location":"reference/ajax/#ajax-input-validation","title":"Ajax Input Validation","text":"There are prebuild library components for creating inputs with ajax validation responses.
A simple example:
template demo(){ \n var s := \"test\"\n form{ \n inputajax(s)\n submit action{ log(s); } {\"log\"}\n }\n}\n
"},{"location":"reference/app-configuration/","title":"App Configuration","text":"In the application.ini file compile-, database- and deployment information is stored. Executing the webdsl command in a certain directory will look for a application.ini file to obtain compilation information. If no such file was found, it will start a simple wizard to create one.
Example application.ini:
backend=servlet\ntomcatpath=/opt/tomcat\nappname=hello\ndbserver=localhost\ndbuser=webdsluser\ndbpassword=webdslpassword\ndbname=webdsldb\ndbmode=update\nsmtphost=localhost\nsmtpport=25\nsmtpuser=\nsmtppass=\n
"},{"location":"reference/app-configuration/#required-configuration","title":"Required Configuration","text":"backend The back-end target platform of the application. Currently, the servlet back-end is only up-to-date.
appname The name of the application to compile. The compiler will look for a APPNAME.app file to compile. This name will also become the servlet name and show up as part of the URL. By renaming the generated APPNAME.war file to ROOT.war and then deploying it, the application name will not be in the URL.
tomcatpath This field should contain the root directory of the Tomcat installation. For example /opt/tomcat. It is used when executing 'webdsl deploy'.
"},{"location":"reference/app-configuration/#database-configuration-mysql","title":"Database Configuration MySQL","text":"dbmode This field indicates if the application should try to create tables in a database, or try to sync it with the existing schema to avoid loss of data. Valid values are create-drop, update, and false. Update can lead to unpredictable results if data model is changed too much. For production deployment use 'export DBMODE=false'.
dbserver Location of the Mysql server, which will be used in the connection URL, e.g. 'localhost'.
dbuser User to be used for connecting to the MySQL database.
dbpassword Password for the specified user.
dbname Database name, note that the database needs to exist when the application is run. The 'webdsl' script will try to create the database in the wizard, but manually creating it via command-line or MySQL Administrator is also possible.
"},{"location":"reference/app-configuration/#database-configuration-h2-database-engine-in-file","title":"Database Configuration H2 Database Engine in file","text":"db Set db=h2
to enable H2 Database Engine instead of the default MySQL.
dbfile H2 database file, an empty file will be populated with tables automatically, when using 'create-drop' or 'update' db modes.
dbmode Same as for MySQL.
"},{"location":"reference/app-configuration/#database-configuration-h2-database-engine-in-memory","title":"Database Configuration H2 Database Engine in memory","text":"db Set db=h2mem
to enable in-memory H2 Database Engine instead of the default MySQL.
dbmode Same as for MySQL, although effectively the tables are always dropped after a restart with in-memory database
"},{"location":"reference/app-configuration/#database-configuration-through-jndi","title":"Database Configuration through JNDI","text":"db Set db=jndi
to retrieve a JDBC resource from the application server, rather than providing the configuration in the web application.
dbjndipath JNDI path to the JDBC resource. On Apache Tomcat this is typically prefixed by 'java:comp/env'. An example may be: 'java:comp/env/jdbc/mydatabase'
dbmode Same as for MySQL.
Apart from settings in the application.ini, also a Context XML file must be provided for Apache Tomcat. An example may be:
<Context>\n <Resource name=\"jdbc/mydatabase\"\n auth=\"Container\"\n type=\"javax.sql.DataSource\"\n driverClassName=\"com.mysql.jdbc.Driver\"\n maxActivate=\"100\" maxIdle=\"30\" maxWait=\"10000\"\n username=\"root\" password=\"dbpassword\"\n url=\"jdbc:mysql://localhost:3306/mydatabase?useServerPrepStmts=false&characterEncoding=UTF-8&useUnicode=true&autoReconnect=true\" />\n</Context>\n
This XML file must be stored in: $TOMCAT_BASE/conf/Catalina/localhost/<appname>.xml
"},{"location":"reference/app-configuration/#email-configuration","title":"Email Configuration","text":"smtphost SMTP host for sending email, e.g. smtp.gmail.com
smtpport SMTP port for sending email, e.g. 465
smtpuser SMTP username
smtppass SMTP password
smtpprotocol smtpprotocol=smtps
[smtp/smtps] Use smtp or smtps as protocol.
smtpauthenticate smtpauthenticate=true
[true/false] Authenticate with a username and password.
indexdir set the index directory, default is /var/indexes.
searchstats Enable/disable search statistics, which can be displayed using template showSearchStats(). Default is false.
"},{"location":"reference/app-configuration/#optional-configuration","title":"Optional Configuration","text":"rootapp rootapp=true
will deploy the application as root application, it will not have the application name prefix in the URL.
wikitext-hardwraps wikitext-hardwraps=true
will enable so-called hard wraps in markdown. This way, each newline which isn't followed by 2 white spaces is also rendered as new line. Default is false. See http://yellowgrass.org/issue/WebDSL/818
appurlforrenderwithoutrequest (as of WebDSL 1.3.0) Sets the URL to be used when links to pages are to be rendered outside a request. Normally, WebDSL will construct links using the request URL as a base. In case pages or templates with links are to be rendered outside a request (e.g. using a background task), WebDSL will use this property value as the base url.
sessiontimeout Sets the session timeout, specified in minutes.
javacmem javacmem=3G
set javac max memory for compilation of generated Java classes
debug debug=true
will show queries and Java exception stacktraces in the log.
verbose verbose=2
will show more info during compilation, mainly for developers.
fastpp fastpp=true
will make the compiler write Java code faster (writing files stage), however, it also becomes less readable. (only for C-based back-end of the WebDSL compiler)
For the webdsl tomcatdeploy
and webdsl tomcatundeploy
commands to work, a user has to be configured in Tomcat (tomcat/conf/tomcat-users.xml). For example:
<tomcat-users>\n <role rolename=\"manager\"/>\n <user username=\"tomcat\" password=\"tomcat\" roles=\"manager\"/>\n</tomcat-users>\n
The tomcat manager URL and username and password can be set in the application.ini file (defaults are listed as examples):
tomcatmanager tomcatmanager=http:\\\\localhost:8080\\manager
URL to Tomcat manager
tomcatuser tomcatuser=tomcat
manager user declared in tomcat/conf/tomcat-users.xml
tomcatpassword tomcatpassword=tomcat
password for that user
The quickest way to get an application running is to execute:
webdsl run appname\n
This will generate an application.ini file with default settings, then compile the application, and start a Tomcat instance on port 8080 with the application deployed.
If there is already an application.ini file with settings that have to be used, execute:
webdsl run\n
This will also build and run, using the settings in the existing application.ini file.
To create just the war file instead, use:
webdsl war\n
"},{"location":"reference/cli-usage/#building-war-file-and-deploying-to-external-tomcat","title":"Building .war file and deploying to external Tomcat","text":"The installation of WebDSL will result in a webdsl script and a directory with templates being added to your install location. The script is used to invoke the compilation and deployment of WebDSL applications.
In your console, go to the location of the main .app file and invoke the webdsl script with
webdsl build\n
The script uses an application.ini file for configuration. If an application.ini file is not in the current directory, the script will offer an interactive way to generate it. If the application.ini is available it will be used to configure the application with e.g. database connection settings. The compilation begins by creating a .servletapp directory to which the WebDSL template, the application files, and the static resources are copied. Then the actual WebDSL compiler, webdslc, is invoked. This will either produce an error and halt, or it will produce the source code of a java web application. Upon a successful run of the webdsl compiler, the script will compile the java code, and build a war file. This war file can be copied manually to the tomcat /webapps dir, or it can be uploaded through the web deploy interface of tomcat. If the tomcat path is set in application.ini, then
webdsl deploy\n
will copy the war file to the /webapps directory.
If you have updated webdsl and need to copy the new WebDSL template in .servletapp use
webdsl cleanall\n
to remove the .servletapp directory (or simply delete it with rm) and then do a build.
The script commands can be combined, e.g.
webdsl cleanall build deploy\n
to clean the generated directory and its contents, regenerate, and deploy.
"},{"location":"reference/cli-usage/#example-application","title":"Example Application","text":"1 create a hello.app file
hello.app:
application test\n\npage root(){\n \"Hello world\"\n}\n
create or generate application.ini:
backend=servlet\ntomcatpath=**path to your tomcat directory e.g. /Apps/tomcat/**\nappname=hello\ndbserver=localhost\ndbuser=**mysql user account, e.g. root**\ndbpassword=**password**\ndbname=webdsldb\ndbmode=create-drop\nsmtphost=localhost\nsmtpport=25\nsmtpuser=\nsmtppass=\n
2 create the database
mysql -u root -p
create database webdsldb;
exit
3 start tomcat in another shell:
catalina.sh run (stop with cmd/ctrl+c)
or in the background
catalina.sh start (stop with catalina.sh stop)
4 compile and deploy WebDSL app
webdsl cleanall deploy
5 open browser and go to http://localhost:8080/hello
"},{"location":"reference/development/","title":"Development","text":"This section contains information for developers of WebDSL.
"},{"location":"reference/development/#debugging-the-generated-code-developers","title":"Debugging the generated code (developers)","text":"When running an application entirely in the Eclipse environment, you can choose to start debug mode in the 'Servers' view.
On the command-line, 'webdsl run' will set up the remote debugger interface, on the usual port 8000. Then set it up in eclipse: Run menu -> debug configurations\u2026 -> click on remote java applications -> new (icon top left) -> add source dirs of your project -> press 'debug'
"},{"location":"reference/entities/","title":"Entities","text":"Data models in WebDSL are defined using entity definitions. An entity definition consists of the entity's name, possibly a super-entity from which it inherits, 0 or more properties and 0 or more entity functions:
entity User {\n name : String (length = 25)\n email : Email\n password : Secret\n homepage : URL\n pages : {Page}\n function checkPassword( s: String ): Bool { \n return password.check( s );\n }\n predicate sameUser( u: User ){ this == u } \n}\n
A property consists of 3 parts:
a name
a property type, e.g. value types String, Int, Long, Text or reference/composite types which refer to other entities, such as Person, {Person} (set), and [Person] (list).
For a complete overview of the available types, see Types.
An example data model for a blogging site:
entity Author {\n name : String\n email : Email\n password : Secret\n posts : {Post} (inverse = author)\n}\n\nentity Post {\n author : Author\n title : String\n text : Text\n comments : {Comment} (inverse = post)\n}\n\nentity Comment {\n post : Post\n author : String\n text : Text\n}\n
"},{"location":"reference/entities/#instantiating-entity-objects","title":"Instantiating Entity Objects","text":"Instantiating new entity objects is done with the following expression:
Entity{ [property := value]* }\n
The entity name followed by an optional list of property assignments between curly brackets.
Example:
User{}\nUser{ name := \"Alice\" }\nUser{ name := \"Bob\" age := 34 }\n
Default initialization (what you would put into the constructor of an object in e.g. the Java programming language), can be added by extending the constructor function that is implicitly called.
Example:
entity A : B{\n extend function A(){\n name := name +\"A\";\n } \n}\n\nentity B{\n extend function B(){\n name := name +\"B\";\n } \n}\n\ntest constructors {\n var t := A{};\n assert(t.name == \"BA\");\n}\n
Creating an empty entity which doesn't call the constructor extensions can be done using createEmptyEntity, e.g. createEmptyUser()
The 'name' property is special, it is declared for each entity. By default it is a derived property that simply returns the id of the entity (which is also a special property declared for each entity, id:UUID is set automatically). The name can be customized by declaring a real name property:
name : String\n
Or derived name property:
name : String := firstname + lastname\n
Or by declaring a property as the name using an annotation:
someproperty : String (name)\n
The name property is used in input
and select
template elements to refer to an entity. Example:
application exampleapp\ninit{\n var u := User{};\n u.save();\n u := User{};\n u.save();\n u := User{};\n u.save();\n}\nentity User{} \nentity UserList{\n users : [User]\n}\nvar globalList := UserList{}\n\npage root {\n for( u in globalList.users ){\n output(u.name) //there is always a name property\n }\n form{ \n input( globalList.users ) //this will show three UUIDs as options\n submit action{ }{ \"save\" }\n }\n}\n
If the name is not a real property, you cannot create an input for it or assign to it.
Note that because 'name' is a derived property, by default you can't assign to it. To make 'name' mutable, you need to explicitly add it to the entity in one of the ways described above.
"},{"location":"reference/entities/#derived-properties","title":"Derived Properties","text":"A derived property is a property of an entity (or session entity) which is always equal to the result of an expression. The expression may reference other fields in the entity, but doesn't need to. Derived properties are always read-only. An example:
entity User {\n firstname: String\n lastname: String\n name : String := firstname + lastname\n}\n
"},{"location":"reference/entities/#list-of-field-properties-for-entities","title":"List of field properties for entities","text":"not null
. Makes sure a field isn't nulldefault=<value>
. Sets a default value. Note that this is different from derived propertiesallowed
. See specific docs The allowed
annotation for entity properties provides a way to restrict the choices the user has when the property is used in an input:
entity Person{\n friends : {Person} (allowed = from Person as p where p != this)\n}\nvar p1 := Person{}\npage root {\n form {\n input( p1.friends )\n submit action{ }{\"save\"}\n } \n}\n
The allowed collection can be accessed through an entity function with name allowed[PropertyName]
, e.g. p1.allowedFriends()
Entities can inherit properties and functions from other entities, like subclassing in Object-Oriented programming.
Example:
entity Sub : Super {\n str : String\n}\nentity Super {\n i : Int\n}\nfunction test(){\n var e1 := Sub{ i := 1 str := \"sdf\" };\n var e2 := Super{ i := 1 };\n}\n
Subclass entities can be passed whenever an argument of one of its super types is expected.
Example:
function test(){\n var e1 := Sub{ i := 1 str := \"sdf\" };\n test(e1);\n}\nfunction test(s:Super){\n log(s.i);\n}\n
Checking the dynamic type of an entity can be done using isa
and casting is performed using as
.
Example:
function test(s:Super){\n if(s isa Sub){\n var su :Sub := s as Sub;\n log(su.str);\n }\n}\n
When specifically want to call a function from the Superclass, use the 'super' keyword.
Example:
entity Sub : Super {\n function foo() : Int {\n return super.foo();\n }\n}\nentity Super {\n function foo() : Int {\n return 42;\n }\n}\n
"},{"location":"reference/entities/#generated-properties-for-entities","title":"Generated Properties for Entities","text":"For defined entities, a number of properties are automatically generated.
"},{"location":"reference/entities/#id","title":"ID","text":"id : UUID\n
The id property is used in the database as key for the objects. The property is can only be read.
"},{"location":"reference/entities/#version","title":"Version","text":"version : Int\n
The version property is a hibernate property which auto-increases for an object that is dirty when it is written to the database.
"},{"location":"reference/entities/#created","title":"Created","text":"created : DateTime\n
The created property is a generated property which is set on the save of an object also with cascaded saves.
"},{"location":"reference/entities/#modified","title":"Modified","text":"modified : DateTime\n
The modified property is a generated property which is automatically set on flush of an dirty object.
"},{"location":"reference/entities/#generated-functions-for-entities","title":"Generated Functions for Entities","text":"For defined entities, a number of global functions are automatically generated. Replace Entity with the defined entity name below.
"},{"location":"reference/entities/#property-with-id-annotation","title":"Property with id annotation","text":"If the Entity has an id annotation on a property, the following functions are generated (idtype is the type of the id property):
getUniqueEntity
getUniqueEntity(id : idtype) : Entity\n
If the Entity with the given id already exists, it is returned. If it did not exist, it is created once and a flush to the database is performed (this will commit any changes made to the entities in memory, e.g. the changes from data binding of input fields), repeated calls to this function with the same argument will keep returning that created Entity.
isUniqueEntity
isUniqueEntity(ent : Entity) : Bool\n
This function returns false when the value of the id property of ent is already taken. The function returns true when the id property is not taken, but will do so only once, subsequent calls with different entities but the same id will then return false (which makes this function suitable for processing a batch of entities in an action).
isUniqueEntityId
isUniqueEntityId(id : idtype, ent : Entity) : Bool\n
This function returns false when the entity would not be unique when given the id argument. The function returns true when the entity would be unique, but will do so only once for a given id, checking a different entity with the same id will return false in the rest of the action handling.
isUniqueEntityId(id : idtype) : Bool\n
This function returns false when the given id is not available for the Entity type. The function will return true only once, to cope with batch processing.
Note that these functions use one collection per entity to determine whether an id is available, so a call to isUniqueUserId(id) can influence the result of isUniqueUser(ent).
findEntity
findEntity(id : idtype) : Entity\n
This function returns the Entity with the given id value, null if it does not exist.
"},{"location":"reference/entities/#string-property","title":"String property","text":"For each String property in an Entity, a find function is generated (repace Property with the property name):
findEntityByProperty
findEntityByProperty(val : String) : [Entity]\n
This function returns a list of all Entitys with the exact given Property value, an empty list if there are none.
findEntityByPropertyLike
findEntityByPropertyLike(val : String) : [Entity]\n
This function returns a list of all Entitys with the given Property value as substring, an empty list if there are none.
"},{"location":"reference/entities/#entity-name","title":"Entity Name","text":"Every entity has a name, which is always a string. This name can be retrieved by the automatically generated getName() function.
The name of an entity is determined as follows:
If a property of the entity has the name annotation, the name of the entity equals this property. This property must be of type String.
If a property of the entity is called 'name' and is of type String, this property determines the entity name.
Otherwise, the id of the entity (converted to its string-value) is used.
A typical scenario where these functions come in handy is a create/edit page for an entity. In the following example the isUniquePage function is used to verify that the new page has a unique identifier property:
entity Page {\n identifier : String (id, validate(isUniquePage(this), \"Identifier is taken\")\n}\npage createPage { \n var p := Page{}\n form {\n label(\"Identifier\"){input(p.identifier)}\n submit save() { \"save\" }\n action save(){\n p.save();\n message(\"New page created.\");\n return home();\n }\n } \n}\n
"},{"location":"reference/entities/#derive-crud-pages","title":"Derive CRUD Pages","text":"You can quickly generate basic pages for creating, reading, updating and deleting entities using derive CRUD -entityname-
. It will create pages that allows creating and deleting such entities, and editing of all entities of this type in the database.
Example:
application test\n\nentity User {\n username : String\n}\n\nderive CRUD User\n\n//application global var\nvar u_1 := User{username:= \"test\"}\n\npage root {\n navigate(createUser()){ \"create\" } \" \"\n navigate(user(u_1)){ \"view\" } \" \"\n navigate(editUser(u_1)){ \"edit\" } \" \"\n navigate(manageUser()){ \"manage\" }\n}\n
As the navigates indicate, the pages that are created are:
view:
page entity(arg:Entity){...}\n
create:
page createEntity(){...}\n
edit:
page editEntity(arg:Entity){...}\n
manage (delete):
page manageEntity(){...}\n
These pages are particularly useful when you're just constructing the domain model, because the generated pages are usually too generic for a real application.
"},{"location":"reference/examples/","title":"Examples","text":"Below are larger examples of WebDSL applications than those found on the other manual pages.
"},{"location":"reference/examples/#minimal-access-control-example","title":"Minimal Access Control Example","text":"application minimalac\n\n entity User {\n name : String\n password : Secret\n }\n\n init{\n var u := User{ name := \"1\" password := (\"1\" as Secret).digest() };\n u.save();\n }\n\n page root(){\n authentication()\n \" \"\n navigate protectedPage() { \"go\" }\n }\n\n page protectedPage(){ \"access granted\" }\n\n principal is User with credentials name, password\n\n access control rules\n\n rule page root(){true}\n rule page protectedPage(){loggedIn()}\n
"},{"location":"reference/examples/#example-native-interface","title":"Example Native Interface","text":"A simple application that shows the public timeline of Twitter using Twitter4J. Example project code is available here
Screenshot of result: Project files: WebDSL application with native class interface declaration:
application exampleapp\n\npage root() {\n output(TwitterReader.read(null,null))\n}\n\nnative class nativejava.TwitterReader as TwitterReader {\n static read(String,String) : [String]\n}\n
Implementation of TwitterReader.java:
package nativejava;\n\nimport java.util.*;\n\nimport twitter4j.Status;\nimport twitter4j.Twitter;\nimport twitter4j.TwitterException;\nimport twitter4j.TwitterFactory;\npublic class TwitterReader{\n\n public static List<String> read(String twitterID,String twitterPassword){\n //The factory instance is re-useable and thread safe.\n Twitter twitter = new TwitterFactory().getInstance(twitterID,twitterPassword);\n List<String> result = new LinkedList<String>();\n List<Status> statuses;\n try {\n statuses = twitter.getPublicTimeline();\n for (Status status : statuses) {\n result.add(status.getUser().getName() + \":\" + status.getText());\n }\n } catch (TwitterException e) {\n e.printStackTrace();\n }\n return result;\n }\n}\n
"},{"location":"reference/examples/#ajax-example-custom-validation","title":"Ajax Example Custom Validation","text":"This example will show a complex form that uses Ajax for custom data validation.
The full project source of this example is located here:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/manual/ajax-form-validation/
"},{"location":"reference/examples/#important-note-1-inverse-annotations","title":"Important Note 1: inverse annotations","text":"Inverse annotations can cause problems due to save cascading in WebDSL, if an inverse is made with an entity in the database, then your temporary entity will be automatically saved in the database as well.
"},{"location":"reference/examples/#important-note-2-data-validation","title":"Important Note 2: data validation","text":"Don't use WebDSL's data validation described on the Validation page in combination with this example, because validation is done with custom code here. Data validation will be integrated with ajax to more easily get the result that is implemented in this example.
We're going to create an edit page for a Person
entity:
entity Person {\n fullname : String\n username : String (name)\n parents : {Person}\n children : {Person}\n}\n
The name annotation indicates that the username
is used to refer to the Person entity in select inputs, such as those for the parents
and children
property, see the Name Property page.
The root
page includes a personedit
template and passes it a new Person
object.
page root() {\n main()\n template body() {\n personedit(Person{})\n\n }\n}\n
The personedit
template provides the form that checks values whenever changes occur. The various placeholder
elements provide a location to insert error messages. The actual checks are encapsulated in functions, this allows the save action to easily do a server-side check before saving the new Person object.
template personedit(p:Person){\n form{\n par{\n label(\"username: \"){ input(p.username)[onkeyup := action{ checkUsername(p); checkUsernameEmpty(p); checkFullname(p); }] }\n placeholder pusernameempty { }\n placeholder pusername { }\n }\n par{\n label(\"fullname: \"){ input(p.fullname)[onkeyup := action{ checkFullname(p); checkFullnameEmpty(p); } ] }\n placeholder pfullnameempty { }\n placeholder pfullname { }\n }\n par{\n label(\"parents: \"){ input(p.parents)[onchange := action{ checkParents(p); } ] }\n placeholder pparents { }\n }\n par{\n label(\"children: \"){ input(p.children)[onchange := action{ checkParents(p); } ] }\n }\n submit save() [ajax] {\"save\"} //explicit ajax modifier currently necessary in non-ajax templates to enable replace. A warning is shown in the log if this is missing.\n } \n action save(){ \n // made an issue requesting & operator :)\n var checked := checkUsernameEmpty(p);\n checked := checkUsername(p) && checked;\n checked := checkFullname(p) && checked;\n checked := checkParents(p) && checked;\n checked := checkFullnameEmpty(p) && checked;\n if(checked){\n p.save();\n return root();\n } \n }\n}\n
The function definitions perform the check, and also update the placeholders (note that placeholder names are currently global in the application). They also return the result as a boolean, so the functions can be reused in the save action. replace
calls perform the insertion of templates into placeholders, in this case the templates are just creating messages.
function checkUsernameEmpty(p:Person):Bool{\n if(p.username != \"\"){ \n replace(pusernameempty, empty());\n return true;\n } \n else {\n replace(pusernameempty, mpusernameempty());\n return false; \n }\n}\nfunction checkUsername(p:Person):Bool{\n if((from Person as p1 where p1.username = ~p.username).length == 0)){ \n replace(pusername, empty());\n return true;\n } \n else {\n replace(pusername, mpusername(p.username));\n return false; \n }\n}\n\nfunction checkFullnameEmpty(p:Person):Bool{\n if(p.fullname != \"\"){ \n replace(pfullnameempty, empty());\n return true;\n } \n else {\n replace(pfullnameempty, mpfullnameempty());\n return false; \n }\n}\nfunction checkFullname(p:Person) :Bool{\n if(p.username != p.fullname) { \n replace(pfullname, empty());\n return true;\n } \n else{\n replace(pfullname, mpfullname());\n return false; \n }\n}\n
The templates for the messages are shown below. An errorclass template is used to wrap all the errors in the same div tag with special error class, to provide a hook for CSS styling. Templates that are used in replace actions have to be declared as ajax
template. When access control is enabled the ajax templates can be protected with the ajaxtemplate
rule type.
template errorclass(){\n <div class=\"error\"> elements() </div>\n}\najax template empty { \"\" }\najax template mpusername(name: String) { errorclass{ \"Username \" output(name) \" has been taken already\" } }\najax template mpusernameempty { errorclass{ \"Username may not be empty\" } }\najax template mpfullname { errorclass{ \"Username and fullname should not be the same\" } }\najax template mpfullnameempty { errorclass{ \"Fullname may not be empty\" } }\najax template mpparents(pname : String, names : [String]){ \n errorclass{ \n \"Person\" \n if(names.length > 1){\"s\"}\n \" \" \n for(name: String in names){\n output(name)\n } separated-by {\", \"}\n \" cannot be both parent and child of \" output(pname)\n }\n}\n
This app includes some CSS for top-aligned labels (http://css-tricks.com/label-placement-on-forms/), the errors are shown on new lines and in red.
label {\n clear:both;\n float:left;\n margin:10px 0 2px 0;\n}\ninput, select {\n clear:both;\n float:left;\n}\n#errorclass {\n color: red;\n clear: both;\n float: left;\n margin:2px 0 0 0;\n}\ninput[type=\"button\"]{\n clear: both;\n float: left;\n margin: 20px 0 10px 0; \n}\n
"},{"location":"reference/examples/#edit-instead-of-create","title":"Edit instead of Create","text":"Since the example used a new Person entity, the save
controls whether the object is persisted. If the entity was already in the database, and this is an edit page, then the save wouldn't be necessary to persist changes. Unfortunately this has the side-effect that all intermediate submits (on every change) already persist the changes automatically. One way to work around this issue create a copy of the entity and use that for data binding. Instead of the save()
call, the code needs to put the changes back into the real persisted entity.
The editpage, in this case a global entity var is passed in, to demonstrate changing an entity that is persisted.
page edit(){\n main()\n template body() {\n personedit(pAlice)\n }\n}\n
The template is shown below, unchanged parts are left out. A template var that copies the original values is used for data binding, in the save action the changes are placed in the real person object. The save
call is not necessary for edits, but now the template works correctly for both edit and create actions.
template personedit(realp:Person){\n var p := Person{ username := realp.username fullname := realp.fullname children := realp.children parents := realp.parents};\n form{\n par{\n label(\"username: \"){ input(p.username)[onkeyup := action{ checkUsername(p,realp); checkUsernameEmpty(p); checkFullname(p); }] }\n\n ...\n\n action save(){ \n // made an issue requesting & operator :)\n var checked := checkUsernameEmpty(p);\n checked := checkUsername(p,realp) && checked;\n checked := checkFullname(p) && checked;\n checked := checkParents(p) && checked;\n checked := checkFullnameEmpty(p) && checked;\n if(checked){\n realp.username := p.username;\n realp.fullname := p.fullname;\n realp.parents := p.parents;\n realp.children := p.children;\n realp.save(); // does nothing in the case of an update\n return root();\n } \n }\n}\n
One of the checks needs to change, because the entity might be already in the database now.
function checkUsername(p:Person, realp:Person):Bool{\n var matches := from Person as p1 where p1.username = ~p.username;\n if(matches.length == 0 || (matches.length == 1 && matches[0] == realp)){ \n replace(pusername, empty());\n return true;\n } \n else {\n replace(pusername, mpusername(p.username));\n return false; \n }\n}\n
The full project source of this example is located here:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/manual/ajax-form-validation/
"},{"location":"reference/forms/","title":"Forms","text":"The form
element enables user input, and should include submit
or submitlink
elements to handle that user input. When pressing such a submit button/link, data binding will be performed for all inputs in the form.
form {\n var name: String\n var pass: Secret\n\n label(\"Username:\"){ input(name) }\n label(\"Password:\"){ input(pass) }\n\n submit save() { \"save\" }\n}\naction save(){\n User{ \n username := name \n password := pass.digest()\n }.save();\n}\n
"},{"location":"reference/forms/#input","title":"Input","text":"input(<expression>)
creates an input form element. Can be applied directly to the properties of an entity (e.g., input(user.name)) or to page variables.
Input widgets are determined by the type of the property passed to the input template call:
For example, to get a checkbox, use:
page root {\n var x : Bool := false\n form{\n input(x)\n submit action{ log(x); } { \"log result\" }\n }\n}\n
or:
entity TestEntity {\n x : Bool\n}\ntemplate editTestEntity (e:TestEntity){\n form{\n input(e.x)\n submit action{ } { \"update entity\" }\n }\n}\n
"},{"location":"reference/forms/#page-variables","title":"Page Variables","text":"Page and template definitions can contain variables. This example displays \"Dexter\":
page cat {\n var c := Cat { name := \"Dexter\" }\n output(c.name)\n}\n\nentity Cat {\n name : String\n}\n
These variables are necessary when constructing a page that creates a new entity instance. The instance can be created in the variable and data binding can be used for the input page elements. The next example allows new cat entity instances to be created, and the default name in the input form is \"Dexter\":
page newCat {\n var c := Cat { name := \"Dexter\" }\n form{\n label(\"Cat's name:\"){ input(c.name) }\n submit action {\n c.save();\n return showCat(c);\n } { \"save\" }\n }\n}\n
It is possible to initialize such a page/template variable with arbitrary statments using an 'init' action:
page newCat {\n var c\n init {\n c := Cat{};\n c.name := \"Dexter\";\n }\n form{\n label(\"Cat's name:\"){ input(c.name) }\n submit action {\n c.save();\n return showCat(c);\n } { \"save\" }\n }\n}\n
Be aware that these type of variables (and the init blocks) are handled separately from the other elements. They do not adhere to template control flow constructs like 'if' and 'for'; they are extracted from the definition. However, you can express such functionality in the 'init' block. For example:
error:
page bad {\n if(someConditionFunction()){\n var c := Cat{}\n }\n else {\n var c := Cat{ name := \"Dexter\" }\n }\n output(c.name)\n}\n
ok:
page good {\n var c\n init{ \n if(someConditionFunction()){\n c := Cat{}\n }\n else {\n c := Cat{ name := \"Dexter\" }\n }\n }\n output(c.name)\n}\n
"},{"location":"reference/forms/#input-for-entity","title":"input for Entity","text":"input(x, y)
can be used as input for an entity variable or for a collection of entities variable, where x
is the variable or fieldaccess and y
is the collection of options. It will create a dropdown box/select or a multi-select respectively. The name
property of an entity is used to describe the entity in a select, see Name Property.
input(x)
for an entity reference property or a collection property is the same as select
, with as options all entities of its type that are in the database.
Example:
entity User {\n username : String (name)\n teammate : User\n group : {Group}\n}\nentity Group {\n groupname : String (name)\n}\ninit { // application init\n var u := User { username := \"Alice\" };\n u.save();\n u := User { username := \"Bob\"};\n u.save();\n var g := Group { groupname := \"group 1\" };\n g.save();\n g := Group { groupname := \"group 2\" };\n g.save();\n}\npage root {\n form{\n table{\n for( u: User ){\n output(u.username)\n input(u.teammate)\n input(u.group)\n }\n }\n submit action{} { \"save\" }\n }\n}\n
input(u.teammate)
is a dropdown/select with options null
, \"Alice\", \"Bob\". input(u.group)
is a multi-select with options \"group 1\" and \"group 2\".
Example 2:
page root {\n var teammates := from User\n var groups := from Group\n form{\n table{\n for(u:User){\n output(u.username)\n input(u.teammate, teammates)\n input(u.group, groups)\n }\n }\n submit action{} { \"save\" }\n }\n}\n
Equivalent to the previous example, but using explicit selects instead.
Example 3:
var u3 := User { username:=\"Dave\" }\nvar g3 := Group { groupname:=\"group 3\" }\n\npage root {\n var teammates := [u3]\n var groups := {g3}\n form{\n table{\n for(u:User){\n output(u.username)\n input(u.teammate, teammates)\n input(u.group, groups)\n }\n }\n submit action{ }{ \"save\" }\n }\n}\n
Options are restricted in this example, null
and \"Dave\" for select(u.teammate, teammates)
and only \"group 3\" for select(u.group, groups)
The null
option for a select can be removed either by a not null
annotation on the property:
teammate : User (not null)\n
Or by setting [not null]
on the input
or select
itself:
input(u.teammate)[not null]\ninput(u.teammate, teammates)[not null]\n
"},{"location":"reference/forms/#allowed","title":"Allowed","text":"The possible options can also be determined using an annotation on the property:
group : {Group} (allowed = {g3})\n
In this case just using input(u.group)
will only show \"group 3\"
Radio buttons can be used as an alternative to select
for selecting an entity from a list of entities. The name
property, or the property with name
annotation, will be used as a label for the corresponding radio button.
entity Person {\n name : String\n parent : Person\n}\n\npage editPerson(p:Person){\n radio(p.parent, getPersonList())\n}\n
"},{"location":"reference/forms/#captcha","title":"Captcha","text":"The captcha
element creates a fully automatic CAPTCHA form element.
Example:
page root {\n var i : Int\n form{\n input(i)\n captcha()\n submit action{ Registration{ number := i }.save(); } {\"save\"}\n }\n}\n
"},{"location":"reference/https-encryption/","title":"HTTPS Encryption","text":"note: this section is outdated, it is recommended to configure HTTPS on the Nginx or Apache Httpd in front of tomcat, using HSTS policy to force all traffic over HTTPS
"},{"location":"reference/https-encryption/#tomcat-configuration","title":"Tomcat Configuration","text":"Using https requires some extra configuration when deploying to an external tomcat server, the tomcat instance used in the plugin and command-line test and run commands is already configured (note: this uses a dummy configuration which should not be used in production deployment of the app). Follow these steps to configure Tomcat 6:
Run this command and follow the instructions (note down the password):
%JAVA_HOME%\\bin\\keytool -genkey -alias tomcat -keyalg RSA\n
Then, in tomcat/conf/server.xml add (use the password entered in the keytool):
<Connector port=\"8443\" protocol=\"HTTP/1.1\" SSLEnabled=\"true\"\n maxThreads=\"150\" scheme=\"https\" secure=\"true\"\n keystoreFile=\"${user.home}/.keystore\" keystorePass=\"--password--\"\n clientAuth=\"false\" sslProtocol=\"TLS\" />\n
Read more about this topic here: http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html
"},{"location":"reference/lightweight-vps/","title":"Lightweight VPS","text":"WebDSL applications can be deployed on a light-weight server or VPS. Whether performance is acceptable depends on many factors such as complexity of the application, number of users, capacity of the server. Currently, the ram usage is usually the limiting factor. The JVM halts or gets stuck when the max heap space limit is crossed (-Xmx setting). 512mb ram, typically the lowest VPS option, can run a simple WebDSL application, but getting 1gb or 2gb is recommended.
In the rest of this section is a walkthrough of the minimal steps required for installation of a WebDSL application on an Ubuntu Server (this was for a VPS with 1gb ram).
Update packages library (run all apt-get commands as root or with sudo):
apt-get update\n
Install MySQL:
apt-get install mysql-server\n
Enter a password for the mysql root account.
Install Java, Tomcat, and other requirements for running the WebDSL compiler:
apt-get install ant unzip openjdk-7-jdk tomcat7\n
Get WebDSL compiler:
wget http://hydra.nixos.org/job/webdsl/trunk/buildJavaZip/latest/download/1/webdsl-java.zip\nunzip webdsl-java.zip\nchmod +x webdsl/bin/webdsl\nexport PATH=$PATH:/[path]/webdsl/bin/\n
Add the export PATH line to your ~/.bashrc file to make the 'webdsl' command work the next time you log in as well.
Install mail SMTP server:
apt-get install postfix\n
Choose the internet configuration, test locally with 'sendmail' command. If something is wrong in the configuration, change it with:
sudo dpkg-reconfigure postfix\n/etc/init.d/postfix reload\n
Configure WebDSL application, create application.ini:
appname=myapp\nbackend=servlet\ntomcatpath=/var/lib/tomcat7/\nhttpport=8080\nhttpsport=8443\ndbmode=update\nindexdir=/var/indexes/\ndbserver=localhost\ndbname=mydb\ndbuser=myuser\ndbpassword=mypass\nsmtphost=localhost\nsmtpport=25\nsmtpprotocol=smtp\nsmtpauthenticate=false\nrootapp=true\n
If using a gmail account to send mail instead of local SMTP server, use:
smtphost=smtp.gmail.com\nsmtpport=465\nsmtpuser=blabla\nsmtppass=thepass\n
Create database and mysql user:
mysql -u root -p\ncreate database mydb;\ngrant all privileges on mydb.* to myuser@'localhost' identified by 'mypass';\nflush privileges;\nquit\n
Open up the indexes directory (can be placed anywhere):
mkdir /var/indexes\nchown -R tomcat7 /var/indexes\n
Compile application (in this application.ini myapp.app is the main file) and deploy:
webdsl build deploy\n
Check what's going on in Tomcat using:
tail -f /var/lib/tomcat7/logs/catalina.out\n
Set Tomcat's heap higher:
nano /etc/default/tomcat7\n
Change
JAVA_OPTS=\"-Djava.awt.headless=true -Xmx128m -XX:+UseConcMarkSweepGC\"\n
to
JAVA_OPTS=\"-Djava.awt.headless=true -Xmx768m -XX:+UseConcMarkSweepGC\"\n
Restart Tomcat:
/etc/init.d/tomcat7 restart\n
Check Tomcat's current JVM arguments:
ps aux | grep tomcat\n
Tomcat will run on port 8080 instead of 80, a quick fix to get it to work on port 80 is the following:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n
"},{"location":"reference/mysql/","title":"MySQL","text":"This section gives some practical tips on working with MySQL.
"},{"location":"reference/mysql/#working-with-mysql-dumps","title":"Working with MySQL dumps","text":""},{"location":"reference/mysql/#creating-a-mysql-dump","title":"Creating a MySQL dump","text":"mysqldump -u root --single-transaction dbname > mydump.sql\n
Optionally exclude less important tables:
mysqldump -u root myapplication > dump.sql --single-transaction --ignore-table=myapplication._SecurityContext --ignore-table=myapplication._RequestLogEntry\n
"},{"location":"reference/mysql/#loading-a-small-mysql-dump","title":"Loading a small Mysql dump","text":"mysql -u root dbname < mydump.sql\n
"},{"location":"reference/mysql/#loading-a-large-mysql-dump","title":"Loading a large MySQL dump","text":"When loading large MySQL dumps (for local testing), convert them to MyISAM (lack of transactions makes it unusable for production db, but it loads a lot faster due to lack of foreign key checks).
Before loading the dump, increase these settings in /etc/my.cnf and restart MySQL to load the changed settings:
key_buffer_size=1024m\nmax_allowed_packet=1024m\n\nmysqladmin -u root shutdown\nmysqld -u root &\n
Then run
cat mydump.sql | sed s/ENGINE=InnoDB/ENGINE=MyISAM/ | mysql -u root\n
Or, if you want to create an intermediate file with the MyISAM dump first (slower):
sed s/ENGINE=InnoDB/ENGINE=MyISAM/ mydump.sql > mydump.sql.myisam\nmysql -u root dbname < mydump.sql.myisam\n
"},{"location":"reference/mysql/#mysql-settings","title":"MySQL Settings","text":"Show status of InnoDB:
mysql -u root \nshow innodb status;\n
See what MySQL is doing (e.g. expensive query):
show processlist;\n
Check the current structure of a table, including foreign key constraints. This can be helpful in resolving issues caused by db mode 'update', which only adds columns but will not change an existing column:
show create table _Alias;\n
We use the following settings for MySQL on our production server (NixOS/Linux):
[mysqld]\nkey_buffer_size = 256M\nmax_allowed_packet = 64M\nsort_buffer_size = 2M\nread_buffer_size = 2M\nmyisam_sort_buffer_size = 64M\nquery_cache_size = 128M\nmax_connections = 250\n\n[mysqldump]\nmax_allowed_packet = 16M\n\n[isamchk]\nkey_buffer = 256M\nsort_buffer_size = 256M\n\n[myisamchk]\nkey_buffer = 256M\nsort_buffer_size = 256M\n
"},{"location":"reference/native-java-interface/","title":"Native Java Interface","text":""},{"location":"reference/native-java-interface/#native-classes","title":"Native Classes","text":"Native Java classes can be declared in a WebDSL application in order to interface with existing libraries and code.
The supported elements are properties, (static) methods, and constructors. The supported types are
Both the primitive type and the object types such as int and Integer can be produced by the WebDSL call (so overloading between these types is a problem here).
Add Java classes to a nativejava/ dir next to your app file and jar files in lib/.
Example:
native class nativejava.TestSub as SubClass : SuperClass {\n prop :String\n getProp():String\n setProp(String)\n constructor()\n}\n\nnative class nativejava.TestSuper as SuperClass {\n getProp():String\n static getStatic(): String\n returnList(): List<SubClass>\n}\n\npage root() {\n var d : SuperClass := SubClass() \n output(d.getProp())\n\n var s : SubClass := SubClass()\n init{\n s.setProp(\"test\");\n }\n output(s.prop)\n\n output(SuperClass.getStatic())\n\n for(a: SubClass in d.returnList()){\n output(a.prop)\n } \n}\n
(Example taken from compiler tests, source
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/native-classes.app\nhttps://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/nativejava/TestSub.java\nhttps://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/nativejava/TestSuper.java\n
)
"},{"location":"reference/native-java-interface/#passing-a-native-java-instance-as-page-argument","title":"Passing a native java instance as page argument","text":"If you want an instance of your defined native java class to be passed as page (or ajax template) argument, the class should be serializable for WebDSL. From WebDSL version 1.3.0 and on, support is added for doing this by implementing the following 2 methods in your java class. (If you use a class defined in a library, you may need to extend this class with the following methods)
public static YourClass fromParamMap( Map<String,String> paramMap )\npublic final Map<String,String> toParamMap()\n
note: The keys in the returned Map may only consist of character classes [A-Z][a-z][0-9], values may hold any value as they get filtered. On deserialization, the static fromParamMap
method is invoked and its result is cast to the type as defined in the page/java template definition.
Examples can be found (notice the link ;)) in WebDSL's source code itself.
"},{"location":"reference/pages/","title":"Pages","text":"Pages in WebDSL can be defined using the following construct:
page [pagename]( [page-arguments]* ){ [page-elements]* }\n
There are basic output elements for structure and layout of the page, such as title
and header
.
Example:
page root {\n title { \"Page title\" }\n section {\n header{ \"Hello world.\" } \n \"Greetings to you.\"\n }\n}\n
"},{"location":"reference/pages/#page-parameters","title":"Page Parameters","text":"Pages can have parameters, and output
is used for inserting data values.
Example:
page user(u : User) {\n \"The name of this user is \" output(u.name)\n}\n
"},{"location":"reference/pages/#input-forms","title":"Input Forms","text":"The form
element in combination with submit
is used for submitting data. input
elements perform automatic data binding upon submit. For more information about forms, go to the Forms page.
Example:
page editUser(u:User){\n form{\n input(u.name)\n submit action{} { \"save\" } \n }\n}\n
"},{"location":"reference/pages/#templates","title":"Templates","text":"Pages can be made reusable by declaring them as template, and calling them from other pages or templates.
Example:
template common(){\n header{ \"my page\" }\n}\npage root(){\n common()\n}\n
"},{"location":"reference/pages/#output","title":"Output","text":"The output(<expression>)
template call is used to display a value in a page. It can also be used with Entity type expressions, and collections.
Example:
page user(u:User){\n output(u)\n}\n
The output
template can be customized for each entity type.
Example:
template output(u:User){\n \"user with name: \" output(u.name)\n}\n
"},{"location":"reference/pages/#navigate","title":"Navigate","text":"navigate <page call> { <page element*> }\n
Link to a page. For example:
page news() { \"News\" }\n
"},{"location":"reference/pages/#title","title":"Title","text":"Declares the title of the current page.
title { element* }\n
"},{"location":"reference/pages/#description","title":"Description","text":"Declares the description of a page (not visible, added as description meta tag in the head section of a page). This data is often viewed in search result snippets. Introduced in WebDSL 1.3.0.
description { element* }\n
"},{"location":"reference/pages/#section","title":"Section","text":"section { element* }\n
Indicate sections in a document; may be nested. May include a
header { element* }\n
element that indicates the section title.
"},{"location":"reference/pages/#image","title":"Image","text":"image ( <string with relative or absolute path to image> )\n
Displays an image. Images placed in an \"images\" folder in the root directory of your application will be automatically copied during deployment.
Example:
page root(){\n image(\"https://update.webdsl.org/images/WebDSL-small.png\") \n image(\"/images/WebDSL-small.png\") \n}\n
"},{"location":"reference/pages/#lists","title":"Lists","text":"Lists can be created with the list
and listitem
elements.
Example:
list {\n listitem { \"Milk\" }\n listitem { \"Potatoes\" }\n listitem { \"Cheese (lots)\" }\n}\n
"},{"location":"reference/pages/#tables","title":"Tables","text":"Tables can be created with the table
, row
, and column
elements.
Example:
table {\n row { column{ \"Username\" } column{ output(user.name) } }\n row { column{ \"Password\" } column{ \"it's a secret\" } }\n}\n
"},{"location":"reference/pages/#block","title":"Block","text":"block{ <page element*> }\nblock(String){ <page element*> }\n
Groups text; optionally defines a class for referencing in CSS. Results in a <div> element in HTML.
"},{"location":"reference/pages/#templates_1","title":"Templates","text":"Templates enable reuse of page elements. For example, a template for a footer could be:
template footer() { All your page are belong to us. }\n
This template can be included in a page with a template call:
page example(){\n footer\n}\n
Like pages, templates can be parameterized.
template edit(g:Group){\n form {\n input(g.members)\n submit action {} { \"save\" }\n } \n}\n\npage editGroup(g:Group){\n edit(g)\n}\n
"},{"location":"reference/pages/#overloading","title":"Overloading","text":"While pages must have unique names, templates can be overloaded. The overloading is resolved compile-time, based on the static types of the arguments.
template edit(g:Group){...}\ntemplate edit(u:User){...}\n\npage editGroup(g:Group){\n edit(g)\n}\n
"},{"location":"reference/pages/#dynamically-scoped-templates-redefinitions","title":"Dynamically scoped templates redefinitions","text":"Template definitions can be redefined locally in a page or template, to change their meaning in that specific context. All uses are replaced in templates called from the redefining template.
template main{\n body()\n}\ntemplate body(){\n \"default body\"\n}\npage root(){\n main\n template body(){\n \"custom body\"\n }\n}\n
"},{"location":"reference/pages/#for-loop","title":"For-loop","text":"Iterating a collection of entities or primitives can be done using a for loop. There are three types of for loops for templates:
For
for(id:t in e){ elem* }\n
This type of for loop iterates the collection produced by expression e, which must contain elements of type t. The elements in the collection are accessible through identifier id.
The collection can be filtered:
for(id:t in e filter){ elem* }\n
ForAll
This for loop iterates all the entities in the database of type t. These can also be filtered. Note that it is more efficient to retrieve the objects using a filtering query and use the regular for loop above for iteration.
for(id:t){ elem* }\n for(id:t filter){ elem* }\n
For Count
This for loop iterates the numbers from e1 to e2-1.
for(id:Int from e1 to e2){ elem* }\n
For Separator
All three template for loops can be followed by a separated-by declaration, which will separate the outputs from the for loop with the declared elem*.
separated-by{ elem* }\n
For-loop Filter
The filter part of a for loop can consist of four parts:
Where
where e1\n
e1 is a boolean expression which needs to evaluate to true for the element to be iterated.
Order By
order by e2 asc/desc\n
e2 is an expression that needs to produce a primitive type such as String or Int, which will be used to order the elements ascending or descending.
Limit
limit e3\n
e3 is an Int expression which will limit the number of elements that get iterated.
Offset
offset e4\n
e4 is an Int expression which will offset the starting element of the iteration.
Each of the four parts is optional, but they have to be specified in this order. The filtering is done in the application, so use queries instead of filters to optimize the WebDSL application.
"},{"location":"reference/pages/#xml-embedding","title":"XML Embedding","text":"XML fragments can be embedded directly in templates. This allows easy reuse of existing XHTML fragments and CSS. For example:
template main() {\n <div id=\"pagewrapper\">\n <div id=\"header\">\n header()\n </div>\n <div id=\"footer\">\n <p />\"powered by \" <a href=\"https://webdsl.org\">\"WebDSL\"</a><p />\n </div>\n </div>\n <some:tag />\n}\n
While the name and attribute names are fixed, the attribute values can be any WebDSL expression that produces a string:
template test(i : Int) {\n <div id=\"page\" + \"wrapper\" + i />\n}\n
"},{"location":"reference/pages/#include-css","title":"Include CSS","text":"The includeCSS(String)
template call allows you to include a CSS file in the resulting page. CSS files can be included in your project by placing them in a stylesheets/ directory in the project root.
Example 1:
page root() {\n includeCSS(\"dropdownmenu.css\")\n}\n
It is also possible to include a CSS file using an absolute URL.
Example 2:
page root() {\n includeCSS(\"https://webdsl.org/stylesheets/common_.css\")\n}\n
The media attribute can be set by passing it as second argument in includeCSS(String,String)
Example 3:
page root(){\n includeCSS(\"https://webdsl.org/stylesheets/common_.css\",\"screen\")\n}\n
"},{"location":"reference/pages/#include-javascipt","title":"Include Javascipt","text":"The includeJS(String)
template call allows you to include a javascript file in the resulting page. Javascript files can be included in your project by placing them in a javascript/ directory in the project root.
Example 1:
page root() {\n includeJS(\"sdmenu.js\")\n}\n
It is also possible to include a Javascript file using an absolute URL.
Example 2:
page root() {\n includeJS(\"http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js\")\n}\n
"},{"location":"reference/pages/#page-not-found-error","title":"Page Not Found Error","text":"When an invalid URL is being requested from a WebDSL application, the default response is to give a 404 error. To customize this error page, define a pagenotfound
page in your application.
Example:
page pagenotfound() {\n title{ \"myapp / page not found (404)\" }\n main()\n template body() {\n par{ \"That page does not exist!\" }\n par{ \"Maybe you can find what you are looking for using the search page.\" }\n }\n}\n
"},{"location":"reference/pages/#raw-output","title":"Raw output","text":"By default, any template content will be escaped, if you want to include a string directly in the page source use rawoutput.
Example:
rawoutput{\" \"}\n
"},{"location":"reference/pages/#html-element-attributes-on-template-call","title":"HTML Element Attributes on Template Call","text":"Setting HTML element attributes is supported for calls to built-in templates, the syntax is as follows:
templatename(...)[attrname=e, ...]{ ... }\n
attrname
is an attribute name, and e
is a webdsl expression such as \"foo\"
or \"foo\"+bar
.
Example:
page root(){ \n var somevalue := \"lo\"\n image(\"/images/logosmall.png\")[alt = somevalue+\"go\", longdesc = \"blablabla\"]\n navigate root()[title = \"root page\"]{ \"root\" }\n}\n
"},{"location":"reference/pages/#override-modifier","title":"Override Modifier","text":"Template and page definitions can be overridden using the override
modifier, e.g. to override a built-in page such as pagenotfound
:
override page pagenotfound(){\n \"page does not exist!\"\n}\n
"},{"location":"reference/pages/#sql-logging-for-page-rendering","title":"SQL Logging for Page Rendering","text":"add ?logsql
after the URL of a page to get a log of all the SQL queries executed to render that page
for applications with access control enabled, accessing this log is disabled by default, it can be enabled using an access control rule:
rule logsql { check }\n
e.g.
rule logsql { principal.isAdmin }\n
"},{"location":"reference/production-server/","title":"Production Server","text":""},{"location":"reference/production-server/#tomcat","title":"Tomcat","text":"We use the following settings for Tomcat on our production server (NixOS/Linux):
-Xms350m \n-Xss8m \n-Xmx8G \n-Djava.security.egd=file:/dev/./urandom \n-XX:MaxPermSize=512M \n-XX:PermSize=512M \n-XX:-UseGCOverheadLimit \n-XX:+UseCompressedOops\n-XX:+HeapDumpOnOutOfMemoryError \n-XX:HeapDumpPath=/var/tomcat/logs/heapdump.hprof\n
"},{"location":"reference/production-server/#details","title":"Details","text":"-Xmx8G\n
The most important setting, maximum heap space, value depends on size/number of applications, but the default setting is usually too low. If this setting is too high for your JVM, it won't start at all.
-XX:MaxPermSize=512M\n
This allows redeploying the application without running into permgenspace errors too quickly.
-Djava.security.egd=file:/dev/./urandom\n
The default implementation for random can be too slow (java.util.UUID.randomUUID is used for entity identifiers, including RequestLogEntry) see http://stackoverflow.com/questions/137212/how-to-solve-performance-problem-with-java-securerandom
"},{"location":"reference/production-server/#monitoring-and-debugging","title":"Monitoring and Debugging","text":"Use jvisualvm
to inspect the Tomcat process, this allows you to look at the heap and running threads and create dumps for later inspection. A heap dump can also be created using:
jmap -F -dump:format=b,file=<filename> <process id>\n
The Eclipse Memory Analyzer can be used to inspect this file, get it from http://www.eclipse.org/mat/
If the tomcat process becomes unresponsive try
kill -3 <process id>\n
to generate a thread dump in the catalina.out log.
"},{"location":"reference/production-server/#mysql","title":"MySQL","text":"See the MySQL page.
"},{"location":"reference/production-server/#lightweight-vps","title":"Lightweight VPS","text":"See the Lightweight VPS page, it also contains a step-by-step installation guide.
"},{"location":"reference/queries/","title":"Queries","text":"WebDSL supports a subset of HQL. You can use these queries directly in your webdsl program, by for example assigning them to variables. An example:
// select all updates\n var allUpdates : [Update] := from Update;\n
"},{"location":"reference/queries/#escaping","title":"Escaping","text":"Often it is useful to use values from your WebDSL program inside a query. To do this you can escape WebDSL expressions by prefixing them with a tilde (~
).
To escape to WebDSL inside a query, prefix the expression with ~. For example:
// Select all users whose username is \"zef\"\n var username : String := \"zef\";\n var users : [User] := from User as u where u.username = ~username;\n
"},{"location":"reference/queries/#notes","title":"Notes","text":"offset
. Even though offset
is valid SQL, it's not a valid keyword to use in WebDSL queries, even though it is a valid keyword in other places in WebDSL so will be highlighted in your editor.// A paginated for, showing 10 items per page and prefetch the \"user\" column (in a template)\nvar page : Int := 0;\nfor(update : Update in from Update as u left join fetch u.user order by u.date desc limit 10*page, 10) {\n output(update.text)\n}\n
"},{"location":"reference/recurring-tasks/","title":"Recurring Tasks","text":"Recurring task allow you to execute a certain function in set interval, e.g. every minute, 5 hours or every week. For this WebDSL uses the following syntax (which is subject to change):
function someFunction() {\n log(\"I was executed!\");\n}\n\ninvoke someFunction() every 5 minutes\n
If the called function returns anything, this value is discarded. Functions invoked in this manner have access to entities and global variables, but not session data (because the function is not invoked by a user). Syntax of the time intervals:
TimeInterval = TimeIntervalPart*\nTimeIntervalPart = Exp \"weeks\"\nTimeIntervalPart = Exp \"days\"\nTimeIntervalPart = Exp \"hours\"\nTimeIntervalPart = Exp \"minutes\"\nTimeIntervalPart = Exp \"seconds\"\nTimeIntervalPart = Exp \"milliseconds\"\n
So valid time intervals are:
1 hours // note the plural\n1 hours 10 minutes // repeat every 70 minutes\n2 weeks 10 milliseconds\n
"},{"location":"reference/request-processing/","title":"Request Processing","text":"Users interact with web applications through the browser. This process consists of request and response strings being exchanged between the web server and the browser. A form is defined by a response string, which is interpreted by the browser to produce components that allow user interaction. A user can fill in data in a text field, and press the submit button. The browser first collects the data from the form input fields, and constructs a request string to send to the web server, which receives the request string and parses it. Values from input fields can be accessed separately but are represented as strings. A web application bears the responsibility of converting these strings to actual types to be used in further processing of the request. In WebDSL, the conversion of request parameters is done automatically. This is the first phase of the request processing lifecycle. The request processing lifecycle consists of the following phases:
Request parameter conversion is not possible if the incoming value is not well-formed. For example, a value of \"3f\" cannot be converted to an integer. Since a failed conversion invalidates any input this triggers re-rendering the page with error messages.
In the first phase, parameters are decoded from strings. In the 'Update Model Values' phase, these parameters are automatically inserted in data model entities. WebDSL supports such data binding through input elements. For example, the element
input(u.email)\n
declares that an input field should be displayed with the current contents of the email property of variable u of type User. Furthermore, when a user submits the containing form with a new value in the email field, the new value will be assigned to u.email.
Data binding requires assignments to and collection operations on entity properties which trigger validation checks defined in the entity. When a property is validated each validation rule defined on that property is checked, possibly producing multiple error messages. When at least one validation fails during this phase, further processing is disabled and errors are displayed.
When the model is updated and entity validations are checked, there can still be validation rules in pages which need to be enforced. The form validation phase traverses the form that is submitted and checks any validation it encounters. An invalid result prevents any action from executing and produces an error in the page.
When all validation checks in previous phases have succeeded, the selected action is executed. During the execution of an action there can be action assertions that validate the data in the current execution state of the action. Moreover, data invariants are still checked during this phase and can produce validation errors as well. If any validation check fails, the entire action is cancelled (clearing all changes made during that request).
Validation messages produced in the previous phases result in a re-render of the same page with error messages inserted. If all validations succeed, the action results in a redirect to the same or a different page.
Notes:
WebDSL supports simple search capabilities through Lucene. Entity properties can be marked as \"searchable\" to subsequently be indexed:
entity Message {\n subject : String (searchable)\n text : Text (searchable)\n sender : ForumUser\n}\n
The searchable can be applied to the following built-in WebDSL types: String, Text, WikiText, Int, Long, Float and date types. Properties of user defined entity types are currently not supported to be searchable (i.e. sender in the previous example). If one or more properties of an entity are marked as searchable, a set of searchEntity
functions are generated, in this case:
function searchMessage(query : String) : [Message]\nfunction searchMessage(query : String, limit : Int) : [Message]\nfunction searchMessage(query : String, limit : Int, offset : Int) : [Message]\n
Which can be used from anywhere. For instance on a search page: page search(query : String) {\n var newQuery : String := query;\n action doSearch() {\n return search(newQuery);\n }\n\n title { \"Search\" }\n form {\n input(newQuery)\n submit(\"Search\", doSearch())\n }\n for(m : Message in searchMessage(query, 50)) {\n output(m)\n }\n}\n
The query syntax adheres to Lucene's query syntax as does the scoring.
All data to be indexed (properties marked as \"searchable\") and queries are analyzed using the default analyzer of Lucene. This means that punctuation and stop words (commonly used words like 'the', 'to', 'be') are stripped from text and text is transformed to tokens in lowercase.
"},{"location":"reference/search/#index-location","title":"Index location","text":"WebDSL stores its search index in the /var/indexes/APPNAME
directory. This is currently not configurable. Make sure this directory is readable and writeable for the user that runs tomcat.
When the searchable
annotations or search mappings are added/changed when data is already in the database, the search index has to be recreated. When your application has been deployed , go to the directory in which it was deployed, for instance /var/tomcat/webapps/yourapp. In this directory you will find a webdsl-reindex
script (for *nix only) which will invoke ant reindex and fixes permissions of the index directory. By default, the reindex task completely reindexes all searchable entities. As of WebDSL version 1.2.9 it also accepts entity names as command line argument (separated by whitespace) to reindex a subset of entities.
Reindex all entities *nix:
# sudo sh webdsl-reindex\n
Windows as of 1.3.0:
# ant reindex\n
before 1.3.0:
# ant reindex -f build.reindex.xml\n
Reindex a subset of entities *nix:
# sudo sh webdsl-reindex Entity1 Entity2 Etc\n
Windows: as of 1.3.0:
# ant reindex -Dentities=\"Entity1 Entity2 Etc\"\n
before 1.3.0:
# ant reindex -f build.reindex.xml -Dentities=\"Entity1 Entity2 Etc\"\n
Note: Don't start your application during reindexing, it will crash because it can't initialize the directory provider. So reindexing should be done before starting or when already running your application.
A demo of the search functionality can be seen on Reposearch.
"},{"location":"reference/send-email/","title":"Send Email","text":"This page describes how to create an email template and send email from your application. Make sure the email settings are configured in application.ini, see Application Configuration. If you are interested in storing email addresses in an entity, have a look at the Email type.
Defining an email template:
email testemail(us : User) {\n to(us.mail)\n from(\"webdslorg@gmail.com\")\n subject(\"Test Email\")\n\n par{ \"Dear \" output(us.name) \", \" }\n par{\n \"Look at your profile page: \"\n navigate(user(us)){\"go\"}\n //navigate will become an absolute link in the email\n }\n}\n
Sending email:
email testemail(someuser);\n
The actual sending happens asynchronously, if there are issues while the application is trying to send an email, it will retry that email after 3 hours. If necessary, you can inspect and influence this email queue through the QueuedEmail
entity:
entity QueuedEmail {\n body : String (length=1000000) \n //Note: default length for string is currently 255\n to : String (length=1000000)\n cc : String (length=1000000)\n bcc : String (length=1000000)\n replyTo : String (length=1000000)\n from : String (length=1000000)\n subject : String (length=1000000)\n lastTry : DateTime \n}\n
"},{"location":"reference/services/","title":"Services","text":"WebDSL includes a simple way to define string and JSON-based webservices.
"},{"location":"reference/services/#json-api","title":"JSON API","text":"The JSON interface is defined as follows:
native class org.json.JSONObject as JSONObject {\n constructor()\n constructor(String)\n get(String) : Object\n getBoolean(String) : Bool\n getDouble(String) : Double\n getInt(String) : Int\n getJSONArray(String) : JSONArray\n getJSONObject(String) : JSONObject\n getString(String) : String\n has(String) : Bool\n names() : JSONArray\n put(String, Object)\n toString() : String\n toString(Int) : String\n }\n\n native class org.json.JSONArray as JSONArray {\n constructor()\n constructor(String)\n get(Int) : Object\n getBoolean(Int) : Bool\n getDouble(Int) : Double\n getInt(Int) : Int\n getJSONArray(Int) : JSONArray\n getJSONObject(Int) : JSONObject\n getString(Int) : String\n length() : Int\n join(String) : String\n put(Object)\n remove(Int)\n toString() : String\n toString(Int) : String\n } \n
Example use in WebDSL:
function myJsonFun() : String {\n var obj := JSONObject(\"{}\");\n obj.put(\"name\", \"Pete\");\n obj.put(\"age\", 27);\n return obj.toString();\n // Will return '{\"name\": \"Pete\", \"age\": 27}'\n}\n
"},{"location":"reference/services/#defining-services","title":"Defining services","text":"A service is simply a WebDSL function that uses the service
keyword instead of function
, you don't have to specify a return type, it will convert anything you return to a string (using .toString()
):
entity Document {\n title : String (id, name)\n text : Text\n}\n\nservice document(id : String) {\n if(getHttpMethod() == \"GET\") {\n var doc := findDocument(id);\n var json := JSONObject();\n json.put(\"title\", doc.title);\n json.put(\"text\", doc.text);\n return json;\n }\n if(getHttpMethod() == \"PUT\") {\n var doc := getUniqueDocument(id);\n var json := JSONObject(readRequestBody());\n doc.text := json.getString(\"text\");\n return doc.title;\n }\n}\n
services are mapped to /serviceName
, e.g. /document
. Here's a few sample requests to test (note, these are services part of an application called \"hellojson\"):
$ curl -X PUT 'http://localhost:8080/hellojson/document/my-first-doc' \\\n -d '{\"text\": \"This is my first document\"}'\nmy-first-doc\n$ curl http://localhost:8080/hellojson/document/my-first-doc\n{\"text\":\"\"This is my first document\",\"title\":\"my-first-doc\"}\n
But, like pages, services can also have entities as arguments:
service documentJson(doc : Document) {\n var obj := JSONObject();\n obj.put(\"title\", doc.title);\n obj.put(\"text\", doc.text);\n return obj;\n}\n
"},{"location":"reference/session-entities/","title":"Session Entities","text":"Storing data in the session context on the server is done using session entities. Example:
session shoppingcart {\n products : [Product]\n}\n
A session entity name is a globally visible variable in the application code. The entity object is automatically instantiated and saved, one for each browser session accessing the application.
Typically, session data is used for keeping track of authentication state, but it can also be used for temporarily storing data for anonymous users. A common oversight with session data is that it is shared between tabs in a browser.
To initialize session data to default values, remember that using :=
after a field means that that field is a derived property. That also means you can't assign to this field. To set defaults use (default = value)
. Example:
session history {\n number_of_entries : Int (default = 10)\n}\n
Declaring an access control principle, e.g. principal is User with credentials name,password
, automatically creates a securityContext
session entity. For more information about access control see the Access Control page.
Session entities can also be extended with extra properties. Example:
extend session shoppingcart{\n lastSearchQuery : String\n}\n
Session data times out by default, this timeout length can be adjusted in the application.ini
file, e.g. sessiontimeout=10080
. This time is specified in minutes. More information about application settings is shown on the Application Configuration page.
Styling of WebDSL pages is done using CSS. In the application directory add the following directory and file:
stylesheets/common_.css\n
This CSS file will be automatically included when deploying the application. Other CSS files can be included using includeCSS
(in this example the included file is located at stylesheets/jquery-ui.css):
includeCSS(\"jquery-ui.css\")\n
When the application is deployed in the Eclipse plugin you can edit the CSS file directly in the tomcat directory (don't forget to also save the CSS file back to your project):
WebContent/stylesheets/common_.css\n
For deployment to external tomcat this directory is:
tomcat/webapps/appname/stylesheets/common_.css\n
The Firebug add-on for Firefox can be very helpful in figuring out the page structure, other browsers have similar development tools.
Explicit hooks for CSS can be added using the XML embedding:
template someTemplate(){ \n <div class=\"mydiv\">\n \"content of mydiv\"\n </div>\n}\n
Note that the \"mydiv\" is a WebDSL expression, so this could also be stored in an entity and retrieved using a field access:
<div class=user.cssclass>\n
Classes for styling can also be added to a template call (separate from the regular arguments):
input[class=\"mynameinput\"](u.name)\n
If you want to define your own template that takes such extra arguments, use all attributes
:
page root(){\n someOtherTemplate[class=\"importantdiv\"]{ \"content\" }\n}\n\ntemplate someOtherTemplate(){ \n <div all attributes>\n elements\n </div>\n}\n
The span
template modifier adds a span around a template, which can then be used as a hook for CSS:
template span spanTemplate(){ \"span around me\" }\n
"},{"location":"reference/synchronization-framework/","title":"Synchronization Framework","text":"WebDSL provides possibility to generate code for a synchronization framework combined with mobl. Nevertheless, the webservices are open to be used by other applications.
"},{"location":"reference/synchronization-framework/#required-steps","title":"Required steps","text":"The steps that are required for the synchronization framework are the following:
The framework is meant as an extension to a WebDSL application. This means that it requires at least a full model in the application to generate a working framework.
"},{"location":"reference/synchronization-framework/#additional-settings","title":"Additional Settings","text":"The synchronization framework requires and allows some adaption by additional settings. Those settings can be added in the synchronization settings for each entity.
entity Example{\n synchronization configuration{\n } \n}\n
The following settings can be configured:
The data synchronization requires a Toplevel Entity to enable the data partitioning. This is simple a flag that specifies that objects of this type represent a data partition. Additionally, this setting requires a String property that can be used to represent this object.
entity Car{\n registrationIdentifier : String\n\n synchronization configuration{\n toplevel name property : registrationIdentifier\n } \n}\n
"},{"location":"reference/synchronization-framework/#access-control","title":"Access Control","text":"The data synchronization framework enable external sources to read and modify data on the server with the web application. The framework allows control over which data can be accessed by who. This can only be specified when the a principal is defined in the web application. There are three different levels that can be specified for each entity: read, write and create. It is recommended to specify those rules for each entity.
entity Dummy{\n name : String\n\n synchronization configuration{\n access read: true\n access write: Logedin()\n access create: principal().isAdmin()\n } \n}\n
"},{"location":"reference/synchronization-framework/#restricted-properties","title":"Restricted Properties","text":"The last setting that can be configured is that of restricted properties. It allows to simplify the data model that you want to use on synchronization. The properties that are specified in this configuration are removed from the shared data and also for the calculation of data partitioning.
entity Person{\n surName : String\n firstName : String\n fullName : String\n\n synchronization configuration{\n restricted properties : surName, firstName\n } \n}\n
"},{"location":"reference/synchronization-framework/#generation-of-the-framework","title":"Generation of the framework","text":"Generation of the framework is easy. After specifying the settings, open the main application file in the IDE. Then select the generate synchronization framework from the Transform menu.
"},{"location":"reference/synchronization-framework/#importing-of-the-framework","title":"Importing of the framework","text":"The framework is generated in the folder webservices. To enable the synchronization framework inside the web application you need to include the main file of the framework.
application TestApp\nimports webservices/services/interface\n
"},{"location":"reference/synchronization-framework/#mobl-or-other-remote-application","title":"mobl or other remote application","text":"The framework generates code for mobl that enable synchronization in a mobl application. However, it still needs a full mobile application.
Other applications can use the available webservices to synchronize with the application.
"},{"location":"reference/synchronization-framework/#what-is-generated","title":"What is generated","text":"The framework generates a lot of files, but what does it contain:
The core of the synchronization contains functions that overlook the main functionality of the synchronization. Identification, detection and resolution of updates.
"},{"location":"reference/synchronization-framework/#webservices","title":"Webservices","text":"The webservices are used for communication with mobl of other applications. This is a layer on the core of the synchronization. All services are called by post request to the url:
http://<websiteurl>/webservice/<webservicename>\n
The following services are available and should be used in that order:
The mappers are meant for mapping the updates to local values. The also have some additional statements for checking validity of the input. There are two mappers, one for modification and one for creation. Currently, they contain the same code. This is done so they can be overwritten separately.
The values in the database are not in a format that can be send through webservices. Therefor, the framework has 3 functions for each entity
The synchronization framework uses data partitioning to reduce the amount of data for mobile applications. This solution chooses to use object relations to determine if objects are linked to the TopLevel entity. This requires that each entity has a function to calculate the related objects.
The main function of data partitioning gets a closure of a data partitioning by calling the related functions until there are no new objects any more.
"},{"location":"reference/synchronization-framework/#access-control_1","title":"Access Control","text":"As mentioned before you can specify three rules for the access control of objects. Those rules are turned into functions named:
The access control requires that remote applications can login to the application. To improve security a device can register itself and get a devicekey. Which then can be used to authenticate instead of using the password. Those keys are stored as an additional property of the principal and if removed the device is de-authenticated.
"},{"location":"reference/synchronization-framework/#model","title":"Model","text":"A big part of the data synchronization is about the model. The model is basically a copy of that of WebDSL only with other mobl types. It should also be used for developers that try to understand what model is expected for the webservices. The following sections are additional notes to the creation of the model.
"},{"location":"reference/synchronization-framework/#restricted-types","title":"Restricted types","text":"Mobl has a more restricted set of types. This let to the choice to not support all types. properties with the following types are removed:
Mobl does not support class Hierarchy. To support all entities from the application the synchronization framework has flatten the hierarchy. The influence can be found in the renamed properties that now have a prefix of there original class name. And a additional property that tells the actual type: Typefield
"},{"location":"reference/synchronization-framework/#toplevel-properties","title":"TopLevel properties","text":"The data partitioning requires some additional information. This is stored in the property sync and lastSynced.
"},{"location":"reference/synchronization-framework/#search-annotations","title":"Search annotations","text":"The search annotations in mobl are expensive and better can be removed from the model.
"},{"location":"reference/synchronization-framework/#mappers","title":"Mappers","text":"Mobl also needs some mappers of the values. However the limited difference between mobile and JSON representation, allows it to use the function generated by the mobl compiler.
"},{"location":"reference/synchronization-framework/#integration-functions","title":"Integration functions","text":"There are some integration functions for mobl that can be used to call synchronization processes. It can be seen as the core of the synchronization for mobl applications.
"},{"location":"reference/synchronization-framework/#authentication_1","title":"Authentication","text":"The authentication are some functions to enable the devicekey setup. It has the following functions:
The logging out of the device also cleans the database for security reasons.
"},{"location":"reference/synchronization-framework/#data-browser","title":"Data Browser","text":"As a start the generated framework has a data browser included to have easy start with the application.
It has a page for every entity, namely: showSimple
Those pages allow to click through the data stored locally.
"},{"location":"reference/synchronization-framework/#additional-notes","title":"Additional notes","text":""},{"location":"reference/synchronization-framework/#version-number","title":"version number","text":"The send version numbers of each objects can be used to change the protocol of resolution of outdated objects. Giving it an high number will interpret that the object is newer than that of the system.
"},{"location":"reference/synchronization-framework/#collections","title":"Collections","text":"mobl doesn't have difference between set and lists, it only supports collections. The biggest problem is that the ordering can not be trust.
"},{"location":"reference/testing/","title":"Testing","text":"Tests for the entities and functions operating on those entities can be defined in test blocks.
Example:
test capitalizeTest {\n var u := User{ name := \"alice\" };\n u.capitalizeName();\n assert(u.name == \"Alice\");\n}\nentity User {\n name : String\n function capitalizeName(){\n var temp := name.explodeString();\n temp.set(0,temp.get(0).toUpperCase());\n name := temp.concat();\n }\n}\n
The 'webdsl test appname' command builds the app and runs the tests, an error will be returned when any of the tests fail. This command will create a new application.ini and uses an in-memory H2 Database Engine.
webdsl test myapp\n
If the settings in the existing application.ini can be used for testing, run the 'webdsl check' command instead.
webdsl check\n
Multiple test blocks can be defined in an application, each test will run with a fresh initialization of the database, the global variables and global init blocks are handled before each test.
Use assert calls to verify the correctness of results. When the assert argument evaluates to false, the test run will show the location of the failing assert. If the assert consists of one == or != comparison, the two compared results will also be printed upon failure. The assert function has an optional second argument, a String which can be used to pass a message that will be shown when the assert fails.
Signatures:
assert(Bool)
assert(Bool, String)
Testing example: tests for built-in Text type: text.app.
"},{"location":"reference/testing/#web-testing","title":"Web testing","text":"The resulting web application can also be automatically tested by using
webdsl test-web myapp\n
or
webdsl check-web\n
where 'test-web' will automatically create an application.ini and 'check-web' will use an existing one.
These commands start up tomcat and run the tests. Tests are currently expressed by calls to WebDriver with HTMLUnit behind it (a better abstraction specific to WebDSL tests is planned, current web testing is mainly to support testing the compiler). The interface is described here (see Native Java Interface):
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/src/org/webdsl/dsl/languages/test/native-classes.str\n
Example:
test datavalidation {\n var d : WebDriver := HtmlUnitDriver();\n\n d.get(navigate(root()));\n assert(!d.getPageSource().contains(\"404\"), \"root page may not produce a 404 error\");\n\n var elist : [WebElement] := d.findElements(SelectBy.tagName(\"input\"));\n assert(elist.length == 4, \"expected 4 <input> elements\");\n\n elist[1].sendKeys(\"123\");\n elist[2].sendKeys(\"111\");\n\n elist[3].click();\n\n var pagesource := d.getPageSource();\n\n var list := pagesource.split(\"<hr/>\");\n\n assert(list.length == 3, \"expected two occurences of \\\"<hr/>\\\"\");\n\n assert(list[1].contains(\"inputcheck\"), \"cannot find inputcheck message\");\n assert(list[2].contains(\"formcheck\"), \"cannot find formcheck message\");\n
}
Source:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/data-validation/validate-in-elements.app\n
"},{"location":"reference/types/","title":"Types","text":"This section lists all the built-in types available in WebDSL.
There are multiple types that are equivalent to String. These types can have different validation rules, functions, inputs, and outputs. Converting between these types can be done with casts, e.g.
var : Secret := url(\"123\") as Secret;\n
The String compatible types are:
Similarly, the Date times are equivalent as well:
Enumeration types consist of a list of options that contain both an identifier and a visible name. You define them as follows:
enum Gender {\n maleGender(\"Male\"),\n femaleGender(\"Female\"),\n otherGender(\"Other\"),\n}\n
You can use them as follows: entity User {\n gender : Gender\n}\n\npage somePage() {\n var u : User;\n input(u.gender) // shows a drop-down\n output(u.gender.name) // shows either Male or Female\n}\n
Or, in action code: function setMale(u : User) {\n u.gender := maleGender;\n}\n
Note that internally an enumeration becomes an entity declaration with a name property for the visible name. The options become global variable declarations that get initialized once. Visible names can be updated using regular WebDSL forms.
"},{"location":"reference/types/#string","title":"String","text":"Represents a string of characters. Example:
var s : String := \"Hello world\";\n
The default value for String properties and variables is \"\".
"},{"location":"reference/types/#functions","title":"Functions","text":"contains(s: String):Bool
Tests whether s is a substring of this string.
length():Int
Returns the length of this string.
parseInt():Int
Returns the Int value in this string. If this string does not contain a valid Int value, this function returns null.
parseUUID():UUID
Returns the UUID value in this string. If this string does not contain a valid UUID value, this function returns null.
toUpperCase():String
Returns this string in uppercase.
toLowerCase():String
Returns this string in lowercase.
split():[String]
Returns the characters in this string as separate strings in a list.
split(separator:String):[String]
Returns a list of strings produced by splitting this string around matches of separator.
makePatch(new : String):Patch
Creates a Patch from this String to the new String, see Patch.
diff(new : String):[String]
Creates a list of String
s describing the differences between this String and the new String.
trim(): String
Returns this string, with leading and trailing whitespace omitted.
"},{"location":"reference/types/#list-functions","title":"List Functions","text":"concat():String
Concatenates the strings in this list of strings.
concat(separator:String):String
Concatenates the strings in this list of strings, separated by separator.
"},{"location":"reference/types/#int","title":"Int","text":"Represents an integer number. Example:
var i : Int := 3;\n
The default value for Int properties and variables is 0.
"},{"location":"reference/types/#functions_1","title":"Functions","text":"floatValue():Float
Converts this value to a Float.
toString():String
Converts this value to a String.
"},{"location":"reference/types/#float","title":"Float","text":"Represents a floating point number. Example:
var f : Float := 3.5;\n
The default value for Float properties and variables is 0f.
"},{"location":"reference/types/#functions_2","title":"Functions","text":"round():Int
Rounds this value to the nearest Int value.
floor():Int
Returns the largest Int that is less than or equal to this value.
ceil():Int
Returns the smallest Int that is greater than or equal to this value.
toString():String
Converts this value to a String.
"},{"location":"reference/types/#static-functions","title":"Static Functions","text":"random():Float
Produces a random Float between 0 and 1.
"},{"location":"reference/types/#bool","title":"Bool","text":"Represents a truth value. Either true
or false
. Example:
var b : Bool := true;\n
The default value for Bool properties and variables is false.
"},{"location":"reference/types/#functions_3","title":"Functions","text":"toString():String
Converts this value to a String.
"},{"location":"reference/types/#list","title":"List","text":"Represents an ordered list of items of a certain type. Example:
var l : [Int] := [1, 2, 3, 4];\n
Sorted output of lists can be created using the for
loop filter in templates or actions:
for(u:User in [u1,u2,u3] order by u.name desc){\n output(u)\n}\n
"},{"location":"reference/types/#fields","title":"Fields","text":"length
Gives the number of items in the list.
"},{"location":"reference/types/#list-creation-expressions","title":"List Creation Expressions","text":"List()
Creates an empty list of type Entity
.
List(..., Entity, ...)
Creates a list of type Entity
with the elements resulting from the comma separated argument expressions.
Example:
var list := List<User>(User{},SubSubUser{},SubUser{})\n
[Entity, ...]
Creates a list of type Entity
(type of first element) with the elements resulting from the comma separated expressions between the [ ].
Example:
var list := [User{ name := \"test\" },SubUser{},uservar]\n
"},{"location":"reference/types/#functions_4","title":"Functions","text":"add(Entity)
Adds the entity to this list.
remove(Entity)
Removes the first occurence of Entity in this list.
clear()
Removes all entities in this list.
addAll(List/Set)
Adds all entities of the List/Set to this list.
set() : {Entity}
Creates a Set containing the unique elements in this list.
indexOf(Entity) : Int
Returns the index of the first occurence of Entity in this list. Returns -1 if the Entity is not in this list.
get(Int)
Returns the element at location Int in this list.
set(Int,Entity)
Sets the list element at Int to Entity. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
insert(Int,Entity)
Inserts the Entity at location Int in this list. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
removeAt(Int)
Removes the element at location Int in this list. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
subList(from:Int,to:Int):[Entity]
Returns a portion of this list between the specified from, inclusive, and to, exclusive.
"},{"location":"reference/types/#set","title":"Set","text":"Represents an unordered collection of unique items of a certain type. Example
var s : Set<Int> := {1, 2, 3, 4};\n
Sorted output of sets can be created using the for
loop filter in templates or actions.
for(u:User in [u1,u2,u3] order by u.name desc){\n output(u)\n}\n
"},{"location":"reference/types/#fields_1","title":"Fields","text":"length
Gives the number of items in the set.
"},{"location":"reference/types/#set-creation-expressions","title":"Set Creation Expressions","text":"Set()
Creates an empty set of type Entity.
Set(..., Entity, ...)
Creates a set of type Entity with the elements resulting from the comma separated argument expressions.
Example:
var set := Set<Person>(Person{},SubSubPerson{},SubPerson{})\n
{Entity, ...}
Creates a set of type Entity (type of first element) with the elements resulting from the comma separated expressions.
Example:
var set : {Person} := {Person{ name := \"test\" },personvar}\n
"},{"location":"reference/types/#functions_5","title":"Functions","text":"add(Entity)
Adds the entity to this set.
remove(Entity)
Removes Entity in this set.
clear()
Removes all entities in this set.
addAll(List/Set)
Adds all entities of the List/Set to this set.
list() : [Entity]
Creates a List containing the elements in this set.
"},{"location":"reference/types/#secret","title":"Secret","text":"Represents a secret string (usually a password). The page input for a Secret is a masked textfield. The page output for a Secret is \"********\".
Example:
var pass : Secret := \"123\";\n
The default value for Secret properties and variables is \"\".
The Secret type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Secret typed vars. A Secret can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(pass:Secret){\n test1(pass as String); \n}\n
A String can also be cast to a Secret:
assert(pass == \"123\" as Secret)\n
"},{"location":"reference/types/#functions_6","title":"Functions","text":"all String functions
Secret is compatible with String.
check(input:Secret):Bool
Checks the input Secret (not digested) against the digest version contained in this Secret.
Example:
if (user.password.check(password)) {\n securityContext.principal := us;\n securityContext.loggedIn := true;\n}\n
digest():Secret
Generates a digest of the clear-text password contained in this Secret.
Example:
var s : Secret := \"123\";\ns := s.digest();\nassert(s.check(\"123\" as Secret));\n
"},{"location":"reference/types/#email","title":"Email","text":"Represents an e-mail address as a string. If you are interested in sending email from your application, have a look at the Send Email page. The page input for an Email is a textfield with the following validation:
validate(/[a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?/.match(this), \"Not a valid email address\")\n
The page output for Email is the same as String output.
Example:
var address : Email := \"webdslorg@gmail.com\";\n
The default value for Email properties and variables is \"\". Add the 'not empty' annotation (or a custom validation) to an Email type property in an entity to disallow the empty string.
The Email type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Email typed vars. An Email can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(address:Email){\n test1(address as String); \n}\n
A String can also be cast to an Email:
assert(address == \"123\" as Email)\n
"},{"location":"reference/types/#functions_7","title":"Functions","text":"all String functions
Email is compatible with String.
"},{"location":"reference/types/#text","title":"Text","text":"Represents a large string. The page input for a Text is a textarea. The page output for Text is the same as String output.
Example:
var t : Text := \"123\";\n
The default value for Text properties and variables is \"\".
The Text type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Text typed vars. A Text can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t:Text){\n test1(t as String); \n}\n
A String can also be cast to a Text:
assert(t == \"123\" as Text)\n
"},{"location":"reference/types/#functions_8","title":"Functions","text":"all String functions
Text is compatible with String.
"},{"location":"reference/types/#wikitext","title":"WikiText","text":"Represents a large string with Markdown syntax support and internal page links. Internal page links in WikiText can be created using [[page(arg)|caption]]
.
The page input for WikiText is a textarea. The page output for WikiText processes the Markdown and page links and produces html elements.
Example:
var t : WikiText := \"123\";\n
The default value for WikiText properties and variables is \"\".
The WikiText type is compatible with the String type, all the String functions can be used, and String literals can be assigned to WikiText typed vars. A WikiText can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t: WikiText){\n test1(t as String); \n}\n
A String can also be cast to a WikiText:
assert(t == \"123\" as WikiText)\n
"},{"location":"reference/types/#functions_9","title":"Functions","text":"all String functions
WikiText is compatible with String.
"},{"location":"reference/types/#patch","title":"Patch","text":"Represents a patch. The page input for a Patch is the same as for Text. The page output for Patch is the same as for Text.
Example:
var p : Patch := \"12345\".makePatch(\"24\");\n
The default value for Patch properties and variables is \"\".
The Patch type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Patch typed vars. A Patch can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(p:Patch){\n test1(p as String); \n}\n
A String can also be cast to a Patch:
assert(p == \"123\" as Patch);\n
"},{"location":"reference/types/#functions_10","title":"Functions","text":"all String functions
Patch is compatible with String.
applyPatch(arg: String):String
Applies this patch to the arg String.
Example:
var s1 : Patch := \"12345\".makePatch(\"24\");\nassert(s1.applyPatch(\"12345\") == \"24\");\n
"},{"location":"reference/types/#datetime","title":"DateTime","text":"Represents both a date and a time. The page input for a DateTime is a textfield, the expected format is dd/MM/yyyy H:mm. The page output for a DateTime shows the DateTime formatted with dd/MM/yyyy H:mm. Use the format function to customize the output format.
The default value for DateTime properties and variables is null.
The DateTime type is compatible with the Time and Date types. A DateTime can be cast to these types.
"},{"location":"reference/types/#date-creation-functions","title":"Date Creation Functions","text":"DateTime(String):DateTime
Dates can be constructed using the Date constructor (expected format dd/MM/yyyy H:mm):
var dt : DateTime := DateTime(\"22/06/1983 22:08\");\n
DateTime(String, String):DateTime
var dt : DateTime := DateTime(\"12:12 05-1994-06\", \"mm:H MM-yyyy-dd\");\n
The second parameter represents the date/time formatting string.
now():DateTime
Creates a DateTime containing the current time and day.
"},{"location":"reference/types/#functions_11","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
getSecond():Int
Gets the second.
getMinute():Int
Gets the minute.
getHour():Int
Gets the hour.
getDay():Int
Gets the day of the month.
getDayOfYear():Int
Gets the day of the year.
getMonth():Int
Gets the month.
getYear():Int
Gets the year.
"},{"location":"reference/types/#date","title":"Date","text":"Represents a date (not including a time). The page input for a Date is a textfield, the expected format is dd/MM/yyyy. The page output for a Date shows the Date formatted with dd/MM/yyyy. Use the format function to customize the output format.
The default value for Date properties and variables is null. Note that all Date types are DateTime at run-time.
The Date type is compatible with the DateTime and Time types. A Date can be cast to these types.
"},{"location":"reference/types/#date-creation-functions_1","title":"Date Creation Functions","text":"Date(String):Date
Dates can be constructed using the Date constructor (expected format dd/MM/yyyy):
var d : Date := Date(\"04/09/2009\");\n
Date(String, String):Date
The second parameter represents the date formatting string.
var d1 : Date := Date(\"12-20-1990\", \"MM-dd-yyyy\");\n
today():Date
Creates a Date containing the current day and time 00:00.
age(Date):Int
Gets the age from a date of birth.
"},{"location":"reference/types/#functions_12","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
Represents a time (not including a date). The page input for a Time is a textfield, the expected format is H:mm. The page output for a Time shows the Time formatted with H:mm. Use the format function to customize the output format.
The default value for Time properties and variables is null. Note that all Date types are DateTime at run-time.
The Time type is compatible with the DateTime and Date types. A Time can be cast to these types.
"},{"location":"reference/types/#time-creation-functions","title":"Time Creation Functions","text":"Time(String):Time
Time can be constructed using the Time function (expected format H:mm):
var t : Time := Time(\"22:08\");\n
Time(String, String):Time
The second parameter represents the date formatting string.
var t1 : Time := Time(\"59:08\", \"mm:H\");\n
"},{"location":"reference/types/#functions_13","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
Represents a hyperlink. The page input for a URL is the same as for String. The page output for URL is a hyperlink.
Example:
var u : URL := \"https://webdsl.org\";\n
The default value for URL properties and variables is \"\".
The URL type is compatible with the String type, all the String functions can be used, and String literals can be assigned to URL typed vars. A URL can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t: URL){\n test1(t as String); \n}\n
A String can also be cast to a URL:
assert(t == \"https://webdsl.org\" as URL);\n
"},{"location":"reference/types/#url-creation-functions","title":"URL Creation Functions","text":"url(arg : String):URL
Casts the arg String to a URL type, equivalent to 'arg as URL'.
You can also use this in a navigate templatecall in order to provide an absolute URL instead of an internal link.
Example:
navigate(url(\"https://webdsl.org\")){ \"powered by WebDSL\" }\n
"},{"location":"reference/types/#functions_14","title":"Functions","text":"all String functions
URL is compatible with String.
"},{"location":"reference/types/#file","title":"File","text":"Represents an (uploaded) file. The page input for a File is a file upload component. The page output is a file download link.
"},{"location":"reference/types/#functions_15","title":"Functions","text":"fileName():String
Returns the name of this file.
getContentAsString():String
Returns the content of this file as String.
download()
The current action will result in a download of this file.
"},{"location":"reference/types/#image","title":"Image","text":"Represents an (uploaded) image. The page input for an Image is a file upload component. The page output shows the image.
"},{"location":"reference/types/#functions_16","title":"Functions","text":"fileName():String
Returns the name of this image file.
getContentAsString():String
Returns the content of this file as String.
download()
The current action will result in a download of this image file.
getWidth()
Returns the calculated width of this image.
getHeight()
Returns the calculated height of this image.
resize(maxWidth : Int, maxHeight : Int)
Resizes the image to the set dimensions. Note that currently, images can only be downscaled.
crop(x : Int, y : Int, width : Int, height : Int)
Crops the image to the specified size and coordinates.
clone() : Image
Makes a copy of the image and returns it.
"},{"location":"reference/using-the-editor/","title":"Using the Editor","text":"This is the WebDSL language reference. For more background information on the ideas, architecture, and design decisions behind WebDSL, see the Background section.
"},{"location":"reference/using-the-editor/#download-and-installation","title":"Download and Installation","text":"See Install WebDSL.
"},{"location":"reference/using-the-editor/#new-project-wizard","title":"New Project Wizard","text":"See Hello World! in WebDSL.
The plugin includes a new project wizard which will help you get started using WebDSL:
http://localhost:8080/{projectname}
to see the result.Use the \u2018Convert to a WebDSL Project\u2019 wizard to regenerate the project build files (this will overwrite the old files, including application.ini).
"},{"location":"reference/using-the-editor/#troubleshooting","title":"Troubleshooting","text":"If you encounter issues when running the plugin, here are a few things that you should check or try:
Checking user inputs and providing clear feedback is essential for the usability of web applications. WebDSL allows declarative specification of such input validation rules using the validate feature.
Validation rules in WebDSL are of the form validate(e,s) and consist of a Boolean expression e to be validated, and a String expression s to be displayed as error message. Any globally visible functions or data can be accessed as well as any of the properties and functions in scope of the validation rule context.
Value well-formedness checks (e.g. whether the user enters a valid integer in an Int input) are added automatically to each input field.
Validation can be specified on entities in property annotations:
entity User { \n username : String (id, validate(isUniqueUser(this), \"Username is taken\")) \n password : Secret (validate(password.length() >= 8, \"Password needs to be at least 8 characters\") \n , validate(/[a-z]/.find(password), \"Password must contain a lower-case character\") \n , validate(/[A-Z]/.find(password), \"Password must contain an upper-case character\") \n , validate(/[0-9]/.find(password), \"Password must contain a digit\"))\n email : Email\n} \nextend entity User { \n username(validate(isUniqueUser(this),\"Username is taken\")) \n password(validate(password.length() >= 8, \"Password needs to be at least 8 characters\") \n ,validate(/[a-z]/.find(password), \"Password must contain a lower-case character\") \n ,validate(/[A-Z]/.find(password), \"Password must contain an upper-case character\") \n ,validate(/[0-9]/.find(password), \"Password must contain a digit\")) \n}\n
Validation can be specified directly in pages:
page editUser( u: User ){ \n var p: Secret; \n form { \n group( \"User\" ){ \n label( \"Username\" ){ input( u.username ) } \n label( \"Email\" ){ input( u.email ) } \n label( \"New Password\" ){ \n input( u.password )\n } \n label( \"Re-enter Password\" ){ \n input( p ){ \n validate( u.password == p, \"Password does not match\" ) \n } \n } \n submit action{} { \"Save\" }\n } \n }\n}\n
Validation can be specified in actions:
page createGroup { \n var ug := UserGroup{}\n form { \n group( \"User Group\" ){ \n label( \"Name\" ){ input( ug.name ) } \n label( \"Owner\" ){ input( ug.owner ) } \n submit save() { \"Save\" }\n }\n }\n action save() {\n validate( ug.owner != null, \"A group must have an owner\" );\n validate( ug.owner.email != \"\", \"Owner has not provided an email address\" ); \n ug.save();\n email newGroupNotify( ug );\n return userGroup( ug );\n }\n}\nemail newGroupNotify( u: UserGroup ){\n from( \"info@example.com\" )\n to( u.owner.email )\n subject( \"Usergroup created: ~u.name\" )\n \"Your usergroup ~u.name has been created.\"\n}\n
"},{"location":"reference/validation/#customizing-validation-output","title":"Customizing Validation Output","text":"Validation output can be customized by overriding the templates used to display validation messages. Currently, there are 4 global validation templates:
ignore-access-control template errorTemplateInput(messages : [String])\n
Displays validation message related to an input.
ignore-access-control template errorTemplateForm(messages : [String])\n
Displays validation message for validation in a form.
ignore-access-control template errorTemplateAction(messages : [String])\n
Displays validation message for validation in an action.
ignore-access-control template templateSuccess(messages : [String])\n
Displays validation message for success messages.
When overriding these validation templates, use an elements
templatecall to refer to the element being validated.
Example:
ignore-access-control template errorTemplateInput(messages : [String]){\n elements\n for(ve: String in messages){\n output(ve)\n } \n}\n
"},{"location":"reference/validation/#ajax-validation","title":"Ajax Validation","text":"WebDSL provides input components that validate the inputs using ajax.
built-in value types:
inputajax(String/Secret/URL/Email/Text/WikiText)\ninputajax(Int/Bool/Float/Long)\n
reference types:
inputajax(Entity / [Entity] / {Entity})\nselectajax(Entity / {Entity})\nradioajax(Entity)\n
provide selection options:
inputajax(Entity / [Entity] / {Entity} , [Entity])\nselectajax(Entity / {Entity} , [Entity])\nradioajax(Entity , [Entity])\n
Selection options can also be provided using the allowed
annotation on an entity property. Example:
entity Person{\n parent : Person (allowed=from Person as p where p != this)\n}\n
"},{"location":"reference/webdsl-apps/","title":"WebDSL Apps","text":""},{"location":"reference/webdsl-apps/#the-structure-of-a-webdsl-application","title":"The structure of a WebDSL application","text":"WebDSL application are organized in *.app files. Each .app file has a header, that either declares the name of a module or the name of an application. The declared name should be identical to the filename. Each application needs an .app file that declares the name of the application. This is the name refered to in the application.ini file (see below).
An application can be organized in different modules. In a typical .app file the header is followed by a list of import statements, which contain a path to other modules (without extension). In this way your application can be separated over several files, and modules can be reused.
Within .app files one can define sections. A section is merely a label to identify the structure of a file. Most section names have no influence on the program itself, some have however, for example in styling definitions.
The real contents of a .app file are a list of definitions. This might be page-, template-, action- or entity definitions. Other kinds of definitions might be introduced by WebDSL modules. A module might either refer to a module of an WebDSL application, or to an module of the WebDSL compiler itself. In this case the latter one is refered to. Those definitions will be examined in detail in the next chapters.
A very simple application might look like:
HelloWorld.app:
application HelloWorld\n\nimports MyFirstImport\n\nsection pages\n\npage root () { \n \"hello world\" \n IAmImported() \n}\n
MyFirstImport.app:
module MyFirstImport\n\ntemplate IAmImported() { \n spacer \n \"I am imported from a module file\" \n}\n
In the second file the section declaration is omitted, since an application may start with a list of declarations as well. The page that is shown when no page is specified (e.g. when visiting http://localhost:8080/yourapp) is named \"root\" and has no arguments.
"},{"location":"releases/","title":"WebDSL Releases","text":""},{"location":"releases/#webdsl-plugin-for-eclipse","title":"WebDSL Plugin for Eclipse","text":"Download the latest release of the WebDSL Plugin for Eclipse bundle for your platform:
WebDSL in Eclipse bundle (Linux) WebDSL in Eclipse bundle (MacOS) WebDSL in Eclipse bundle (Windows)
Installation instructions.
Or install the WebDSL plugin in an existing Eclipse 3.5+ installation using this update site:
https://update.webdsl.org/update\n
Installation instructions.
"},{"location":"releases/#webdsl-command-line-interface","title":"WebDSL Command-Line Interface","text":"Download the latest release of the WebDSL CLI:
WebDSL CLI
Installation instructions.
"},{"location":"support/","title":"Support","text":"If you need help with installing or using WebDSL, or you are interested in contributing to WebDSL, here is how to get involved:
"},{"location":"support/#mailing-list","title":"Mailing list","text":"You can subscribe to the mailing list for asking questions:
https://groups.google.com/d/forum/webdsl
"},{"location":"support/#issue-tracker","title":"Issue Tracker","text":"Issues are tracked in YellowGrass:
http://yellowgrass.org/project/WebDSL
"},{"location":"support/#search-the-webdsl-source-code","title":"Search the WebDSL Source Code","text":"The WebDSL repositories are indexed on reposearch:
http://codefinder.org/search/WebDSL
"},{"location":"tutorials/","title":"Tutorials","text":"This page lists tutorials that take you step-by-step through a project to learn a variety of concepts and aspects of WebDSL in a specific scope. For guides on achieving specific tasks, see the How To's section. For the WebDSL language reference, see the Reference section.
"},{"location":"tutorials/#setup-for-the-tutorials","title":"Setup for the Tutorials","text":"Installing WebDSL is necessary to follow the tutorials. Use the installation guide and start to learn by example using the tutorials.
"},{"location":"tutorials/#list-of-available-tutorials","title":"List of Available Tutorials","text":"To create a Hello World application in WebDSL, we have to:
Open the WebDSL editor and right-click the Package Explorer.
Select New WebDSL Project to make the project configuration show up.
Enter project name: HelloWorld
and press Finish.
The generated project contains lots of files but we only need the main application file for this tutorial. Open HelloWorld.app
in the root directory of the new project. It should look as follows:
application HelloWorld\n\n page root(){ \"Hello world\" }\n
"},{"location":"tutorials/hello-world/#build-and-deploy-the-application","title":"Build and Deploy the Application","text":"To see the resulting web application in the browser, we have to compile and deploy the WebDSL code.
Navigate to the project directory in the terminal (e.g. ~/eclipse-workspace/HelloWorld/
). To compile and run the application in one command, execute webdsl run
:
$ webdsl run\n# This should start a local web server and generate the following output:\n# ...\n# [ Main | info ] stage 1: parsing HelloWorld.app [ 0.78s ]\n# ...\n# [ Main | info ] stage 11: writing files [ 0.04s ]\n# ...\n# Now compiling generated Java code\n# ...\n# Some file copying\n# ...\n# Some SQL DROP/CREATE table statements\n# ...\n# Some Tomcat commands\n# ...\n# [XX <Month> XX:XX:XX HelloWorld] Tomcat web server started, your web application is available at http://localhost:8080/HelloWorld. Press ctrl+c to stop the web server.\n
Browsing to http://localhost:8080/HelloWorld/
opens the root page of your application and should output Hello world as we specified in HelloWorld.app
.
To stop the application server, simply press Ctrl+C in the terminal where the app is running.
"},{"location":"tutorials/hello-world/#summary","title":"Summary","text":"In this tutorial we created a web application with WebDSL, consisting of a single page that outputs Hello world.
"},{"location":"tutorials/pages-and-navigation/","title":"Pages and navigation","text":"In this tutorial we will create an online cookbook. To achieve this, we have to:
We start this tutorial with a project named cookbook
. To learn how to set up a WebDSL project, follow our Hello World tutorial.
Our homepage will list all recipes available in the cookbook. Open cookbook.app
and change the root page accordingly.
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem { \"Lasagne\" }\n listitem { \"Pancakes\" }\n listitem { \"Tomato Soup\" }\n }\n }\n
Built-in templates In the code snippet above, header
, list
and listitem
are templates defined in the built-in.app
. header { \"Recipes\" }
is syntactic sugar for <h1> \"Recipes\" </h1>
.
Once built and ran, the result (visible at http://localhost:8080/cookbook/
) is a homepage with the static elements we defined.
Next, we would like to have an additional page in our application:
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem { \"Lasagne\" }\n listitem { \"Pancakes\" }\n listitem { \"Tomato Soup\" }\n }\n }\n\n page recipe() {\n header { \"I'm a recipe!\" }\n }\n
In this updated version, visiting http://localhost:8080/cookbook/
will still show the home page as we have seen before. The new recipe page is available on http://localhost:8080/cookbook/recipe
.
In our previous build, we have two separate pages but no link from one to the other. To add links to other pages, we can use the navigate
construct that WebDSL provides:
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem {\n navigate recipe() { \"Lasagne\" }\n }\n listitem {\n navigate recipe() { \"Pancakes\" }\n }\n listitem {\n navigate recipe() { \"Tomato Soup\" }\n }\n }\n }\n\n page recipe() {\n navigate root() { \"Back to homepage\" }\n\n header { \"I'm a recipe!\" }\n }\n
A navigate
call consists of:
recipe()
in our case)\"Lasagne\"
in our case)The updated version of our application shows the list items as links to the recipe page, which now also includes a link back to the homepage.
"},{"location":"tutorials/pages-and-navigation/#add-page-parameters","title":"Add Page Parameters","text":"The last thing to do, in order to complete our cookbook, is to show which recipe you are currently viewing. In the previous build, we could link from our homepage to the recipe page, but the recipe page was always the same.
To make the recipe page aware of the current recipe, we will add a page parameter to its definition, and update the page calls to the recipe page accordingly.
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem {\n navigate recipe(\"Lasagne\") { \"Lasagne\" }\n }\n listitem {\n navigate recipe(\"Pancakes\") { \"Pancakes\" }\n }\n listitem {\n navigate recipe(\"Tomato Soup\") { \"Tomato Soup\" }\n }\n }\n }\n\n page recipe(name : String) {\n navigate root() { \"Back to homepage\" }\n\n header { \"Recipe: ~name\" }\n }\n
String interpolation In the code snippet above, String interpolation (with a tilde ~
) is used to insert the parameter name
in the header: \"Recipe: ~name\"
.
The updated version of our application passes the recipe name as page pararmeter. This change is reflected in the URL. Instead of http://localhost:8080/cookbook/recipe
, the name
parameter is now a part of the new URL: http://localhost:8080/cookbook/recipe/<name>
.
In this tutorial, we created an online cookbook using multiple page definitions, page parameters and navigation between pages.
"},{"location":"tutorials/templates/","title":"Templates","text":"In this tutorial we will change the online cookbook from previous tutorial to have less code duplication. To achieve this, we have to:
WORK IN PROGRESS
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"WebDSL","text":"WebDSL is a domain-specific language for developing dynamic web applications with a rich data model, developed and maintained by the Programming Language Group at Delft University of Technology.
"},{"location":"background/","title":"Background","text":"This section contains information on the ideas, architecture, and design decisions behind WebDSL. For the WebDSL language reference, see the Reference section.
"},{"location":"background/about/","title":"About WebDSL","text":"WebDSL is a domain-specific language for developing dynamic web applications with a rich data model.
"},{"location":"background/about/#features","title":"Features","text":"WebDSL is being developed by Eelco Visser and Ph.D./M.Sc. students in the context of the Model-Driven Software Evolution project at Delft University of Technology.
"},{"location":"background/about/#active-developers","title":"Active developers","text":"Meeting notes of past WebDSL meetings are documented here.
"},{"location":"background/meeting-notes/#webdsl-improvement-meeting-may-2021","title":"WebDSL Improvement Meeting May 2021","text":"The following points were discussed in a WebDSL improvement meeting.
"},{"location":"background/meeting-notes/#documentation","title":"Documentation","text":".length
and .length()
are used for different types.inputajax
for Secret
and WikiText
types.application.ini
. Certain database modes would not work on certain operating systems, the default one has to work on all platforms.All WebDSL publications on researchr.org
"},{"location":"howtos/","title":"How-To's","text":"These are some How-To's that help you to get to a specific goal or result with WebDSL. For hands-on tutorials on learning WebDSL, see the Tutorials section. For the WebDSL language reference, see the Reference section.
"},{"location":"howtos/#installation-and-build","title":"Installation and Build","text":"Download the WebDSL CLI for your platform:
WebDSL CLI
"},{"location":"howtos/install-cli/#installation","title":"Installation","text":"An installation of Java 8 or newer and Apache Ant are required.
webdsl/bin
directory to your $PATH
.Go to the directory of your application and execute:
webdsl run appname.app\n
This will override any existing application.ini
file.
This generates an application.ini
file configured for testing with an H2 in-memory database.
If there is already an application.ini
file with configuration (see Application Configuration options), use instead:
webdsl run\n
Pressing Ctrl+C stops the application.
Faster compilation on command-lineYou can avoid JVM startup overhead by keeping the WebDSL compiler process running.
This requires having the Nailgun client installed. For example, on macOS you can install nailgun
using brew install nailgun
.
Add to the application.ini
configuration file of your project the following line to use the compile server for all the commands like webdsl run
:
usecompileserver=true\n
To start the WebDSL nailgun server process, invoke:
webdsl start\n
"},{"location":"howtos/install-eclipse-bundle/","title":"Install the Eclipse with WebDSL Plugin Bundle","text":"Install an Eclipse instance with the WebDSL plugin pre-installed for your platform:
WebDSL in Eclipse bundle
"},{"location":"howtos/install-eclipse-bundle/#troubleshooting","title":"Troubleshooting","text":""},{"location":"howtos/install-eclipse-bundle/#macos-eclipse-cannot-be-opened-because-the-developer-could-not-be-verified","title":"macOS: \"Eclipse\" cannot be opened because the developer could not be verified","text":"macOS puts unverified binaries in 'quarantine' and disallows their execution. To remove the com.apple.quarantine
attribute, do:
xattr -rc Eclipse.app\n
"},{"location":"howtos/install-eclipse-bundle/#eclipse-does-not-start-or-complains-about-missing-java","title":"Eclipse does not start, or complains about missing Java","text":"Ensure you have a distribution of Java installed. Then in eclipse.ini
, add a -vm
line at the top of the file, followed by the path to the Java installation. For example, with SDKMan! on macOS:
-vm\n/Users/myusername/.sdkman/candidates/java/current/jre/lib/jli/libjli.dylib\n
"},{"location":"howtos/install-eclipse-plugin-manually/","title":"Install the WebDSL Eclipse Plugin","text":"Perform a manual installation of the WebDSL plugin in Eclipse 3.5 or newer.
In the Work with: text area, type:
https://update.webdsl.org/update\n
Uncheck Group items by category to make the plugin visible.
It is recommended that the eclipse.ini
of Eclipse is updated to give WebDSL enough stack space and memory to function correctly. Include the following options in eclipse.ini
, below the line that starts with -vmargs
.
-Xss8m -Xms256m -Xmx1024m -XX:MaxPermSize=256m -server\n
On macOS this file can be found at Eclipse.app/Contents/MacOS/eclipse.ini
.
To get started with WebDSL, install the WebDSL editor and the WebDSL Command-Line interface.
"},{"location":"howtos/install/#webdsl-editor","title":"WebDSL Editor","text":"We provide an Eclipse bundle in which you can create and edit WebDSL applications with the assistance of syntax highlighting, static analysis and code completion. Alternatively, you can download the WebDSL plugin in your own Eclipse instance.
Recommended: Eclipse BundleDownload an Eclipse instance with the latest WebDSL plugin pre-installed for your platform:
WebDSL in Eclipse bundle
Installation instructions.
Alternative: Eclipse PluginPerform a manual installation of the WebDSL plugin in Eclipse 3.5 or newer through the update site:
https://update.webdsl.org/update\n
Installation instructions.
"},{"location":"howtos/install/#command-line-interface","title":"Command-Line Interface","text":"The WebDSL CLI transforms your WebDSL code to web applications.
Recommended: Download Latest BuildDownload the WebDSL CLI for your platform:
WebDSL CLI
Installation instructions.
Alternative: Build WebDSL YourselfClone the WebDSL GitHub repository and follow the instructions listed there.
:fontawesome-solid-external-link-alt: WebDSL GitHub Repository
"},{"location":"howtos/install/#start-creating-web-applications","title":"Start Creating Web Applications","text":"Once installed and started, you are ready to create web applications with WebDSL. Learn WebDSL by example through our tutorials.
"},{"location":"reference/","title":"Reference","text":"This is the WebDSL language reference. For more background information on the ideas, architecture, and design decisions behind WebDSL, see the Background section.
"},{"location":"reference/access-control/","title":"Access Control","text":""},{"location":"reference/access-control/#configuration-of-the-principal","title":"Configuration of the Principal","text":"The Access Control sublanguage is supported by a session entity that holds information regarding the currently logged in user. This session entity is called the securityContext and is configured as follows:
principal is User with credentials name, password\n
This states that the User entity will be the entity representing a logged in user. The credentials are not used in the current implementation (the idea is to derive a default login template). The resulting generated session entity will be:
session securityContext\n{\n principal : User\n loggedIn : Bool := this.principal != null\n}\n
Note that this principal declaration is used to enable access control in the application.
It will also generate authentication()
(both login and logout), login()
, and logout()
templates, and an authenticate
function that takes the credentials as arguments and, if they are correct, returns true and sets the principal (only String/Email/Secret-type credential properties are allowed).
Authentication can be added manually, instead of using the generated authentication templates. Here is a small example application with custom authentication:
principal is User with credentials name, password\n\nentity User {\n name : String\n password : Secret\n}\n\ntemplate login {\n var username := \"\"\n var password : Secret := \"\"\n\n form { \n label(\"Name: \"){ input(username) }\n label(\"Password: \"){ input(password) }\n captcha()\n submit login() { \"Log In\" }\n }\n\n action login(){\n validate(authenticate(username,password), \n \"The login credentials are not valid.\");\n message(\"You are now logged in.\");\n }\n}\n\ntemplate logout {\n \"Logged in as \" output(securityContext.principal)\n form{\n submitlink logout() {\"Log Out\"}\n }\n action logout(){\n securityContext.principal := null;\n }\n}\n\npage root {\n login()\n \" \"\n logout()\n}\n\ninit {\n var u1 : User := \n User{ name := \"test\" password := (\"test\" as Secret).digest() };\n u1.save();\n}\n\naccess control rules\n\n rule page root(){\n true\n }\n
When storing a secret property you need to create a digest of it:
newUser.password := newUser.password.digest();\n
This makes sure the secret property is stored encrypted. A digest can be compared with an entered string using the check method: us.password.check(enteredpassword)\n
"},{"location":"reference/access-control/#protecting-resources","title":"Protecting Resources","text":"The default policy is to deny access to all pages and ajax templates, the rules determine what the conditions for allowing access are. Regular templates are accessible by default, however, you can add additional access control rules on templates to limit their accessibility.
A simple rule protecting the editUser page to be only accessable by the user being edited looks like this:
access control rules\n\n rule page editUser(u:User){\n u == principal \n }\n
An analysis of this rule: section some description
.Matching can be done a bit more freely using a trailing * as wildcard character, both in resource name and arguments:
rule page viewUs*(*){\n true\n}\n
When more fine-grained control is needed for rules, it is possible to specify nested rules. This implies that the nested rule is only valid for usage of that resource inside the parent resource. The allowed combinations are page - action, template - action, page - template. The next example shows nested rules for actions in a page:
rule page editDocument(d:Document){\n d.author == principal\n rule action save(){\n d.author == principal\n }\n rule action cancel(){\n d.author == principal\n } \n }\n
This flexibility is often not necessary, and it is also inconvenient having to explicitly allow all the actions on the page, for these reasons some extra desugaring rules were added. When specifying a check on a page or template without nested checks, a generic action rules block with the same check is added to it by default. For example:
rule page editDocument(d:Document){\n d.author == principal \n }\n
becomes rule page editDocument(d:Document){\n d.author == principal\n rule action *(*)\n {\n d.author == principal\n } \n }\n
"},{"location":"reference/access-control/#reuse-in-access-control-rules","title":"Reuse in Access Control rules","text":"Predicates are functions consisting of one boolean expression, which allows reusing complicated expressions, or simply giving better structure to the policy implementation. An example of a predicate:
predicate mayViewDocument (u:User, d:Document){\n d.author == principal\n || u in d.allowedUsers\n }\n rule page viewDocument(d:Document){\n mayViewDocument(principal,d)\n }\n rule page showDocument(d:Document){\n mayViewDocument(principal,d)\n }\n
"},{"location":"reference/access-control/#inferring-visibility","title":"Inferring Visibility","text":"A disabled page or action redirects to a very simple page stating access denied. Since this is not very user friendly, the visibility of navigate links and action buttons/links are automatically made conditional using the same check as the corresponding resource. An example conditional navigate:
if(mayViewDocument(securityContext.principal,d)){\n navigate(viewDocument(d)){ \"view \" output(d.title) } \n}\n
When using conditional forms it is often more convenient to put the form in a template, and control the visibility by a rule on the template.
"},{"location":"reference/access-control/#using-entities","title":"Using Entities","text":"Access Control policies that rely on extra data can create new or extend existing properties. An example of extending an entity is adding a set of users property to a document representing the users allowed access to that document:
extend entity Document{\n allowedUsers : {User}\n }\n
"},{"location":"reference/access-control/#administration-of-access-control","title":"Administration of Access Control","text":"Administration of Access Control in WebDSL is done by the normal WebDSL page definitions. All the data of the Access Control policy is integrated into the WebDSL application. An option is to incorporate the administration into an existing page with a template. This example illustrates the use of a template for administration:
template allowedUsersRow(document:Document){\n row{ \"Allowed Users:\" input(document.allowedUsers) }\n }\n
The template call for this template is added to the editDocument page: table{\n row{ \"Title:\" input(document.title) }\n row{ \"Text:\" input(document.text) }\n row{ \"Author:\" input(document.author) } \n allowedUsersRow(document)\n }\n
By using a template the Access Control can be disabled easily by not including the access control definitions and the template. The unresolved template definitions will give a warning but the page will generate normally and ignore the template call."},{"location":"reference/access-control/#minimal-access-control-example","title":"Minimal Access Control Example","text":"application minimalac\n\n entity User {\n name : String\n password : Secret\n }\n\n init {\n var u := User{ name := \"1\" password := (\"1\" as Secret).digest() };\n u.save();\n }\n\n page root {\n authentication()\n \" \"\n navigate protectedPage() { \"go\" }\n }\n\n page protectedPage { \"access granted\" }\n\n principal is User with credentials name, password\n\n access control rules\n\n rule page root(){true}\n rule page protectedPage(){loggedIn()}\n
"},{"location":"reference/action-code/","title":"Action Code","text":"This section describes the expressions and statements available in WebDSL.
"},{"location":"reference/action-code/#expressions","title":"Expressions","text":"literals A number of literals are supported:
\"This is a string\"
22
8.3
true
/false
[<expression>, <expression>, ...]
List<Int>()
{<expression>, <expression>, ...}
Set<Int>()
null
operators The following operators are supported:
+
-
*
/
%
as
(example: 8 as Float
)binary operators
==
!=
>
>=
<
<=
is a
(checks if a certain expression is of a certain runtime type)in
(checks if a certain expression is contained in a collection)&&
||
!
Example:
if(!(b is a String) && (b in [8, 5] || b + 3 = 7)) {\n // ...\n}\n
variables Variables can be accessed by use of their identifiers and their properties using the . notation. Example: person.lastName
indexed access List elements can be retrieved and assigned using index access syntax:
var a := list[0]; \nlist[2] := \"test\";\n
"},{"location":"reference/action-code/#functions","title":"Functions","text":"Functions can be defined globally and as methods in entities:
function sayHello(to : String) : String {\n return \"Hello, \" + to;\n}\n\nentity User {\n name : String\n\n function showName() : String {\n return sayHello(name + \"!\");\n }\n}\n
As of August 2016, entity functions without arguments can also be preceded with the cache
keyword. This cache operates at the request level, i.e. it only calculates its value once per request. This is useful for cases where a more expensive function is repeatedly invoked (e.g. for access control).
entity SubForum {\n name : String\n managers : [User]\n ...\n\n cached function isManager() : String {\n return loggedIn() \n && ( principal() in managers || parentForum.isManager() )\n }\n}\n
"},{"location":"reference/action-code/#variable-declaration","title":"Variable Declaration","text":"Variables can be defined globally, in pages (see Page Variables, and in code blocks.
Syntax:
var <identifier> : <Sort>;\n
This defines a variable within the current scope with name identifier and type Sort.
Variable declarations can also have an expression that initializes the value:
var <identifier> [: <Sort>] := expression;\n
The sort is optional in this case, if the Sort is not declared, the var will receive the type resulting from the expression (also known as local type inference).
Global variables always need an expression for initialization, they are added to the database once (when the first page is loaded, the database is checked to see whether all global vars have been created already). Global variables can be edited, but removing them can cause problems when there are explicit references to those variables.
Global variables can be further initialized using a global init{} block, e.g.
var defaultUser := User{ name := \"default\" }\ninit {\n defaultUser.someInitializeFunction();\n}\npage root {\n output(defaultUser.name)\n}\n
Global inits are also performed only once after database creation (if the dbmode is create-drop each new deploy will recreate the globals and execute inits, see App Configuration.
The ; is optional for global and page variable declarations.
"},{"location":"reference/action-code/#assignment","title":"Assignment","text":"The syntax of an assignment:
<variable> := <value expression>;\n
Example:
p.lastName := \"Doe\";\n
"},{"location":"reference/action-code/#return","title":"Return","text":"Syntax:
return <expression>;\n
Example:
function test() : String{\n return p.lastName;\n}\n
In the context of a entity function this returns the expression as the result of that function. In the context of an action or page init definition, it redirects the user to the page specified in the expression.
Example:
action done() { return root(); }\n
"},{"location":"reference/action-code/#for-loop","title":"For-loop","text":"Iterating a collection of entities or primitives can be done using a for loop. There are three types of for loop statements:
For
This type of for loop iterates the collection produced by expression e, which must contain elements of type t. The elements in the collection are accessible through identifier id.
The collection can be filtered:
for (id:t in e) { stat* }\n for (id:t in e filter) { stat* }\n
ForAll
This for loop iterates all the entities in the database of type t. These can also be filtered. Note that it is more efficient to retrieve the objects using a filtering query and use the regular for loop above for iteration.
for (id:t) { stat* }\n for (id:t filter) { stat* }\n
For Count
This for loop iterates the numbers from e1 to e2-1.
for (id:Int from e1 to e2) { stat* }\n
For-loop Filter
The filter part of a for loop can consist of four parts:
Where
where e1\n
e1 is a boolean expression which needs to evaluate to true for the element to be iterated.
Order By
order by e2 asc/desc\n
e2 is an expression that needs to produce a primitive type such as String or Int, which will be used to order the elements ascending or descending.
Limit
limit e3\n
e3 is an Int expression which will limit the number of elements that get iterated.
Offset
offset e4\n
e4 is an Int expression which will offset the starting element of the iteration.
Each of the four parts is optional, but they have to be specified in this order. The filtering is done in the application, so use queries instead of filters to optimize the WebDSL application.
"},{"location":"reference/action-code/#list-comprehension","title":"List Comprehension","text":"List comprehensions are a combination of mapping, filtering and sorting.
[ e1 | id : t in e2 ]\n
e2 produces a collection of elements with type t, e1 is an expression that allows transformation of the elements using identifier id.
Filters are also allowed:
[ e1 | id : t in e2 filter ]\n
Example:
[e.title | e : BlogEntry in b.entries \n where e.created > date \n order by e.created desc]\n
This expression returns all titles (e.title) from b.entries where the time created (e.created) is greater than a certain date, ordered by e.created in descending order. Both the where and order by clauses are optional. An ordering is either ascending (asc) or descending (desc).
Conjunction
And [ e1 | id : t in e2 ]\n
If e1 produces a boolean, the list comprehension can be preceded by \"And\" to create the conjunction of the elements produced by the list comprehension.
Disjunction
Or [ e1 | id : t in e2 ]\n
If e1 produces a boolean, the list comprehension can be preceded by \"Or\" to create the disjunction of the elements produced by the list comprehension.
"},{"location":"reference/action-code/#while-statement","title":"While Statement","text":"Besides for loops, iteration can also be performed using the while statement.
while (e) { stat* }\n
This will repeat stat* while e evaluates to true.
"},{"location":"reference/action-code/#switch-statement","title":"Switch Statement","text":"The case-statement has the following syntax:
case (<expression>) {\n [case <expr-1> {\n <block executed if true>\n }] *\n [default {\n <block executed if no cases match>\n }]\n}</verbatim>\n
Any number of cases and optionally one default case can be specified.
Example:
case (formatNumber) {\n 1 {\n // format is one\n }\n 2 {\n // format is two\n }\n default {\n // format is neither one nor two\n }\n}\n
"},{"location":"reference/action-code/#regular-expressions-regex","title":"Regular Expressions / Regex","text":"Examples in codefinder
"},{"location":"reference/action-code/#render-template-to-string","title":"Render Template to String","text":"The rendertemplate
function can be used to render template contents to a String.
rendertemplate(TemplateCall):String\n
Example:
template test(a:Int){ output(a) \"!\" }\nfunction showContent(i:Int){\n log(rendertemplate(test(i)));\n}\n
"},{"location":"reference/advanced-search/","title":"Advanced Search","text":"note: Some syntax changes and additional features are expected for WebDSL 1.3.0. The search language will become more structured. This manual will be updated/completed upon 1.3.0 release
WebDSL offers full text search engine capabilities based on Apache Lucene and Hibernate Search. Current implementation supports:
Using search in WebDSL starts by marking which entities need to be searchable. If one property is marked searchable, the entity can be searched. For each entity property one or more search fields can be specified. There are 2 ways to specify these: using search mappings or using searchable annotations. For simple search functionality, searchable annotations will suffice, but for cleaner code we recommend using search mappings.
"},{"location":"reference/advanced-search/#using-search-mappings-recommended","title":"Using search mappings (recommended)","text":"A search mapping starts with the name of the property to be indexed, optionally followed by mapping specifications:
"},{"location":"reference/advanced-search/#as-name","title":"asname
","text":"Override the default search field name
. Default: property name
analyzer
","text":"Indexed using analyzer analyzer
instead of the default analyzer.
Float
|^Float
","text":"Search field is boosted to Float
at index time (default 1.0).
Indicate that this search field can be used for spell checking/autocompletion.
"},{"location":"reference/advanced-search/#for-subclass-entity","title":"for subclassentity
","text":"In case marking an reference/composite property as searchable, you might want to make only a specific subclass of the property type searchable.
"},{"location":"reference/advanced-search/#depth-intwith-depth-int","title":"depthInt
|with depth Int
","text":"In case marking an reference/composite property as searchable, you can specify the depth of the 'embedded' path, 1 is the default.
"},{"location":"reference/advanced-search/#mapping-specification","title":"+mapping specification
","text":"Prefix a mapping specification
with the plus sign if you want this search field to be used by default at query time. If no default search field is specified, all search fields are used by default.
Search mappings belong to an entity and can be placed inside an entity declaration, or somewhere else by adding the entity name. Names of the search fields are scoped to entities, so different entities may share the same names for search fields.
// Embedded search mapping\nentity Message {\n subject : String\n text : Text\n category : String\n sender : User\n\n search mapping {\n +subject\n +text using snowballporter as textSnowBall\n text\n category\n +sender for subclass ForumUser\n }\n}\n
// External search mapping\nentity ForumUser : User {\n forumName : String\n forumPwd : Secret\n messages : {Message} (inverse=Message.sender)\n}\n...\nsearch mapping ForumUser {\n forumName using none\n}\n
"},{"location":"reference/advanced-search/#using-annotations","title":"Using annotations","text":"Search fields can also be specified using property annotations:
// Using searchable annotations\nentity Message {\n subject : String (searchable)\n text : Text (searchable, searchable(name=textSnowBall, analyzer=snowballporter)\n category : String (searchable)\n sender : ForumUser (searchable())\n}\n
The above code marks the entity Message
searchable, and it has 3 search fields: subject
, text
using the default analyzer, and textSnowball
, which uses the snowball porter analyzer. Searchable annotations have no restriction w.r.t. search mappings, and both can be used interchangeably (not recommended since it's less transparent). The following table shows the annotation equivalent of specifications in search mappings.
Properties of any type can be made searchable, although there are some notes to make.
"},{"location":"reference/advanced-search/#reference-and-composite-properties","title":"Reference and composite properties","text":"These properties don't contain any text or value by themselves, but hold references to other entities. Therefore, the properties themselves cannot be indexed, but the searchable properties of the referred entity/entities will be indexed in the scope of the current entity. For example if you want to be able to search for Message
entities by the name of the sender (in the above example), the property forumName
of ForumUser
needs to be indexed in the scope of Message
. This can be done by marking the sender
property as searchable. All search fields from ForumUser
will then be available for Message
, and searchfields are prefixed with 'propertyName.
' by default (or different name if specified using as
in search mappings). The search field from the example becomes : sender.forumName
.
Note: Searchable reference/composite properties need to be part of an inverse relation to keep the index of the owning entity updated with changes in its reference entity/entities. The mapping options available for reference properties are restricted to name
and subclass
.
In case no analyzer is specified for a numeric property search field, it will be indexed as numeric fields, which is a special type of field in Lucene. It enables efficient range queries and sorting on this field.
"},{"location":"reference/advanced-search/#derived-properties","title":"Derived properties","text":"Derived properties are currently only indexed when the entity owning this property is saved/changed.
"},{"location":"reference/advanced-search/#how-to-analyze-your-dataqueries","title":"How to analyze your data/queries","text":"By default, textual properties will use the default analyzer from Lucene, which is optimized for the English language. In the specification of a search field (in search mapping or searchable annotation), a different analyzer can be assigned to it like is done for the textSnowBall
search field. A custom analyzers can be declared, each containing:
The range of tokenizers and filters that are supported can be found here and here (with more information about specific analyzers). You don't need to use the factory keyword at the end. Useful analyzers definitions are already included in a new WebDSL project under ./search/searchconfiguration.app. The default analyzer can be overwritten by adding the default
keyword before analyzer
. More advanced analysis may require different behavior at search and query time. Using the index { ... }
and query { ... }
block, the analyzers may be specified different for indexing and query time (see the synonym analyzer).
For each indexed entity, search functions and a searcher class are automatically generated. For simple searches, the generated functions will suffice. For more advanced searches, the magic is in the generated entity searcher(s).
"},{"location":"reference/advanced-search/#search-data-using-generated-search-functions","title":"Search data using generated search functions","text":"For the example entity Message
, the following search functions are generated.
function searchMessage(query : String) : [Message]\nfunction searchMessage(query : String, limit : Int) : [Message]\nfunction searchMessage(query : String, limit : Int, offset : Int) : [Message]\n
The limit and offset parameters can be used for paginated results. It only loads at most the limit
number of results from the database (for efficiency/faster pageloading). These functions use the default search fields when searching, and the specified analyzers are applied for each search field."},{"location":"reference/advanced-search/#search-data-using-webdsl-search-language-for-full-text-search","title":"Search data using WebDSL search language for full text search","text":"More features are available when using WebDSL's search language designed to perform search operations. The language let you interact with the generated Searcher object for the targeted entity. A reference to (or initialization of) a searcher is followed by one or more constructs in which search criteria can be declared.
//matches Messages with \"tablet\", but without \"ipad\"\nvar msgSearcher := search Message matching +\"tablet\", -\"ipad\";\n\n//enable faceting on an existing searcher\nmsgSearcher := ~msgSearcher with facets sender.forumName(20), category(10)\n
List of search language constructs:
"},{"location":"reference/advanced-search/#retrieving-search-results","title":"Retrieving search results","text":"var searcher := search Book matching author: \"dahl\";\nvar results := searcher.results(); //returns [Book];\n
Calling .results() on a searcher returns the search results. Calling .count() on a searcher returns the total number of results."},{"location":"reference/advanced-search/#simple-and-boolean-queries-matching-field-qexp","title":"Simple and boolean queries: 'matching { [{field ,}:] {qExp ,} ,}'","text":"searcher := search Entity matching title: \"user interface\";\nsearcher := search Entity matching title, description: userQuery; \nsearcher := search Entity matching \"user interface\";\nsearcher := search Entity matching title: +userQuery, -\"case study\"; \nsearcher := search Entity matching ranking:4 to 5, title:-\"language\"; \n
Declares a searcher that matches a simple or boolean query. Fields are optional: if the query expression is not preceded by a field constraint, the default search fields are used (i.e. all search fields if no default fields are defined, see ...). qExp can be any String compatible WebDSL expression or a range expression optionally prefixed with a boolean operator (+ for must, - for mustnot, nothing for should)."},{"location":"reference/advanced-search/#range-queries","title":"Range queries","text":"searcher := search Entity matching rating: {1 to 3}\nsearcher := search Entity matching rating: [startDate to endDate]\nsearcher := search Entity matching rating: -[* to sinceDate]\n
Range expressions are in the form [minExp to maxExp] (including min and max value) or {minExp to maxExp} (exludes min and max, where both expressions can be any expression of a simple WebDSL builtin type. An open range is specified with an asterisk : [* to \"A\"} for example."},{"location":"reference/advanced-search/#pagination","title":"Pagination","text":"var searcher := search Book matching author: \"dahl\" start 20 limit 10\n
With the start and limit keywords, you can control which results to be retrieved."},{"location":"reference/advanced-search/#configuration-options-option","title":"Configuration options: '[ {option* ,} ]'","text":"searcher := search Entity on title: q [no lucene, strict matching];\n
Declare the searcher's options. Available options are: searcher := search Entity matching title: \"graph\" \n with filter hidden:false;\n
Specify a filter constraint. A filter constraint is a field-value expression. Be aware that when using a filter, a bitset is constructed and cached to accelerate future queries using the same filter. Filters are not considered in result ranking. Thus, only use field-value filters if you expect the same filtering to occur frequently."},{"location":"reference/advanced-search/#enabling-facets-with-facets-field1e1-field2e2","title":"Enabling facets: 'with facet(s) field1(e1), field2(e2)'","text":"Example:
searcher := search Entity matching title: \"graph\" with facet author(10);\nsearcher := search Entity matching title: \"graph\" with facets author(10), rating([* to 1],[2 to 3},[3 to 4},[4 to *]);\n
Specify enabled facets. These can be discrete or range facets"},{"location":"reference/advanced-search/#retrieving-facets-field-facets-from-searcherexp","title":"Retrieving facets: 'field facets from searcherExp'","text":"facets := author facets from s;\n
Returns a list: [Facet]
with the facets for the specified field. Facet objects have the following boolean functions available, for example to apply different styling on the variety of facet states: searcher := ~searcher with filter(s) selectedDateFacet.must(), selectedPriceFacet.must();\n
Previously returned facets can be used to narrow the search results. The behaviour of the facet (must, should, mustnot) can be set on the facet object itself (should by default)."},{"location":"reference/advanced-search/#namespace-scoping-in-namespace-e1","title":"Namespace scoping: 'in namespace e1'","text":"searcher := search Entity matching title: \"graph\" in namespace \"science\";\n
When using search namespaces, restricting a search to a single namespace is done using the in namespace construct followed by a String-compatible expression."},{"location":"reference/advanced-search/#search-data-using-native-java-instead-of-search-language-some-expert-features","title":"Search data using native java instead of search language (some expert features)","text":"The searcher class that is created for the example Message
entity is MessageSearcher
. The first advantage of using this searcher instead of the generated functions is the ability to interact with the searcher, for further refinements to the search query, or to get information like the total number of results, or time that was needed to perform the search.
page searchPage(query : String) {\n var searcher := MessageSearcher().query(query);\n var results := searcher.results();\n var searchTime := searcher.searchTime(); //String\n\n \"You searched for '\" output(searcher.query()) \"', \" output(searcher.count()) \" results found in \" output(searchTime) \".\"\n\n if(searcher.count() > 0) {\n showResults(results)\n }\n}\ntemplate showResults(results : [Message]) {\n //code to view results\n} \n
The available searcher functions generated for each searchable entity are:
"},{"location":"reference/advanced-search/#disallow-use-of-lucene-in-query-and-filter-values","title":"(Dis)Allow use of Lucene in query and filter values","text":"(see here)
allowLuceneSyntax(allow : Bool) : EntitySearcher\n
"},{"location":"reference/advanced-search/#orand-terms-in-user-queries-by-default","title":"OR/AND terms in user queries by default","text":"OR is the default.
defaultAnd() : EntitySearcher\ndefaultOr() : EntitySearcher\n
"},{"location":"reference/advanced-search/#filter-results-by-field-value-get-filter-value","title":"Filter results by field value, get filter value","text":"addFieldFilter(field : String, value : String) : EntitySearcher\ngetFieldFilterValue(field : String) : String\ngetFilteredFields() : [String]\nremoveFieldFilter(field : String)\nclearFieldFilters()\n
"},{"location":"reference/advanced-search/#get-spellautocomplete-suggestions","title":"Get spell/autocomplete suggestions","text":"The field(s) parameters specify which search field(s) to use for suggestions. 'limit' controls the max number of suggestions to retrieve. Additionally the namespace can be specified, if used. For spell suggestions the accuracy [0..1] can be set
static autoCompleteSuggest(toComplete : String, field : String, limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, namespace : String, field : String, limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, fields : [String], limit : Int) : [String]\nstatic autoCompleteSuggest(toComplete : String, namespace : String, fields : [String], limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, fields : [String], accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, namespace : String, fields : [String], accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, field : String, accuracy : Float, limit : Int) : [String]\nstatic spellSuggest(toCorrect : String, namespace : String, field : String, accuracy : Float, limit : Int) : [String]\n
"},{"location":"reference/advanced-search/#indecrease-the-impact-of-a-search-field-in-ranking-of-results-by-boosting-at-query-time","title":"In/Decrease the impact of a search field in ranking of results by boosting at query-time","text":"boost(field : String, boost : Float) : EntitySearcher\n
"},{"location":"reference/advanced-search/#faceting-on-a-search-field","title":"Faceting on a search field","text":"The max
parameter defines the maximum facets to collect for that field. For range facets, the ranges are encoded as String in the same format as range queries. Multiple ranges can be specified concatenated, optionally seperated with a symbol like white space or comma but that's not required.\"
enableFaceting(field : String, max : Int) : EntitySearcher\nenableFaceting(field : String, rangesAsString : String) : EntitySearcher\ngetFacets(field : String) : [Facet]\naddFacetSelection(facet : Facet) : EntitySearcher\naddFacetSelection(facets : [Facet]) : EntitySearcher\ngetFacetSelection() : [Facet]\ngetFacetSelection(field : String) : [Facet]\nremoveFacetSelection(facet : Facet) : EntitySearcher\nclearFacetSelection() : EntitySearcher\nclearFacetSelection(field : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#specify-search-fields-to-use-for-query-or-range","title":"Specify search field(s) to use for query or range","text":"field(field : String) : EntitySearcher\nfields(fields : [String]) : EntitySearcher\n
"},{"location":"reference/advanced-search/#specify-offset-and-number-of-results-for-pagination","title":"Specify offset and number of results (for pagination)","text":"setOffset(offset : Int) : EntitySearcher\nsetLimit(limit : Int) : EntitySearcher\n
"},{"location":"reference/advanced-search/#hit-highlighting","title":"Hit highlighting","text":"Highlight matched tokens using the analyzer from the specified search field in a given text, optionally specifying a pre- and posttag (bold by default), number of fragments, fragment length and fragment separator. There are 4 types of highlight methods. Replace highlight with the version that is suitable for you:
highlight(field : String, toHighlight : String) : String\nhighlight(field : String, toHighlight : String, preTag : String, postTag : String) : String\nhighlight(field : String, toHighlight : String, preTag : String, postTag : String, nOfFrgmts : Int, frgmtLength : Int, frgmtSeparator : String) : String\n
"},{"location":"reference/advanced-search/#find-similar-entities-based-on-text-fragment","title":"Find similar entities based on text fragment","text":"Just like an ordinary query, first specify the fields using the field(s)
function
moreLikeThis(text : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#setget-the-current-text-query","title":"Set/get the current text query","text":"Note: Query text from the first specified query is returned in case multiple queries are combined using boolean queries.
getQuery() : String\nquery(queryText : String) : EntitySearcher\n
"},{"location":"reference/advanced-search/#sort-results-by-field-ascending-or-descending","title":"Sort results by field ascending or descending","text":"sortDesc(field : String) : EntitySearcher\nsortAsc(field : String) : EntitySearcher\nclearSorting() : EntitySearcher\n
"},{"location":"reference/advanced-search/#range-query-start-and-end-can-be-type-of-string-int-float-and-datedatetimetime-start-and-end-are-included-by-default","title":"Range query, start and end can be type of String, Int, Float and Date/DateTime/Time. start and end are included by default","text":"range(start, end) : EntitySearcher\nrange(start, end, includeMin : Bool, includeMax : Bool) : EntitySearcher\n
"},{"location":"reference/advanced-search/#setget-namespace","title":"Set/get namespace","text":"setNamespace(ns : String) : EntitySearcher\ngetNamespace() : String\nremoveNamespace() : EntitySearcher\n
"},{"location":"reference/advanced-search/#get-the-list-of-results","title":"Get the list of results","text":"results() : [Entity]\n
"},{"location":"reference/advanced-search/#get-the-number-of-results","title":"Get the number of results","text":"count() : Int\n
"},{"location":"reference/advanced-search/#get-the-search-time","title":"Get the search time","text":"searchTime() : String\nsearchTimeMillis() : Int\nsearchTimeSeconds() : Float\n
"},{"location":"reference/advanced-search/#filters","title":"Filters","text":"Filters are an efficient way to filter search results, because they are cached. If you expect to perform many queries using the same filter (like only showing Message
s in a specific category), using a filter is the way to go:
MessageSearcher.query(userQuery).addFieldFilter(\"category\",\"humor\")\n
or
search Message matching userQuery with filter category:\"humor\"\n
To get the value of a previously added field filter, use the getFieldFilterValue(field : String)
method.
Search namespaces become usefull if you want to allow searches on entities with some specific property value. For example searching Message
s by category in the above example. Namespaces have some advantages over using field filters. An index is created for each namespace separately, instead of one for all entities of that type. Since the indexes are used as input for auto completion and spell checking, the use of namespaces enables suggestion services scoped to one, or all, namespace(s).
Facets can be displayed in many contexts. For example, when displaying a list of products, you want the product categories to be displayed as facets. Any searchable property can be used for faceting. The values, as they appear in the search index, are used for faceting. So if you use the default analyzer for the category property of Product, categories containing white spaces are not treated as single facet value. For this to work you need to define an additional field which doesn't tokenize the value of the property, for example by indexing this property untokenized:
entity Product{\n name : String\n categories : {Category} (inverse=Category.products)\n\n search mapping{\n name\n categories\n }\n}\nentity Category {\n name : String\n products : {Product}\n\n search mapping{\n name using none //or 'name using no' in v1.2.9.0\n }\n}\n
Facets can be retrieved through the use of a searcher. You first need to specify the facets you want to use by enabling them in the searcher. A typical example is to display facets in the search results:
(updated April 5th)
template searchbar(){\n var query := \"\";\n form {\n input(query)\n submit action{\n //construct a searcher and enable faceting on tags.name, limited to 20 top categories\n //more facets can be enabled by separating the field(topN) facet definitions by a comma\n var searcher := search Product matching query with facets categories.name(20);\n return search(searcher);} {\"search\"}\n }\n}\n\npage search(searcher : ProductSearcher){\n var results : [Product] := results from searcher;\n var facets : [Facet] := categories.name facets from searcher;\n\n header{\"Filter by product category:\"}\n for(f : Facet in facets){\n facetLink(f, searcher)\n }separated-by{\" \"}\n\n showResults(results)\n}\ntemplate facetLink(facet: Facet, searcher: ProductSearcher){\n submitlink narrow(facet){ if(facet.isSelected()){\"+\"} output(facet.getValue()) }\"(\" output(facet.getCount()) \")\"\n\n action narrow(facet : Facet){\n if (facet.isSelected()) { searcher.removeFacetSelection(facet); } else { ~searcher matching facet.must(); }\n goto search(searcher);\n }\n}\n
"},{"location":"reference/ajax/","title":"Ajax","text":"WebDSL provides ajax operations which allow you to easily replace an element or group of elements in a page without reloading the entire page. These operations can be used as statements inside actions or functions.
"},{"location":"reference/ajax/#ajax-operations","title":"Ajax Operations","text":"replace(target, templatecall);\nappend (target, templatecall);\nclear (target);\n\nrestyle (target, String);\nrelocate(pagecall);\nrefresh();\n\nvisibility(target, show/hide/toggle);\n\nrunscript(String);\n
The most commonly used operation is replace, for example:
action someaction() {\n replace(body, showUser(user)); \n}\n
This will replace the contents of an element or placeholder (see ajax targets section below) that has id 'body' with the output of the templatecall showUser(user)
.
runscript
provides a way to interface with arbitrary Javascript code. Put .js files in a javascript directory in the root of your project and include it using includeJS
in a template, e.g. includeJS(\"sdmenu.js\")
.
Example: Moving a div around using JQuery animate:
runscript(\"$('\"+id+\"').animate({left:'\"+x+\"', top:'\"+y+\"'},1000);\");\n
The refresh action is the default action; when no other interface changing operations are executed the browser will just refresh the current page. For example an input form which submits to a completely empty action results in the data being saved (default behavior) and the page being refreshed (default behavior). If good way to avoid refreshing (if this is really what you want), is to use runscript(\"\")
. Note that it's good practice to return a confirmation message instead of doing nothing.
There are three ways to target an ajax operation. target can either be
Placeholder with non-ajax default content:
placeholder leftbar { \"default content\" /* non-ajax default elements */ }\n
Placeholder with ajax default content:
placeholder leftbar ajaxTemplateCall() /* call to ajax template*/\n
Id attribute example:
table[id := myfirsttable] { /* non-ajax default elements */ }\n
When a template is used as target in an ajax operation, it must be declared with the ajax
modifier.
Example:
template testtemplate(p:Person){\n placeholder testph{ \"no details shown\" }\n submit(\"show details\",show())[ajax]\n action show(){\n replace(testph,showDetails(p));\n }\n}\najax template showDetails(person:Person){\n \" name: \" output(person.name)\n}\n
Since an ajaxtemplate results in an extra entry point at the server, it must be explicitly opened when access control is enabled:
rule ajaxtemplate showDetails(p:Person){true}\n
"},{"location":"reference/ajax/#dom-event-handling","title":"DOM Event Handling","text":"To invoke actions when an HTML event is fired, for example when pressing a key, event attributes can be defined on elements. The syntax of such an attribute is:
<event name> := <action call>\n
W3schools.com provides an overview of many available events. Note that onmouseenter
and onmouseleave
are also available, but not documented by W3Schools. Example:
\"quicksearch: \" \ninput(search)[onkeyup := updatesearch(search)]\n
The result is that the updatesearch
action is invoked on the server.
Typically you should not make a form cross an ajax placeholder. The server considers ajax templates as self-contained components similar to pages.
Example of proper usage:
template demo(){\n placeholder test()\n}\najax template test(){\n form{ input(someGlobal.name) submit action{} {\"save\"} }\n}\n
Example of incorrect usage (the submit will be contained in a form on the client but not on the server):
template demo(){\n form{\n placeholder test()\n }\n}\najax template test(){\n input(someGlobal.name) submit action{} {\"save\"}\n}\n
In some cases interaction between a regular form and ajax operations is not an issue, e.g. when the ajax template does not contain any input elements. The most common case is rendering validation messages in the form, this behavior is provided in the WebDSL library, see next section.
"},{"location":"reference/ajax/#ajax-input-validation","title":"Ajax Input Validation","text":"There are prebuild library components for creating inputs with ajax validation responses.
A simple example:
template demo(){ \n var s := \"test\"\n form{ \n inputajax(s)\n submit action{ log(s); } {\"log\"}\n }\n}\n
"},{"location":"reference/app-configuration/","title":"App Configuration","text":"In the application.ini file compile-, database- and deployment information is stored. Executing the webdsl command in a certain directory will look for a application.ini file to obtain compilation information. If no such file was found, it will start a simple wizard to create one.
Example application.ini:
backend=servlet\ntomcatpath=/opt/tomcat\nappname=hello\ndbserver=localhost\ndbuser=webdsluser\ndbpassword=webdslpassword\ndbname=webdsldb\ndbmode=update\nsmtphost=localhost\nsmtpport=25\nsmtpuser=\nsmtppass=\n
"},{"location":"reference/app-configuration/#required-configuration","title":"Required Configuration","text":"backend The back-end target platform of the application. Currently, the servlet back-end is only up-to-date.
appname The name of the application to compile. The compiler will look for a APPNAME.app file to compile. This name will also become the servlet name and show up as part of the URL. By renaming the generated APPNAME.war file to ROOT.war and then deploying it, the application name will not be in the URL.
tomcatpath This field should contain the root directory of the Tomcat installation. For example /opt/tomcat. It is used when executing 'webdsl deploy'.
"},{"location":"reference/app-configuration/#database-configuration-mysql","title":"Database Configuration MySQL","text":"dbmode This field indicates if the application should try to create tables in a database, or try to sync it with the existing schema to avoid loss of data. Valid values are create-drop, update, and false. Update can lead to unpredictable results if data model is changed too much. For production deployment use 'export DBMODE=false'.
dbserver Location of the Mysql server, which will be used in the connection URL, e.g. 'localhost'.
dbuser User to be used for connecting to the MySQL database.
dbpassword Password for the specified user.
dbname Database name, note that the database needs to exist when the application is run. The 'webdsl' script will try to create the database in the wizard, but manually creating it via command-line or MySQL Administrator is also possible.
"},{"location":"reference/app-configuration/#database-configuration-h2-database-engine-in-file","title":"Database Configuration H2 Database Engine in file","text":"db Set db=h2
to enable H2 Database Engine instead of the default MySQL.
dbfile H2 database file, an empty file will be populated with tables automatically, when using 'create-drop' or 'update' db modes.
dbmode Same as for MySQL.
"},{"location":"reference/app-configuration/#database-configuration-h2-database-engine-in-memory","title":"Database Configuration H2 Database Engine in memory","text":"db Set db=h2mem
to enable in-memory H2 Database Engine instead of the default MySQL.
dbmode Same as for MySQL, although effectively the tables are always dropped after a restart with in-memory database
"},{"location":"reference/app-configuration/#database-configuration-through-jndi","title":"Database Configuration through JNDI","text":"db Set db=jndi
to retrieve a JDBC resource from the application server, rather than providing the configuration in the web application.
dbjndipath JNDI path to the JDBC resource. On Apache Tomcat this is typically prefixed by 'java:comp/env'. An example may be: 'java:comp/env/jdbc/mydatabase'
dbmode Same as for MySQL.
Apart from settings in the application.ini, also a Context XML file must be provided for Apache Tomcat. An example may be:
<Context>\n <Resource name=\"jdbc/mydatabase\"\n auth=\"Container\"\n type=\"javax.sql.DataSource\"\n driverClassName=\"com.mysql.jdbc.Driver\"\n maxActivate=\"100\" maxIdle=\"30\" maxWait=\"10000\"\n username=\"root\" password=\"dbpassword\"\n url=\"jdbc:mysql://localhost:3306/mydatabase?useServerPrepStmts=false&characterEncoding=UTF-8&useUnicode=true&autoReconnect=true\" />\n</Context>\n
This XML file must be stored in: $TOMCAT_BASE/conf/Catalina/localhost/<appname>.xml
"},{"location":"reference/app-configuration/#email-configuration","title":"Email Configuration","text":"smtphost SMTP host for sending email, e.g. smtp.gmail.com
smtpport SMTP port for sending email, e.g. 465
smtpuser SMTP username
smtppass SMTP password
smtpprotocol smtpprotocol=smtps
[smtp/smtps] Use smtp or smtps as protocol.
smtpauthenticate smtpauthenticate=true
[true/false] Authenticate with a username and password.
indexdir set the index directory, default is /var/indexes.
searchstats Enable/disable search statistics, which can be displayed using template showSearchStats(). Default is false.
"},{"location":"reference/app-configuration/#optional-configuration","title":"Optional Configuration","text":"rootapp rootapp=true
will deploy the application as root application, it will not have the application name prefix in the URL.
wikitext-hardwraps wikitext-hardwraps=true
will enable so-called hard wraps in markdown. This way, each newline which isn't followed by 2 white spaces is also rendered as new line. Default is false. See http://yellowgrass.org/issue/WebDSL/818
appurlforrenderwithoutrequest (as of WebDSL 1.3.0) Sets the URL to be used when links to pages are to be rendered outside a request. Normally, WebDSL will construct links using the request URL as a base. In case pages or templates with links are to be rendered outside a request (e.g. using a background task), WebDSL will use this property value as the base url.
sessiontimeout Sets the session timeout, specified in minutes.
javacmem javacmem=3G
set javac max memory for compilation of generated Java classes
debug debug=true
will show queries and Java exception stacktraces in the log.
verbose verbose=2
will show more info during compilation, mainly for developers.
fastpp fastpp=true
will make the compiler write Java code faster (writing files stage), however, it also becomes less readable. (only for C-based back-end of the WebDSL compiler)
For the webdsl tomcatdeploy
and webdsl tomcatundeploy
commands to work, a user has to be configured in Tomcat (tomcat/conf/tomcat-users.xml). For example:
<tomcat-users>\n <role rolename=\"manager\"/>\n <user username=\"tomcat\" password=\"tomcat\" roles=\"manager\"/>\n</tomcat-users>\n
The tomcat manager URL and username and password can be set in the application.ini file (defaults are listed as examples):
tomcatmanager tomcatmanager=http:\\\\localhost:8080\\manager
URL to Tomcat manager
tomcatuser tomcatuser=tomcat
manager user declared in tomcat/conf/tomcat-users.xml
tomcatpassword tomcatpassword=tomcat
password for that user
The quickest way to get an application running is to execute:
webdsl run appname\n
This will generate an application.ini file with default settings, then compile the application, and start a Tomcat instance on port 8080 with the application deployed.
If there is already an application.ini file with settings that have to be used, execute:
webdsl run\n
This will also build and run, using the settings in the existing application.ini file.
To create just the war file instead, use:
webdsl war\n
"},{"location":"reference/cli-usage/#building-war-file-and-deploying-to-external-tomcat","title":"Building .war file and deploying to external Tomcat","text":"The installation of WebDSL will result in a webdsl script and a directory with templates being added to your install location. The script is used to invoke the compilation and deployment of WebDSL applications.
In your console, go to the location of the main .app file and invoke the webdsl script with
webdsl build\n
The script uses an application.ini file for configuration. If an application.ini file is not in the current directory, the script will offer an interactive way to generate it. If the application.ini is available it will be used to configure the application with e.g. database connection settings. The compilation begins by creating a .servletapp directory to which the WebDSL template, the application files, and the static resources are copied. Then the actual WebDSL compiler, webdslc, is invoked. This will either produce an error and halt, or it will produce the source code of a java web application. Upon a successful run of the webdsl compiler, the script will compile the java code, and build a war file. This war file can be copied manually to the tomcat /webapps dir, or it can be uploaded through the web deploy interface of tomcat. If the tomcat path is set in application.ini, then
webdsl deploy\n
will copy the war file to the /webapps directory.
If you have updated webdsl and need to copy the new WebDSL template in .servletapp use
webdsl cleanall\n
to remove the .servletapp directory (or simply delete it with rm) and then do a build.
The script commands can be combined, e.g.
webdsl cleanall build deploy\n
to clean the generated directory and its contents, regenerate, and deploy.
"},{"location":"reference/cli-usage/#example-application","title":"Example Application","text":"1 create a hello.app file
hello.app:
application test\n\npage root(){\n \"Hello world\"\n}\n
create or generate application.ini:
backend=servlet\ntomcatpath=**path to your tomcat directory e.g. /Apps/tomcat/**\nappname=hello\ndbserver=localhost\ndbuser=**mysql user account, e.g. root**\ndbpassword=**password**\ndbname=webdsldb\ndbmode=create-drop\nsmtphost=localhost\nsmtpport=25\nsmtpuser=\nsmtppass=\n
2 create the database
mysql -u root -p
create database webdsldb;
exit
3 start tomcat in another shell:
catalina.sh run (stop with cmd/ctrl+c)
or in the background
catalina.sh start (stop with catalina.sh stop)
4 compile and deploy WebDSL app
webdsl cleanall deploy
5 open browser and go to http://localhost:8080/hello
"},{"location":"reference/development/","title":"Development","text":"This section contains information for developers of WebDSL.
"},{"location":"reference/development/#debugging-the-generated-code-developers","title":"Debugging the generated code (developers)","text":"When running an application entirely in the Eclipse environment, you can choose to start debug mode in the 'Servers' view.
On the command-line, 'webdsl run' will set up the remote debugger interface, on the usual port 8000. Then set it up in eclipse: Run menu -> debug configurations\u2026 -> click on remote java applications -> new (icon top left) -> add source dirs of your project -> press 'debug'
"},{"location":"reference/entities/","title":"Entities","text":"Data models in WebDSL are defined using entity definitions. An entity definition consists of the entity's name, possibly a super-entity from which it inherits, 0 or more properties and 0 or more entity functions:
entity User {\n name : String (length = 25)\n email : Email\n password : Secret\n homepage : URL\n pages : {Page}\n function checkPassword( s: String ): Bool { \n return password.check( s );\n }\n predicate sameUser( u: User ){ this == u } \n}\n
A property consists of 3 parts:
a name
a property type, e.g. value types String, Int, Long, Text or reference/composite types which refer to other entities, such as Person, {Person} (set), and [Person] (list).
For a complete overview of the available types, see Types.
An example data model for a blogging site:
entity Author {\n name : String\n email : Email\n password : Secret\n posts : {Post} (inverse = author)\n}\n\nentity Post {\n author : Author\n title : String\n text : Text\n comments : {Comment} (inverse = post)\n}\n\nentity Comment {\n post : Post\n author : String\n text : Text\n}\n
"},{"location":"reference/entities/#instantiating-entity-objects","title":"Instantiating Entity Objects","text":"Instantiating new entity objects is done with the following expression:
Entity{ [property := value]* }\n
The entity name followed by an optional list of property assignments between curly brackets.
Example:
User{}\nUser{ name := \"Alice\" }\nUser{ name := \"Bob\" age := 34 }\n
Default initialization (what you would put into the constructor of an object in e.g. the Java programming language), can be added by extending the constructor function that is implicitly called.
Example:
entity A : B{\n extend function A(){\n name := name +\"A\";\n } \n}\n\nentity B{\n extend function B(){\n name := name +\"B\";\n } \n}\n\ntest constructors {\n var t := A{};\n assert(t.name == \"BA\");\n}\n
Creating an empty entity which doesn't call the constructor extensions can be done using createEmptyEntity, e.g. createEmptyUser()
The 'name' property is special, it is declared for each entity. By default it is a derived property that simply returns the id of the entity (which is also a special property declared for each entity, id:UUID is set automatically). The name can be customized by declaring a real name property:
name : String\n
Or derived name property:
name : String := firstname + lastname\n
Or by declaring a property as the name using an annotation:
someproperty : String (name)\n
The name property is used in input
and select
template elements to refer to an entity. Example:
application exampleapp\ninit{\n var u := User{};\n u.save();\n u := User{};\n u.save();\n u := User{};\n u.save();\n}\nentity User{} \nentity UserList{\n users : [User]\n}\nvar globalList := UserList{}\n\npage root {\n for( u in globalList.users ){\n output(u.name) //there is always a name property\n }\n form{ \n input( globalList.users ) //this will show three UUIDs as options\n submit action{ }{ \"save\" }\n }\n}\n
If the name is not a real property, you cannot create an input for it or assign to it.
Note that because 'name' is a derived property, by default you can't assign to it. To make 'name' mutable, you need to explicitly add it to the entity in one of the ways described above.
"},{"location":"reference/entities/#derived-properties","title":"Derived Properties","text":"A derived property is a property of an entity (or session entity) which is always equal to the result of an expression. The expression may reference other fields in the entity, but doesn't need to. Derived properties are always read-only. An example:
entity User {\n firstname: String\n lastname: String\n name : String := firstname + lastname\n}\n
"},{"location":"reference/entities/#list-of-field-properties-for-entities","title":"List of field properties for entities","text":"not null
. Makes sure a field isn't nulldefault=<value>
. Sets a default value. Note that this is different from derived propertiesallowed
. See specific docs The allowed
annotation for entity properties provides a way to restrict the choices the user has when the property is used in an input:
entity Person{\n friends : {Person} (allowed = from Person as p where p != this)\n}\nvar p1 := Person{}\npage root {\n form {\n input( p1.friends )\n submit action{ }{\"save\"}\n } \n}\n
The allowed collection can be accessed through an entity function with name allowed[PropertyName]
, e.g. p1.allowedFriends()
Entities can inherit properties and functions from other entities, like subclassing in Object-Oriented programming.
Example:
entity Sub : Super {\n str : String\n}\nentity Super {\n i : Int\n}\nfunction test(){\n var e1 := Sub{ i := 1 str := \"sdf\" };\n var e2 := Super{ i := 1 };\n}\n
Subclass entities can be passed whenever an argument of one of its super types is expected.
Example:
function test(){\n var e1 := Sub{ i := 1 str := \"sdf\" };\n test(e1);\n}\nfunction test(s:Super){\n log(s.i);\n}\n
Checking the dynamic type of an entity can be done using isa
and casting is performed using as
.
Example:
function test(s:Super){\n if(s isa Sub){\n var su :Sub := s as Sub;\n log(su.str);\n }\n}\n
When specifically want to call a function from the Superclass, use the 'super' keyword.
Example:
entity Sub : Super {\n function foo() : Int {\n return super.foo();\n }\n}\nentity Super {\n function foo() : Int {\n return 42;\n }\n}\n
"},{"location":"reference/entities/#generated-properties-for-entities","title":"Generated Properties for Entities","text":"For defined entities, a number of properties are automatically generated.
"},{"location":"reference/entities/#id","title":"ID","text":"id : UUID\n
The id property is used in the database as key for the objects. The property is can only be read.
"},{"location":"reference/entities/#version","title":"Version","text":"version : Int\n
The version property is a hibernate property which auto-increases for an object that is dirty when it is written to the database.
"},{"location":"reference/entities/#created","title":"Created","text":"created : DateTime\n
The created property is a generated property which is set on the save of an object also with cascaded saves.
"},{"location":"reference/entities/#modified","title":"Modified","text":"modified : DateTime\n
The modified property is a generated property which is automatically set on flush of an dirty object.
"},{"location":"reference/entities/#generated-functions-for-entities","title":"Generated Functions for Entities","text":"For defined entities, a number of global functions are automatically generated. Replace Entity with the defined entity name below.
"},{"location":"reference/entities/#property-with-id-annotation","title":"Property with id annotation","text":"If the Entity has an id annotation on a property, the following functions are generated (idtype is the type of the id property):
getUniqueEntity
getUniqueEntity(id : idtype) : Entity\n
If the Entity with the given id already exists, it is returned. If it did not exist, it is created once and a flush to the database is performed (this will commit any changes made to the entities in memory, e.g. the changes from data binding of input fields), repeated calls to this function with the same argument will keep returning that created Entity.
isUniqueEntity
isUniqueEntity(ent : Entity) : Bool\n
This function returns false when the value of the id property of ent is already taken. The function returns true when the id property is not taken, but will do so only once, subsequent calls with different entities but the same id will then return false (which makes this function suitable for processing a batch of entities in an action).
isUniqueEntityId
isUniqueEntityId(id : idtype, ent : Entity) : Bool\n
This function returns false when the entity would not be unique when given the id argument. The function returns true when the entity would be unique, but will do so only once for a given id, checking a different entity with the same id will return false in the rest of the action handling.
isUniqueEntityId(id : idtype) : Bool\n
This function returns false when the given id is not available for the Entity type. The function will return true only once, to cope with batch processing.
Note that these functions use one collection per entity to determine whether an id is available, so a call to isUniqueUserId(id) can influence the result of isUniqueUser(ent).
findEntity
findEntity(id : idtype) : Entity\n
This function returns the Entity with the given id value, null if it does not exist.
"},{"location":"reference/entities/#string-property","title":"String property","text":"For each String property in an Entity, a find function is generated (repace Property with the property name):
findEntityByProperty
findEntityByProperty(val : String) : [Entity]\n
This function returns a list of all Entitys with the exact given Property value, an empty list if there are none.
findEntityByPropertyLike
findEntityByPropertyLike(val : String) : [Entity]\n
This function returns a list of all Entitys with the given Property value as substring, an empty list if there are none.
"},{"location":"reference/entities/#entity-name","title":"Entity Name","text":"Every entity has a name, which is always a string. This name can be retrieved by the automatically generated getName() function.
The name of an entity is determined as follows:
If a property of the entity has the name annotation, the name of the entity equals this property. This property must be of type String.
If a property of the entity is called 'name' and is of type String, this property determines the entity name.
Otherwise, the id of the entity (converted to its string-value) is used.
A typical scenario where these functions come in handy is a create/edit page for an entity. In the following example the isUniquePage function is used to verify that the new page has a unique identifier property:
entity Page {\n identifier : String (id, validate(isUniquePage(this), \"Identifier is taken\")\n}\npage createPage { \n var p := Page{}\n form {\n label(\"Identifier\"){input(p.identifier)}\n submit save() { \"save\" }\n action save(){\n p.save();\n message(\"New page created.\");\n return home();\n }\n } \n}\n
"},{"location":"reference/entities/#derive-crud-pages","title":"Derive CRUD Pages","text":"You can quickly generate basic pages for creating, reading, updating and deleting entities using derive CRUD -entityname-
. It will create pages that allows creating and deleting such entities, and editing of all entities of this type in the database.
Example:
application test\n\nentity User {\n username : String\n}\n\nderive CRUD User\n\n//application global var\nvar u_1 := User{username:= \"test\"}\n\npage root {\n navigate(createUser()){ \"create\" } \" \"\n navigate(user(u_1)){ \"view\" } \" \"\n navigate(editUser(u_1)){ \"edit\" } \" \"\n navigate(manageUser()){ \"manage\" }\n}\n
As the navigates indicate, the pages that are created are:
view:
page entity(arg:Entity){...}\n
create:
page createEntity(){...}\n
edit:
page editEntity(arg:Entity){...}\n
manage (delete):
page manageEntity(){...}\n
These pages are particularly useful when you're just constructing the domain model, because the generated pages are usually too generic for a real application.
"},{"location":"reference/examples/","title":"Examples","text":"Below are larger examples of WebDSL applications than those found on the other manual pages.
"},{"location":"reference/examples/#minimal-access-control-example","title":"Minimal Access Control Example","text":"application minimalac\n\n entity User {\n name : String\n password : Secret\n }\n\n init{\n var u := User{ name := \"1\" password := (\"1\" as Secret).digest() };\n u.save();\n }\n\n page root(){\n authentication()\n \" \"\n navigate protectedPage() { \"go\" }\n }\n\n page protectedPage(){ \"access granted\" }\n\n principal is User with credentials name, password\n\n access control rules\n\n rule page root(){true}\n rule page protectedPage(){loggedIn()}\n
"},{"location":"reference/examples/#example-native-interface","title":"Example Native Interface","text":"A simple application that shows the public timeline of Twitter using Twitter4J. Example project code is available here
Screenshot of result: Project files: WebDSL application with native class interface declaration:
application exampleapp\n\npage root() {\n output(TwitterReader.read(null,null))\n}\n\nnative class nativejava.TwitterReader as TwitterReader {\n static read(String,String) : [String]\n}\n
Implementation of TwitterReader.java:
package nativejava;\n\nimport java.util.*;\n\nimport twitter4j.Status;\nimport twitter4j.Twitter;\nimport twitter4j.TwitterException;\nimport twitter4j.TwitterFactory;\npublic class TwitterReader{\n\n public static List<String> read(String twitterID,String twitterPassword){\n //The factory instance is re-useable and thread safe.\n Twitter twitter = new TwitterFactory().getInstance(twitterID,twitterPassword);\n List<String> result = new LinkedList<String>();\n List<Status> statuses;\n try {\n statuses = twitter.getPublicTimeline();\n for (Status status : statuses) {\n result.add(status.getUser().getName() + \":\" + status.getText());\n }\n } catch (TwitterException e) {\n e.printStackTrace();\n }\n return result;\n }\n}\n
"},{"location":"reference/examples/#ajax-example-custom-validation","title":"Ajax Example Custom Validation","text":"This example will show a complex form that uses Ajax for custom data validation.
The full project source of this example is located here:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/manual/ajax-form-validation/
"},{"location":"reference/examples/#important-note-1-inverse-annotations","title":"Important Note 1: inverse annotations","text":"Inverse annotations can cause problems due to save cascading in WebDSL, if an inverse is made with an entity in the database, then your temporary entity will be automatically saved in the database as well.
"},{"location":"reference/examples/#important-note-2-data-validation","title":"Important Note 2: data validation","text":"Don't use WebDSL's data validation described on the Validation page in combination with this example, because validation is done with custom code here. Data validation will be integrated with ajax to more easily get the result that is implemented in this example.
We're going to create an edit page for a Person
entity:
entity Person {\n fullname : String\n username : String (name)\n parents : {Person}\n children : {Person}\n}\n
The name annotation indicates that the username
is used to refer to the Person entity in select inputs, such as those for the parents
and children
property, see the Name Property page.
The root
page includes a personedit
template and passes it a new Person
object.
page root() {\n main()\n template body() {\n personedit(Person{})\n\n }\n}\n
The personedit
template provides the form that checks values whenever changes occur. The various placeholder
elements provide a location to insert error messages. The actual checks are encapsulated in functions, this allows the save action to easily do a server-side check before saving the new Person object.
template personedit(p:Person){\n form{\n par{\n label(\"username: \"){ input(p.username)[onkeyup := action{ checkUsername(p); checkUsernameEmpty(p); checkFullname(p); }] }\n placeholder pusernameempty { }\n placeholder pusername { }\n }\n par{\n label(\"fullname: \"){ input(p.fullname)[onkeyup := action{ checkFullname(p); checkFullnameEmpty(p); } ] }\n placeholder pfullnameempty { }\n placeholder pfullname { }\n }\n par{\n label(\"parents: \"){ input(p.parents)[onchange := action{ checkParents(p); } ] }\n placeholder pparents { }\n }\n par{\n label(\"children: \"){ input(p.children)[onchange := action{ checkParents(p); } ] }\n }\n submit save() [ajax] {\"save\"} //explicit ajax modifier currently necessary in non-ajax templates to enable replace. A warning is shown in the log if this is missing.\n } \n action save(){ \n // made an issue requesting & operator :)\n var checked := checkUsernameEmpty(p);\n checked := checkUsername(p) && checked;\n checked := checkFullname(p) && checked;\n checked := checkParents(p) && checked;\n checked := checkFullnameEmpty(p) && checked;\n if(checked){\n p.save();\n return root();\n } \n }\n}\n
The function definitions perform the check, and also update the placeholders (note that placeholder names are currently global in the application). They also return the result as a boolean, so the functions can be reused in the save action. replace
calls perform the insertion of templates into placeholders, in this case the templates are just creating messages.
function checkUsernameEmpty(p:Person):Bool{\n if(p.username != \"\"){ \n replace(pusernameempty, empty());\n return true;\n } \n else {\n replace(pusernameempty, mpusernameempty());\n return false; \n }\n}\nfunction checkUsername(p:Person):Bool{\n if((from Person as p1 where p1.username = ~p.username).length == 0)){ \n replace(pusername, empty());\n return true;\n } \n else {\n replace(pusername, mpusername(p.username));\n return false; \n }\n}\n\nfunction checkFullnameEmpty(p:Person):Bool{\n if(p.fullname != \"\"){ \n replace(pfullnameempty, empty());\n return true;\n } \n else {\n replace(pfullnameempty, mpfullnameempty());\n return false; \n }\n}\nfunction checkFullname(p:Person) :Bool{\n if(p.username != p.fullname) { \n replace(pfullname, empty());\n return true;\n } \n else{\n replace(pfullname, mpfullname());\n return false; \n }\n}\n
The templates for the messages are shown below. An errorclass template is used to wrap all the errors in the same div tag with special error class, to provide a hook for CSS styling. Templates that are used in replace actions have to be declared as ajax
template. When access control is enabled the ajax templates can be protected with the ajaxtemplate
rule type.
template errorclass(){\n <div class=\"error\"> elements() </div>\n}\najax template empty { \"\" }\najax template mpusername(name: String) { errorclass{ \"Username \" output(name) \" has been taken already\" } }\najax template mpusernameempty { errorclass{ \"Username may not be empty\" } }\najax template mpfullname { errorclass{ \"Username and fullname should not be the same\" } }\najax template mpfullnameempty { errorclass{ \"Fullname may not be empty\" } }\najax template mpparents(pname : String, names : [String]){ \n errorclass{ \n \"Person\" \n if(names.length > 1){\"s\"}\n \" \" \n for(name: String in names){\n output(name)\n } separated-by {\", \"}\n \" cannot be both parent and child of \" output(pname)\n }\n}\n
This app includes some CSS for top-aligned labels (http://css-tricks.com/label-placement-on-forms/), the errors are shown on new lines and in red.
label {\n clear:both;\n float:left;\n margin:10px 0 2px 0;\n}\ninput, select {\n clear:both;\n float:left;\n}\n#errorclass {\n color: red;\n clear: both;\n float: left;\n margin:2px 0 0 0;\n}\ninput[type=\"button\"]{\n clear: both;\n float: left;\n margin: 20px 0 10px 0; \n}\n
"},{"location":"reference/examples/#edit-instead-of-create","title":"Edit instead of Create","text":"Since the example used a new Person entity, the save
controls whether the object is persisted. If the entity was already in the database, and this is an edit page, then the save wouldn't be necessary to persist changes. Unfortunately this has the side-effect that all intermediate submits (on every change) already persist the changes automatically. One way to work around this issue create a copy of the entity and use that for data binding. Instead of the save()
call, the code needs to put the changes back into the real persisted entity.
The editpage, in this case a global entity var is passed in, to demonstrate changing an entity that is persisted.
page edit(){\n main()\n template body() {\n personedit(pAlice)\n }\n}\n
The template is shown below, unchanged parts are left out. A template var that copies the original values is used for data binding, in the save action the changes are placed in the real person object. The save
call is not necessary for edits, but now the template works correctly for both edit and create actions.
template personedit(realp:Person){\n var p := Person{ username := realp.username fullname := realp.fullname children := realp.children parents := realp.parents};\n form{\n par{\n label(\"username: \"){ input(p.username)[onkeyup := action{ checkUsername(p,realp); checkUsernameEmpty(p); checkFullname(p); }] }\n\n ...\n\n action save(){ \n // made an issue requesting & operator :)\n var checked := checkUsernameEmpty(p);\n checked := checkUsername(p,realp) && checked;\n checked := checkFullname(p) && checked;\n checked := checkParents(p) && checked;\n checked := checkFullnameEmpty(p) && checked;\n if(checked){\n realp.username := p.username;\n realp.fullname := p.fullname;\n realp.parents := p.parents;\n realp.children := p.children;\n realp.save(); // does nothing in the case of an update\n return root();\n } \n }\n}\n
One of the checks needs to change, because the entity might be already in the database now.
function checkUsername(p:Person, realp:Person):Bool{\n var matches := from Person as p1 where p1.username = ~p.username;\n if(matches.length == 0 || (matches.length == 1 && matches[0] == realp)){ \n replace(pusername, empty());\n return true;\n } \n else {\n replace(pusername, mpusername(p.username));\n return false; \n }\n}\n
The full project source of this example is located here:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/manual/ajax-form-validation/
"},{"location":"reference/forms/","title":"Forms","text":"The form
element enables user input, and should include submit
or submitlink
elements to handle that user input. When pressing such a submit button/link, data binding will be performed for all inputs in the form.
form {\n var name: String\n var pass: Secret\n\n label(\"Username:\"){ input(name) }\n label(\"Password:\"){ input(pass) }\n\n submit save() { \"save\" }\n}\naction save(){\n User{ \n username := name \n password := pass.digest()\n }.save();\n}\n
"},{"location":"reference/forms/#input","title":"Input","text":"input(<expression>)
creates an input form element. Can be applied directly to the properties of an entity (e.g., input(user.name)) or to page variables.
Input widgets are determined by the type of the property passed to the input template call:
For example, to get a checkbox, use:
page root {\n var x : Bool := false\n form{\n input(x)\n submit action{ log(x); } { \"log result\" }\n }\n}\n
or:
entity TestEntity {\n x : Bool\n}\ntemplate editTestEntity (e:TestEntity){\n form{\n input(e.x)\n submit action{ } { \"update entity\" }\n }\n}\n
"},{"location":"reference/forms/#page-variables","title":"Page Variables","text":"Page and template definitions can contain variables. This example displays \"Dexter\":
page cat {\n var c := Cat { name := \"Dexter\" }\n output(c.name)\n}\n\nentity Cat {\n name : String\n}\n
These variables are necessary when constructing a page that creates a new entity instance. The instance can be created in the variable and data binding can be used for the input page elements. The next example allows new cat entity instances to be created, and the default name in the input form is \"Dexter\":
page newCat {\n var c := Cat { name := \"Dexter\" }\n form{\n label(\"Cat's name:\"){ input(c.name) }\n submit action {\n c.save();\n return showCat(c);\n } { \"save\" }\n }\n}\n
It is possible to initialize such a page/template variable with arbitrary statments using an 'init' action:
page newCat {\n var c\n init {\n c := Cat{};\n c.name := \"Dexter\";\n }\n form{\n label(\"Cat's name:\"){ input(c.name) }\n submit action {\n c.save();\n return showCat(c);\n } { \"save\" }\n }\n}\n
Be aware that these type of variables (and the init blocks) are handled separately from the other elements. They do not adhere to template control flow constructs like 'if' and 'for'; they are extracted from the definition. However, you can express such functionality in the 'init' block. For example:
error:
page bad {\n if(someConditionFunction()){\n var c := Cat{}\n }\n else {\n var c := Cat{ name := \"Dexter\" }\n }\n output(c.name)\n}\n
ok:
page good {\n var c\n init{ \n if(someConditionFunction()){\n c := Cat{}\n }\n else {\n c := Cat{ name := \"Dexter\" }\n }\n }\n output(c.name)\n}\n
"},{"location":"reference/forms/#input-for-entity","title":"input for Entity","text":"input(x, y)
can be used as input for an entity variable or for a collection of entities variable, where x
is the variable or fieldaccess and y
is the collection of options. It will create a dropdown box/select or a multi-select respectively. The name
property of an entity is used to describe the entity in a select, see Name Property.
input(x)
for an entity reference property or a collection property is the same as select
, with as options all entities of its type that are in the database.
Example:
entity User {\n username : String (name)\n teammate : User\n group : {Group}\n}\nentity Group {\n groupname : String (name)\n}\ninit { // application init\n var u := User { username := \"Alice\" };\n u.save();\n u := User { username := \"Bob\"};\n u.save();\n var g := Group { groupname := \"group 1\" };\n g.save();\n g := Group { groupname := \"group 2\" };\n g.save();\n}\npage root {\n form{\n table{\n for( u: User ){\n output(u.username)\n input(u.teammate)\n input(u.group)\n }\n }\n submit action{} { \"save\" }\n }\n}\n
input(u.teammate)
is a dropdown/select with options null
, \"Alice\", \"Bob\". input(u.group)
is a multi-select with options \"group 1\" and \"group 2\".
Example 2:
page root {\n var teammates := from User\n var groups := from Group\n form{\n table{\n for(u:User){\n output(u.username)\n input(u.teammate, teammates)\n input(u.group, groups)\n }\n }\n submit action{} { \"save\" }\n }\n}\n
Equivalent to the previous example, but using explicit selects instead.
Example 3:
var u3 := User { username:=\"Dave\" }\nvar g3 := Group { groupname:=\"group 3\" }\n\npage root {\n var teammates := [u3]\n var groups := {g3}\n form{\n table{\n for(u:User){\n output(u.username)\n input(u.teammate, teammates)\n input(u.group, groups)\n }\n }\n submit action{ }{ \"save\" }\n }\n}\n
Options are restricted in this example, null
and \"Dave\" for select(u.teammate, teammates)
and only \"group 3\" for select(u.group, groups)
The null
option for a select can be removed either by a not null
annotation on the property:
teammate : User (not null)\n
Or by setting [not null]
on the input
or select
itself:
input(u.teammate)[not null]\ninput(u.teammate, teammates)[not null]\n
"},{"location":"reference/forms/#allowed","title":"Allowed","text":"The possible options can also be determined using an annotation on the property:
group : {Group} (allowed = {g3})\n
In this case just using input(u.group)
will only show \"group 3\"
Radio buttons can be used as an alternative to select
for selecting an entity from a list of entities. The name
property, or the property with name
annotation, will be used as a label for the corresponding radio button.
entity Person {\n name : String\n parent : Person\n}\n\npage editPerson(p:Person){\n radio(p.parent, getPersonList())\n}\n
"},{"location":"reference/forms/#captcha","title":"Captcha","text":"The captcha
element creates a fully automatic CAPTCHA form element.
Example:
page root {\n var i : Int\n form{\n input(i)\n captcha()\n submit action{ Registration{ number := i }.save(); } {\"save\"}\n }\n}\n
"},{"location":"reference/https-encryption/","title":"HTTPS Encryption","text":"note: this section is outdated, it is recommended to configure HTTPS on the Nginx or Apache Httpd in front of tomcat, using HSTS policy to force all traffic over HTTPS
"},{"location":"reference/https-encryption/#tomcat-configuration","title":"Tomcat Configuration","text":"Using https requires some extra configuration when deploying to an external tomcat server, the tomcat instance used in the plugin and command-line test and run commands is already configured (note: this uses a dummy configuration which should not be used in production deployment of the app). Follow these steps to configure Tomcat 6:
Run this command and follow the instructions (note down the password):
%JAVA_HOME%\\bin\\keytool -genkey -alias tomcat -keyalg RSA\n
Then, in tomcat/conf/server.xml add (use the password entered in the keytool):
<Connector port=\"8443\" protocol=\"HTTP/1.1\" SSLEnabled=\"true\"\n maxThreads=\"150\" scheme=\"https\" secure=\"true\"\n keystoreFile=\"${user.home}/.keystore\" keystorePass=\"--password--\"\n clientAuth=\"false\" sslProtocol=\"TLS\" />\n
Read more about this topic here: http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html
"},{"location":"reference/lightweight-vps/","title":"Lightweight VPS","text":"WebDSL applications can be deployed on a light-weight server or VPS. Whether performance is acceptable depends on many factors such as complexity of the application, number of users, capacity of the server. Currently, the ram usage is usually the limiting factor. The JVM halts or gets stuck when the max heap space limit is crossed (-Xmx setting). 512mb ram, typically the lowest VPS option, can run a simple WebDSL application, but getting 1gb or 2gb is recommended.
In the rest of this section is a walkthrough of the minimal steps required for installation of a WebDSL application on an Ubuntu Server (this was for a VPS with 1gb ram).
Update packages library (run all apt-get commands as root or with sudo):
apt-get update\n
Install MySQL:
apt-get install mysql-server\n
Enter a password for the mysql root account.
Install Java, Tomcat, and other requirements for running the WebDSL compiler:
apt-get install ant unzip openjdk-7-jdk tomcat7\n
Get WebDSL compiler:
wget http://hydra.nixos.org/job/webdsl/trunk/buildJavaZip/latest/download/1/webdsl-java.zip\nunzip webdsl-java.zip\nchmod +x webdsl/bin/webdsl\nexport PATH=$PATH:/[path]/webdsl/bin/\n
Add the export PATH line to your ~/.bashrc file to make the 'webdsl' command work the next time you log in as well.
Install mail SMTP server:
apt-get install postfix\n
Choose the internet configuration, test locally with 'sendmail' command. If something is wrong in the configuration, change it with:
sudo dpkg-reconfigure postfix\n/etc/init.d/postfix reload\n
Configure WebDSL application, create application.ini:
appname=myapp\nbackend=servlet\ntomcatpath=/var/lib/tomcat7/\nhttpport=8080\nhttpsport=8443\ndbmode=update\nindexdir=/var/indexes/\ndbserver=localhost\ndbname=mydb\ndbuser=myuser\ndbpassword=mypass\nsmtphost=localhost\nsmtpport=25\nsmtpprotocol=smtp\nsmtpauthenticate=false\nrootapp=true\n
If using a gmail account to send mail instead of local SMTP server, use:
smtphost=smtp.gmail.com\nsmtpport=465\nsmtpuser=blabla\nsmtppass=thepass\n
Create database and mysql user:
mysql -u root -p\ncreate database mydb;\ngrant all privileges on mydb.* to myuser@'localhost' identified by 'mypass';\nflush privileges;\nquit\n
Open up the indexes directory (can be placed anywhere):
mkdir /var/indexes\nchown -R tomcat7 /var/indexes\n
Compile application (in this application.ini myapp.app is the main file) and deploy:
webdsl build deploy\n
Check what's going on in Tomcat using:
tail -f /var/lib/tomcat7/logs/catalina.out\n
Set Tomcat's heap higher:
nano /etc/default/tomcat7\n
Change
JAVA_OPTS=\"-Djava.awt.headless=true -Xmx128m -XX:+UseConcMarkSweepGC\"\n
to
JAVA_OPTS=\"-Djava.awt.headless=true -Xmx768m -XX:+UseConcMarkSweepGC\"\n
Restart Tomcat:
/etc/init.d/tomcat7 restart\n
Check Tomcat's current JVM arguments:
ps aux | grep tomcat\n
Tomcat will run on port 8080 instead of 80, a quick fix to get it to work on port 80 is the following:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n
"},{"location":"reference/mysql/","title":"MySQL","text":"This section gives some practical tips on working with MySQL.
"},{"location":"reference/mysql/#working-with-mysql-dumps","title":"Working with MySQL dumps","text":""},{"location":"reference/mysql/#creating-a-mysql-dump","title":"Creating a MySQL dump","text":"mysqldump -u root --single-transaction dbname > mydump.sql\n
Optionally exclude less important tables:
mysqldump -u root myapplication > dump.sql --single-transaction --ignore-table=myapplication._SecurityContext --ignore-table=myapplication._RequestLogEntry\n
"},{"location":"reference/mysql/#loading-a-small-mysql-dump","title":"Loading a small Mysql dump","text":"mysql -u root dbname < mydump.sql\n
"},{"location":"reference/mysql/#loading-a-large-mysql-dump","title":"Loading a large MySQL dump","text":"When loading large MySQL dumps (for local testing), convert them to MyISAM (lack of transactions makes it unusable for production db, but it loads a lot faster due to lack of foreign key checks).
Before loading the dump, increase these settings in /etc/my.cnf and restart MySQL to load the changed settings:
key_buffer_size=1024m\nmax_allowed_packet=1024m\n\nmysqladmin -u root shutdown\nmysqld -u root &\n
Then run
cat mydump.sql | sed s/ENGINE=InnoDB/ENGINE=MyISAM/ | mysql -u root\n
Or, if you want to create an intermediate file with the MyISAM dump first (slower):
sed s/ENGINE=InnoDB/ENGINE=MyISAM/ mydump.sql > mydump.sql.myisam\nmysql -u root dbname < mydump.sql.myisam\n
"},{"location":"reference/mysql/#mysql-settings","title":"MySQL Settings","text":"Show status of InnoDB:
mysql -u root \nshow innodb status;\n
See what MySQL is doing (e.g. expensive query):
show processlist;\n
Check the current structure of a table, including foreign key constraints. This can be helpful in resolving issues caused by db mode 'update', which only adds columns but will not change an existing column:
show create table _Alias;\n
We use the following settings for MySQL on our production server (NixOS/Linux):
[mysqld]\nkey_buffer_size = 256M\nmax_allowed_packet = 64M\nsort_buffer_size = 2M\nread_buffer_size = 2M\nmyisam_sort_buffer_size = 64M\nquery_cache_size = 128M\nmax_connections = 250\n\n[mysqldump]\nmax_allowed_packet = 16M\n\n[isamchk]\nkey_buffer = 256M\nsort_buffer_size = 256M\n\n[myisamchk]\nkey_buffer = 256M\nsort_buffer_size = 256M\n
"},{"location":"reference/native-java-interface/","title":"Native Java Interface","text":""},{"location":"reference/native-java-interface/#native-classes","title":"Native Classes","text":"Native Java classes can be declared in a WebDSL application in order to interface with existing libraries and code.
The supported elements are properties, (static) methods, and constructors. The supported types are
Both the primitive type and the object types such as int and Integer can be produced by the WebDSL call (so overloading between these types is a problem here).
Add Java classes to a nativejava/ dir next to your app file and jar files in lib/.
Example:
native class nativejava.TestSub as SubClass : SuperClass {\n prop :String\n getProp():String\n setProp(String)\n constructor()\n}\n\nnative class nativejava.TestSuper as SuperClass {\n getProp():String\n static getStatic(): String\n returnList(): List<SubClass>\n}\n\npage root() {\n var d : SuperClass := SubClass() \n output(d.getProp())\n\n var s : SubClass := SubClass()\n init{\n s.setProp(\"test\");\n }\n output(s.prop)\n\n output(SuperClass.getStatic())\n\n for(a: SubClass in d.returnList()){\n output(a.prop)\n } \n}\n
(Example taken from compiler tests, source
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/native-classes.app\nhttps://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/nativejava/TestSub.java\nhttps://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed/nativejava/TestSuper.java\n
)
"},{"location":"reference/native-java-interface/#passing-a-native-java-instance-as-page-argument","title":"Passing a native java instance as page argument","text":"If you want an instance of your defined native java class to be passed as page (or ajax template) argument, the class should be serializable for WebDSL. From WebDSL version 1.3.0 and on, support is added for doing this by implementing the following 2 methods in your java class. (If you use a class defined in a library, you may need to extend this class with the following methods)
public static YourClass fromParamMap( Map<String,String> paramMap )\npublic final Map<String,String> toParamMap()\n
note: The keys in the returned Map may only consist of character classes [A-Z][a-z][0-9], values may hold any value as they get filtered. On deserialization, the static fromParamMap
method is invoked and its result is cast to the type as defined in the page/java template definition.
Examples can be found (notice the link ;)) in WebDSL's source code itself.
"},{"location":"reference/pages/","title":"Pages","text":"Pages in WebDSL can be defined using the following construct:
page [pagename]( [page-arguments]* ){ [page-elements]* }\n
There are basic output elements for structure and layout of the page, such as title
and header
.
Example:
page root {\n title { \"Page title\" }\n section {\n header{ \"Hello world.\" } \n \"Greetings to you.\"\n }\n}\n
"},{"location":"reference/pages/#page-parameters","title":"Page Parameters","text":"Pages can have parameters, and output
is used for inserting data values.
Example:
page user(u : User) {\n \"The name of this user is \" output(u.name)\n}\n
"},{"location":"reference/pages/#input-forms","title":"Input Forms","text":"The form
element in combination with submit
is used for submitting data. input
elements perform automatic data binding upon submit. For more information about forms, go to the Forms page.
Example:
page editUser(u:User){\n form{\n input(u.name)\n submit action{} { \"save\" } \n }\n}\n
"},{"location":"reference/pages/#templates","title":"Templates","text":"Pages can be made reusable by declaring them as template, and calling them from other pages or templates.
Example:
template common(){\n header{ \"my page\" }\n}\npage root(){\n common()\n}\n
"},{"location":"reference/pages/#output","title":"Output","text":"The output(<expression>)
template call is used to display a value in a page. It can also be used with Entity type expressions, and collections.
Example:
page user(u:User){\n output(u)\n}\n
The output
template can be customized for each entity type.
Example:
template output(u:User){\n \"user with name: \" output(u.name)\n}\n
"},{"location":"reference/pages/#navigate","title":"Navigate","text":"navigate <page call> { <page element*> }\n
Link to a page. For example:
page news() { \"News\" }\n
"},{"location":"reference/pages/#title","title":"Title","text":"Declares the title of the current page.
title { element* }\n
"},{"location":"reference/pages/#description","title":"Description","text":"Declares the description of a page (not visible, added as description meta tag in the head section of a page). This data is often viewed in search result snippets. Introduced in WebDSL 1.3.0.
description { element* }\n
"},{"location":"reference/pages/#section","title":"Section","text":"section { element* }\n
Indicate sections in a document; may be nested. May include a
header { element* }\n
element that indicates the section title.
"},{"location":"reference/pages/#image","title":"Image","text":"image ( <string with relative or absolute path to image> )\n
Displays an image. Images placed in an \"images\" folder in the root directory of your application will be automatically copied during deployment.
Example:
page root(){\n image(\"https://update.webdsl.org/images/WebDSL-small.png\") \n image(\"/images/WebDSL-small.png\") \n}\n
"},{"location":"reference/pages/#lists","title":"Lists","text":"Lists can be created with the list
and listitem
elements.
Example:
list {\n listitem { \"Milk\" }\n listitem { \"Potatoes\" }\n listitem { \"Cheese (lots)\" }\n}\n
"},{"location":"reference/pages/#tables","title":"Tables","text":"Tables can be created with the table
, row
, and column
elements.
Example:
table {\n row { column{ \"Username\" } column{ output(user.name) } }\n row { column{ \"Password\" } column{ \"it's a secret\" } }\n}\n
"},{"location":"reference/pages/#block","title":"Block","text":"block{ <page element*> }\nblock(String){ <page element*> }\n
Groups text; optionally defines a class for referencing in CSS. Results in a <div> element in HTML.
"},{"location":"reference/pages/#templates_1","title":"Templates","text":"Templates enable reuse of page elements. For example, a template for a footer could be:
template footer() { All your page are belong to us. }\n
This template can be included in a page with a template call:
page example(){\n footer\n}\n
Like pages, templates can be parameterized.
template edit(g:Group){\n form {\n input(g.members)\n submit action {} { \"save\" }\n } \n}\n\npage editGroup(g:Group){\n edit(g)\n}\n
"},{"location":"reference/pages/#overloading","title":"Overloading","text":"While pages must have unique names, templates can be overloaded. The overloading is resolved compile-time, based on the static types of the arguments.
template edit(g:Group){...}\ntemplate edit(u:User){...}\n\npage editGroup(g:Group){\n edit(g)\n}\n
"},{"location":"reference/pages/#dynamically-scoped-templates-redefinitions","title":"Dynamically scoped templates redefinitions","text":"Template definitions can be redefined locally in a page or template, to change their meaning in that specific context. All uses are replaced in templates called from the redefining template.
template main{\n body()\n}\ntemplate body(){\n \"default body\"\n}\npage root(){\n main\n template body(){\n \"custom body\"\n }\n}\n
"},{"location":"reference/pages/#for-loop","title":"For-loop","text":"Iterating a collection of entities or primitives can be done using a for loop. There are three types of for loops for templates:
For
for(id:t in e){ elem* }\n
This type of for loop iterates the collection produced by expression e, which must contain elements of type t. The elements in the collection are accessible through identifier id.
The collection can be filtered:
for(id:t in e filter){ elem* }\n
ForAll
This for loop iterates all the entities in the database of type t. These can also be filtered. Note that it is more efficient to retrieve the objects using a filtering query and use the regular for loop above for iteration.
for(id:t){ elem* }\n for(id:t filter){ elem* }\n
For Count
This for loop iterates the numbers from e1 to e2-1.
for(id:Int from e1 to e2){ elem* }\n
For Separator
All three template for loops can be followed by a separated-by declaration, which will separate the outputs from the for loop with the declared elem*.
separated-by{ elem* }\n
For-loop Filter
The filter part of a for loop can consist of four parts:
Where
where e1\n
e1 is a boolean expression which needs to evaluate to true for the element to be iterated.
Order By
order by e2 asc/desc\n
e2 is an expression that needs to produce a primitive type such as String or Int, which will be used to order the elements ascending or descending.
Limit
limit e3\n
e3 is an Int expression which will limit the number of elements that get iterated.
Offset
offset e4\n
e4 is an Int expression which will offset the starting element of the iteration.
Each of the four parts is optional, but they have to be specified in this order. The filtering is done in the application, so use queries instead of filters to optimize the WebDSL application.
"},{"location":"reference/pages/#xml-embedding","title":"XML Embedding","text":"XML fragments can be embedded directly in templates. This allows easy reuse of existing XHTML fragments and CSS. For example:
template main() {\n <div id=\"pagewrapper\">\n <div id=\"header\">\n header()\n </div>\n <div id=\"footer\">\n <p />\"powered by \" <a href=\"https://webdsl.org\">\"WebDSL\"</a><p />\n </div>\n </div>\n <some:tag />\n}\n
While the name and attribute names are fixed, the attribute values can be any WebDSL expression that produces a string:
template test(i : Int) {\n <div id=\"page\" + \"wrapper\" + i />\n}\n
"},{"location":"reference/pages/#include-css","title":"Include CSS","text":"The includeCSS(String)
template call allows you to include a CSS file in the resulting page. CSS files can be included in your project by placing them in a stylesheets/ directory in the project root.
Example 1:
page root() {\n includeCSS(\"dropdownmenu.css\")\n}\n
It is also possible to include a CSS file using an absolute URL.
Example 2:
page root() {\n includeCSS(\"https://webdsl.org/stylesheets/common_.css\")\n}\n
The media attribute can be set by passing it as second argument in includeCSS(String,String)
Example 3:
page root(){\n includeCSS(\"https://webdsl.org/stylesheets/common_.css\",\"screen\")\n}\n
"},{"location":"reference/pages/#include-javascipt","title":"Include Javascipt","text":"The includeJS(String)
template call allows you to include a javascript file in the resulting page. Javascript files can be included in your project by placing them in a javascript/ directory in the project root.
Example 1:
page root() {\n includeJS(\"sdmenu.js\")\n}\n
It is also possible to include a Javascript file using an absolute URL.
Example 2:
page root() {\n includeJS(\"http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js\")\n}\n
"},{"location":"reference/pages/#page-not-found-error","title":"Page Not Found Error","text":"When an invalid URL is being requested from a WebDSL application, the default response is to give a 404 error. To customize this error page, define a pagenotfound
page in your application.
Example:
page pagenotfound() {\n title{ \"myapp / page not found (404)\" }\n main()\n template body() {\n par{ \"That page does not exist!\" }\n par{ \"Maybe you can find what you are looking for using the search page.\" }\n }\n}\n
"},{"location":"reference/pages/#raw-output","title":"Raw output","text":"By default, any template content will be escaped, if you want to include a string directly in the page source use rawoutput.
Example:
rawoutput{\" \"}\n
"},{"location":"reference/pages/#html-element-attributes-on-template-call","title":"HTML Element Attributes on Template Call","text":"Setting HTML element attributes is supported for calls to built-in templates, the syntax is as follows:
templatename(...)[attrname=e, ...]{ ... }\n
attrname
is an attribute name, and e
is a webdsl expression such as \"foo\"
or \"foo\"+bar
.
Example:
page root(){ \n var somevalue := \"lo\"\n image(\"/images/logosmall.png\")[alt = somevalue+\"go\", longdesc = \"blablabla\"]\n navigate root()[title = \"root page\"]{ \"root\" }\n}\n
"},{"location":"reference/pages/#override-modifier","title":"Override Modifier","text":"Template and page definitions can be overridden using the override
modifier, e.g. to override a built-in page such as pagenotfound
:
override page pagenotfound(){\n \"page does not exist!\"\n}\n
"},{"location":"reference/pages/#sql-logging-for-page-rendering","title":"SQL Logging for Page Rendering","text":"add ?logsql
after the URL of a page to get a log of all the SQL queries executed to render that page
for applications with access control enabled, accessing this log is disabled by default, it can be enabled using an access control rule:
rule logsql { check }\n
e.g.
rule logsql { principal.isAdmin }\n
"},{"location":"reference/production-server/","title":"Production Server","text":""},{"location":"reference/production-server/#tomcat","title":"Tomcat","text":"We use the following settings for Tomcat on our production server (NixOS/Linux):
-Xms350m \n-Xss8m \n-Xmx8G \n-Djava.security.egd=file:/dev/./urandom \n-XX:MaxPermSize=512M \n-XX:PermSize=512M \n-XX:-UseGCOverheadLimit \n-XX:+UseCompressedOops\n-XX:+HeapDumpOnOutOfMemoryError \n-XX:HeapDumpPath=/var/tomcat/logs/heapdump.hprof\n
"},{"location":"reference/production-server/#details","title":"Details","text":"-Xmx8G\n
The most important setting, maximum heap space, value depends on size/number of applications, but the default setting is usually too low. If this setting is too high for your JVM, it won't start at all.
-XX:MaxPermSize=512M\n
This allows redeploying the application without running into permgenspace errors too quickly.
-Djava.security.egd=file:/dev/./urandom\n
The default implementation for random can be too slow (java.util.UUID.randomUUID is used for entity identifiers, including RequestLogEntry) see http://stackoverflow.com/questions/137212/how-to-solve-performance-problem-with-java-securerandom
"},{"location":"reference/production-server/#monitoring-and-debugging","title":"Monitoring and Debugging","text":"Use jvisualvm
to inspect the Tomcat process, this allows you to look at the heap and running threads and create dumps for later inspection. A heap dump can also be created using:
jmap -F -dump:format=b,file=<filename> <process id>\n
The Eclipse Memory Analyzer can be used to inspect this file, get it from http://www.eclipse.org/mat/
If the tomcat process becomes unresponsive try
kill -3 <process id>\n
to generate a thread dump in the catalina.out log.
"},{"location":"reference/production-server/#mysql","title":"MySQL","text":"See the MySQL page.
"},{"location":"reference/production-server/#lightweight-vps","title":"Lightweight VPS","text":"See the Lightweight VPS page, it also contains a step-by-step installation guide.
"},{"location":"reference/queries/","title":"Queries","text":"WebDSL supports a subset of HQL. You can use these queries directly in your webdsl program, by for example assigning them to variables. An example:
// select all updates\n var allUpdates : [Update] := from Update;\n
"},{"location":"reference/queries/#escaping","title":"Escaping","text":"Often it is useful to use values from your WebDSL program inside a query. To do this you can escape WebDSL expressions by prefixing them with a tilde (~
).
To escape to WebDSL inside a query, prefix the expression with ~. For example:
// Select all users whose username is \"zef\"\n var username : String := \"zef\";\n var users : [User] := from User as u where u.username = ~username;\n
"},{"location":"reference/queries/#notes","title":"Notes","text":"offset
. Even though offset
is valid SQL, it's not a valid keyword to use in WebDSL queries, even though it is a valid keyword in other places in WebDSL so will be highlighted in your editor.// A paginated for, showing 10 items per page and prefetch the \"user\" column (in a template)\nvar page : Int := 0;\nfor(update : Update in from Update as u left join fetch u.user order by u.date desc limit 10*page, 10) {\n output(update.text)\n}\n
"},{"location":"reference/recurring-tasks/","title":"Recurring Tasks","text":"Recurring task allow you to execute a certain function in set interval, e.g. every minute, 5 hours or every week. For this WebDSL uses the following syntax (which is subject to change):
function someFunction() {\n log(\"I was executed!\");\n}\n\ninvoke someFunction() every 5 minutes\n
If the called function returns anything, this value is discarded. Functions invoked in this manner have access to entities and global variables, but not session data (because the function is not invoked by a user). Syntax of the time intervals:
TimeInterval = TimeIntervalPart*\nTimeIntervalPart = Exp \"weeks\"\nTimeIntervalPart = Exp \"days\"\nTimeIntervalPart = Exp \"hours\"\nTimeIntervalPart = Exp \"minutes\"\nTimeIntervalPart = Exp \"seconds\"\nTimeIntervalPart = Exp \"milliseconds\"\n
So valid time intervals are:
1 hours // note the plural\n1 hours 10 minutes // repeat every 70 minutes\n2 weeks 10 milliseconds\n
"},{"location":"reference/request-processing/","title":"Request Processing","text":"Users interact with web applications through the browser. This process consists of request and response strings being exchanged between the web server and the browser. A form is defined by a response string, which is interpreted by the browser to produce components that allow user interaction. A user can fill in data in a text field, and press the submit button. The browser first collects the data from the form input fields, and constructs a request string to send to the web server, which receives the request string and parses it. Values from input fields can be accessed separately but are represented as strings. A web application bears the responsibility of converting these strings to actual types to be used in further processing of the request. In WebDSL, the conversion of request parameters is done automatically. This is the first phase of the request processing lifecycle. The request processing lifecycle consists of the following phases:
Request parameter conversion is not possible if the incoming value is not well-formed. For example, a value of \"3f\" cannot be converted to an integer. Since a failed conversion invalidates any input this triggers re-rendering the page with error messages.
In the first phase, parameters are decoded from strings. In the 'Update Model Values' phase, these parameters are automatically inserted in data model entities. WebDSL supports such data binding through input elements. For example, the element
input(u.email)\n
declares that an input field should be displayed with the current contents of the email property of variable u of type User. Furthermore, when a user submits the containing form with a new value in the email field, the new value will be assigned to u.email.
Data binding requires assignments to and collection operations on entity properties which trigger validation checks defined in the entity. When a property is validated each validation rule defined on that property is checked, possibly producing multiple error messages. When at least one validation fails during this phase, further processing is disabled and errors are displayed.
When the model is updated and entity validations are checked, there can still be validation rules in pages which need to be enforced. The form validation phase traverses the form that is submitted and checks any validation it encounters. An invalid result prevents any action from executing and produces an error in the page.
When all validation checks in previous phases have succeeded, the selected action is executed. During the execution of an action there can be action assertions that validate the data in the current execution state of the action. Moreover, data invariants are still checked during this phase and can produce validation errors as well. If any validation check fails, the entire action is cancelled (clearing all changes made during that request).
Validation messages produced in the previous phases result in a re-render of the same page with error messages inserted. If all validations succeed, the action results in a redirect to the same or a different page.
Notes:
WebDSL supports simple search capabilities through Lucene. Entity properties can be marked as \"searchable\" to subsequently be indexed:
entity Message {\n subject : String (searchable)\n text : Text (searchable)\n sender : ForumUser\n}\n
The searchable can be applied to the following built-in WebDSL types: String, Text, WikiText, Int, Long, Float and date types. Properties of user defined entity types are currently not supported to be searchable (i.e. sender in the previous example). If one or more properties of an entity are marked as searchable, a set of searchEntity
functions are generated, in this case:
function searchMessage(query : String) : [Message]\nfunction searchMessage(query : String, limit : Int) : [Message]\nfunction searchMessage(query : String, limit : Int, offset : Int) : [Message]\n
Which can be used from anywhere. For instance on a search page: page search(query : String) {\n var newQuery : String := query;\n action doSearch() {\n return search(newQuery);\n }\n\n title { \"Search\" }\n form {\n input(newQuery)\n submit(\"Search\", doSearch())\n }\n for(m : Message in searchMessage(query, 50)) {\n output(m)\n }\n}\n
The query syntax adheres to Lucene's query syntax as does the scoring.
All data to be indexed (properties marked as \"searchable\") and queries are analyzed using the default analyzer of Lucene. This means that punctuation and stop words (commonly used words like 'the', 'to', 'be') are stripped from text and text is transformed to tokens in lowercase.
"},{"location":"reference/search/#index-location","title":"Index location","text":"WebDSL stores its search index in the /var/indexes/APPNAME
directory. This is currently not configurable. Make sure this directory is readable and writeable for the user that runs tomcat.
When the searchable
annotations or search mappings are added/changed when data is already in the database, the search index has to be recreated. When your application has been deployed , go to the directory in which it was deployed, for instance /var/tomcat/webapps/yourapp. In this directory you will find a webdsl-reindex
script (for *nix only) which will invoke ant reindex and fixes permissions of the index directory. By default, the reindex task completely reindexes all searchable entities. As of WebDSL version 1.2.9 it also accepts entity names as command line argument (separated by whitespace) to reindex a subset of entities.
Reindex all entities *nix:
# sudo sh webdsl-reindex\n
Windows as of 1.3.0:
# ant reindex\n
before 1.3.0:
# ant reindex -f build.reindex.xml\n
Reindex a subset of entities *nix:
# sudo sh webdsl-reindex Entity1 Entity2 Etc\n
Windows: as of 1.3.0:
# ant reindex -Dentities=\"Entity1 Entity2 Etc\"\n
before 1.3.0:
# ant reindex -f build.reindex.xml -Dentities=\"Entity1 Entity2 Etc\"\n
Note: Don't start your application during reindexing, it will crash because it can't initialize the directory provider. So reindexing should be done before starting or when already running your application.
A demo of the search functionality can be seen on Reposearch.
"},{"location":"reference/send-email/","title":"Send Email","text":"This page describes how to create an email template and send email from your application. Make sure the email settings are configured in application.ini, see Application Configuration. If you are interested in storing email addresses in an entity, have a look at the Email type.
Defining an email template:
email testemail(us : User) {\n to(us.mail)\n from(\"webdslorg@gmail.com\")\n subject(\"Test Email\")\n\n par{ \"Dear \" output(us.name) \", \" }\n par{\n \"Look at your profile page: \"\n navigate(user(us)){\"go\"}\n //navigate will become an absolute link in the email\n }\n}\n
Sending email:
email testemail(someuser);\n
The actual sending happens asynchronously, if there are issues while the application is trying to send an email, it will retry that email after 3 hours. If necessary, you can inspect and influence this email queue through the QueuedEmail
entity:
entity QueuedEmail {\n body : String (length=1000000) \n //Note: default length for string is currently 255\n to : String (length=1000000)\n cc : String (length=1000000)\n bcc : String (length=1000000)\n replyTo : String (length=1000000)\n from : String (length=1000000)\n subject : String (length=1000000)\n lastTry : DateTime \n}\n
"},{"location":"reference/services/","title":"Services","text":"WebDSL includes a simple way to define string and JSON-based webservices.
"},{"location":"reference/services/#json-api","title":"JSON API","text":"The JSON interface is defined as follows:
native class org.json.JSONObject as JSONObject {\n constructor()\n constructor(String)\n get(String) : Object\n getBoolean(String) : Bool\n getDouble(String) : Double\n getInt(String) : Int\n getJSONArray(String) : JSONArray\n getJSONObject(String) : JSONObject\n getString(String) : String\n has(String) : Bool\n names() : JSONArray\n put(String, Object)\n toString() : String\n toString(Int) : String\n }\n\n native class org.json.JSONArray as JSONArray {\n constructor()\n constructor(String)\n get(Int) : Object\n getBoolean(Int) : Bool\n getDouble(Int) : Double\n getInt(Int) : Int\n getJSONArray(Int) : JSONArray\n getJSONObject(Int) : JSONObject\n getString(Int) : String\n length() : Int\n join(String) : String\n put(Object)\n remove(Int)\n toString() : String\n toString(Int) : String\n } \n
Example use in WebDSL:
function myJsonFun() : String {\n var obj := JSONObject(\"{}\");\n obj.put(\"name\", \"Pete\");\n obj.put(\"age\", 27);\n return obj.toString();\n // Will return '{\"name\": \"Pete\", \"age\": 27}'\n}\n
"},{"location":"reference/services/#defining-services","title":"Defining services","text":"A service is simply a WebDSL function that uses the service
keyword instead of function
, you don't have to specify a return type, it will convert anything you return to a string (using .toString()
):
entity Document {\n title : String (id, name)\n text : Text\n}\n\nservice document(id : String) {\n if(getHttpMethod() == \"GET\") {\n var doc := findDocument(id);\n var json := JSONObject();\n json.put(\"title\", doc.title);\n json.put(\"text\", doc.text);\n return json;\n }\n if(getHttpMethod() == \"PUT\") {\n var doc := getUniqueDocument(id);\n var json := JSONObject(readRequestBody());\n doc.text := json.getString(\"text\");\n return doc.title;\n }\n}\n
services are mapped to /serviceName
, e.g. /document
. Here's a few sample requests to test (note, these are services part of an application called \"hellojson\"):
$ curl -X PUT 'http://localhost:8080/hellojson/document/my-first-doc' \\\n -d '{\"text\": \"This is my first document\"}'\nmy-first-doc\n$ curl http://localhost:8080/hellojson/document/my-first-doc\n{\"text\":\"\"This is my first document\",\"title\":\"my-first-doc\"}\n
But, like pages, services can also have entities as arguments:
service documentJson(doc : Document) {\n var obj := JSONObject();\n obj.put(\"title\", doc.title);\n obj.put(\"text\", doc.text);\n return obj;\n}\n
"},{"location":"reference/session-entities/","title":"Session Entities","text":"Storing data in the session context on the server is done using session entities. Example:
session shoppingcart {\n products : [Product]\n}\n
A session entity name is a globally visible variable in the application code. The entity object is automatically instantiated and saved, one for each browser session accessing the application.
Typically, session data is used for keeping track of authentication state, but it can also be used for temporarily storing data for anonymous users. A common oversight with session data is that it is shared between tabs in a browser.
To initialize session data to default values, remember that using :=
after a field means that that field is a derived property. That also means you can't assign to this field. To set defaults use (default = value)
. Example:
session history {\n number_of_entries : Int (default = 10)\n}\n
Declaring an access control principle, e.g. principal is User with credentials name,password
, automatically creates a securityContext
session entity. For more information about access control see the Access Control page.
Session entities can also be extended with extra properties. Example:
extend session shoppingcart{\n lastSearchQuery : String\n}\n
Session data times out by default, this timeout length can be adjusted in the application.ini
file, e.g. sessiontimeout=10080
. This time is specified in minutes. More information about application settings is shown on the Application Configuration page.
Styling of WebDSL pages is done using CSS. In the application directory add the following directory and file:
stylesheets/common_.css\n
This CSS file will be automatically included when deploying the application. Other CSS files can be included using includeCSS
(in this example the included file is located at stylesheets/jquery-ui.css):
includeCSS(\"jquery-ui.css\")\n
When the application is deployed in the Eclipse plugin you can edit the CSS file directly in the tomcat directory (don't forget to also save the CSS file back to your project):
WebContent/stylesheets/common_.css\n
For deployment to external tomcat this directory is:
tomcat/webapps/appname/stylesheets/common_.css\n
The Firebug add-on for Firefox can be very helpful in figuring out the page structure, other browsers have similar development tools.
Explicit hooks for CSS can be added using the XML embedding:
template someTemplate(){ \n <div class=\"mydiv\">\n \"content of mydiv\"\n </div>\n}\n
Note that the \"mydiv\" is a WebDSL expression, so this could also be stored in an entity and retrieved using a field access:
<div class=user.cssclass>\n
Classes for styling can also be added to a template call (separate from the regular arguments):
input[class=\"mynameinput\"](u.name)\n
If you want to define your own template that takes such extra arguments, use all attributes
:
page root(){\n someOtherTemplate[class=\"importantdiv\"]{ \"content\" }\n}\n\ntemplate someOtherTemplate(){ \n <div all attributes>\n elements\n </div>\n}\n
The span
template modifier adds a span around a template, which can then be used as a hook for CSS:
template span spanTemplate(){ \"span around me\" }\n
"},{"location":"reference/synchronization-framework/","title":"Synchronization Framework","text":"WebDSL provides possibility to generate code for a synchronization framework combined with mobl. Nevertheless, the webservices are open to be used by other applications.
"},{"location":"reference/synchronization-framework/#required-steps","title":"Required steps","text":"The steps that are required for the synchronization framework are the following:
The framework is meant as an extension to a WebDSL application. This means that it requires at least a full model in the application to generate a working framework.
"},{"location":"reference/synchronization-framework/#additional-settings","title":"Additional Settings","text":"The synchronization framework requires and allows some adaption by additional settings. Those settings can be added in the synchronization settings for each entity.
entity Example{\n synchronization configuration{\n } \n}\n
The following settings can be configured:
The data synchronization requires a Toplevel Entity to enable the data partitioning. This is simple a flag that specifies that objects of this type represent a data partition. Additionally, this setting requires a String property that can be used to represent this object.
entity Car{\n registrationIdentifier : String\n\n synchronization configuration{\n toplevel name property : registrationIdentifier\n } \n}\n
"},{"location":"reference/synchronization-framework/#access-control","title":"Access Control","text":"The data synchronization framework enable external sources to read and modify data on the server with the web application. The framework allows control over which data can be accessed by who. This can only be specified when the a principal is defined in the web application. There are three different levels that can be specified for each entity: read, write and create. It is recommended to specify those rules for each entity.
entity Dummy{\n name : String\n\n synchronization configuration{\n access read: true\n access write: Logedin()\n access create: principal().isAdmin()\n } \n}\n
"},{"location":"reference/synchronization-framework/#restricted-properties","title":"Restricted Properties","text":"The last setting that can be configured is that of restricted properties. It allows to simplify the data model that you want to use on synchronization. The properties that are specified in this configuration are removed from the shared data and also for the calculation of data partitioning.
entity Person{\n surName : String\n firstName : String\n fullName : String\n\n synchronization configuration{\n restricted properties : surName, firstName\n } \n}\n
"},{"location":"reference/synchronization-framework/#generation-of-the-framework","title":"Generation of the framework","text":"Generation of the framework is easy. After specifying the settings, open the main application file in the IDE. Then select the generate synchronization framework from the Transform menu.
"},{"location":"reference/synchronization-framework/#importing-of-the-framework","title":"Importing of the framework","text":"The framework is generated in the folder webservices. To enable the synchronization framework inside the web application you need to include the main file of the framework.
application TestApp\nimports webservices/services/interface\n
"},{"location":"reference/synchronization-framework/#mobl-or-other-remote-application","title":"mobl or other remote application","text":"The framework generates code for mobl that enable synchronization in a mobl application. However, it still needs a full mobile application.
Other applications can use the available webservices to synchronize with the application.
"},{"location":"reference/synchronization-framework/#what-is-generated","title":"What is generated","text":"The framework generates a lot of files, but what does it contain:
The core of the synchronization contains functions that overlook the main functionality of the synchronization. Identification, detection and resolution of updates.
"},{"location":"reference/synchronization-framework/#webservices","title":"Webservices","text":"The webservices are used for communication with mobl of other applications. This is a layer on the core of the synchronization. All services are called by post request to the url:
http://<websiteurl>/webservice/<webservicename>\n
The following services are available and should be used in that order:
The mappers are meant for mapping the updates to local values. The also have some additional statements for checking validity of the input. There are two mappers, one for modification and one for creation. Currently, they contain the same code. This is done so they can be overwritten separately.
The values in the database are not in a format that can be send through webservices. Therefor, the framework has 3 functions for each entity
The synchronization framework uses data partitioning to reduce the amount of data for mobile applications. This solution chooses to use object relations to determine if objects are linked to the TopLevel entity. This requires that each entity has a function to calculate the related objects.
The main function of data partitioning gets a closure of a data partitioning by calling the related functions until there are no new objects any more.
"},{"location":"reference/synchronization-framework/#access-control_1","title":"Access Control","text":"As mentioned before you can specify three rules for the access control of objects. Those rules are turned into functions named:
The access control requires that remote applications can login to the application. To improve security a device can register itself and get a devicekey. Which then can be used to authenticate instead of using the password. Those keys are stored as an additional property of the principal and if removed the device is de-authenticated.
"},{"location":"reference/synchronization-framework/#model","title":"Model","text":"A big part of the data synchronization is about the model. The model is basically a copy of that of WebDSL only with other mobl types. It should also be used for developers that try to understand what model is expected for the webservices. The following sections are additional notes to the creation of the model.
"},{"location":"reference/synchronization-framework/#restricted-types","title":"Restricted types","text":"Mobl has a more restricted set of types. This let to the choice to not support all types. properties with the following types are removed:
Mobl does not support class Hierarchy. To support all entities from the application the synchronization framework has flatten the hierarchy. The influence can be found in the renamed properties that now have a prefix of there original class name. And a additional property that tells the actual type: Typefield
"},{"location":"reference/synchronization-framework/#toplevel-properties","title":"TopLevel properties","text":"The data partitioning requires some additional information. This is stored in the property sync and lastSynced.
"},{"location":"reference/synchronization-framework/#search-annotations","title":"Search annotations","text":"The search annotations in mobl are expensive and better can be removed from the model.
"},{"location":"reference/synchronization-framework/#mappers","title":"Mappers","text":"Mobl also needs some mappers of the values. However the limited difference between mobile and JSON representation, allows it to use the function generated by the mobl compiler.
"},{"location":"reference/synchronization-framework/#integration-functions","title":"Integration functions","text":"There are some integration functions for mobl that can be used to call synchronization processes. It can be seen as the core of the synchronization for mobl applications.
"},{"location":"reference/synchronization-framework/#authentication_1","title":"Authentication","text":"The authentication are some functions to enable the devicekey setup. It has the following functions:
The logging out of the device also cleans the database for security reasons.
"},{"location":"reference/synchronization-framework/#data-browser","title":"Data Browser","text":"As a start the generated framework has a data browser included to have easy start with the application.
It has a page for every entity, namely: showSimple
Those pages allow to click through the data stored locally.
"},{"location":"reference/synchronization-framework/#additional-notes","title":"Additional notes","text":""},{"location":"reference/synchronization-framework/#version-number","title":"version number","text":"The send version numbers of each objects can be used to change the protocol of resolution of outdated objects. Giving it an high number will interpret that the object is newer than that of the system.
"},{"location":"reference/synchronization-framework/#collections","title":"Collections","text":"mobl doesn't have difference between set and lists, it only supports collections. The biggest problem is that the ordering can not be trust.
"},{"location":"reference/testing/","title":"Testing","text":"Tests for the entities and functions operating on those entities can be defined in test blocks.
Example:
test capitalizeTest {\n var u := User{ name := \"alice\" };\n u.capitalizeName();\n assert(u.name == \"Alice\");\n}\nentity User {\n name : String\n function capitalizeName(){\n var temp := name.explodeString();\n temp.set(0,temp.get(0).toUpperCase());\n name := temp.concat();\n }\n}\n
The 'webdsl test appname' command builds the app and runs the tests, an error will be returned when any of the tests fail. This command will create a new application.ini and uses an in-memory H2 Database Engine.
webdsl test myapp\n
If the settings in the existing application.ini can be used for testing, run the 'webdsl check' command instead.
webdsl check\n
Multiple test blocks can be defined in an application, each test will run with a fresh initialization of the database, the global variables and global init blocks are handled before each test.
Use assert calls to verify the correctness of results. When the assert argument evaluates to false, the test run will show the location of the failing assert. If the assert consists of one == or != comparison, the two compared results will also be printed upon failure. The assert function has an optional second argument, a String which can be used to pass a message that will be shown when the assert fails.
Signatures:
assert(Bool)
assert(Bool, String)
Testing example: tests for built-in Text type: text.app.
"},{"location":"reference/testing/#web-testing","title":"Web testing","text":"The resulting web application can also be automatically tested by using
webdsl test-web myapp\n
or
webdsl check-web\n
where 'test-web' will automatically create an application.ini and 'check-web' will use an existing one.
These commands start up tomcat and run the tests. Tests are currently expressed by calls to WebDriver with HTMLUnit behind it (a better abstraction specific to WebDSL tests is planned, current web testing is mainly to support testing the compiler). The interface is described here (see Native Java Interface):
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/src/org/webdsl/dsl/languages/test/native-classes.str\n
Example:
test datavalidation {\n var d : WebDriver := HtmlUnitDriver();\n\n d.get(navigate(root()));\n assert(!d.getPageSource().contains(\"404\"), \"root page may not produce a 404 error\");\n\n var elist : [WebElement] := d.findElements(SelectBy.tagName(\"input\"));\n assert(elist.length == 4, \"expected 4 <input> elements\");\n\n elist[1].sendKeys(\"123\");\n elist[2].sendKeys(\"111\");\n\n elist[3].click();\n\n var pagesource := d.getPageSource();\n\n var list := pagesource.split(\"<hr/>\");\n\n assert(list.length == 3, \"expected two occurences of \\\"<hr/>\\\"\");\n\n assert(list[1].contains(\"inputcheck\"), \"cannot find inputcheck message\");\n assert(list[2].contains(\"formcheck\"), \"cannot find formcheck message\");\n
}
Source:
https://svn.strategoxt.org/repos/WebDSL/webdsls/trunk/test/succeed-web/data-validation/validate-in-elements.app\n
"},{"location":"reference/types/","title":"Types","text":"This section lists all the built-in types available in WebDSL.
There are multiple types that are equivalent to String. These types can have different validation rules, functions, inputs, and outputs. Converting between these types can be done with casts, e.g.
var : Secret := url(\"123\") as Secret;\n
The String compatible types are:
Similarly, the Date times are equivalent as well:
Enumeration types consist of a list of options that contain both an identifier and a visible name. You define them as follows:
enum Gender {\n maleGender(\"Male\"),\n femaleGender(\"Female\"),\n otherGender(\"Other\"),\n}\n
You can use them as follows: entity User {\n gender : Gender\n}\n\npage somePage() {\n var u : User;\n input(u.gender) // shows a drop-down\n output(u.gender.name) // shows either Male or Female\n}\n
Or, in action code: function setMale(u : User) {\n u.gender := maleGender;\n}\n
Note that internally an enumeration becomes an entity declaration with a name property for the visible name. The options become global variable declarations that get initialized once. Visible names can be updated using regular WebDSL forms.
"},{"location":"reference/types/#string","title":"String","text":"Represents a string of characters. Example:
var s : String := \"Hello world\";\n
The default value for String properties and variables is \"\".
"},{"location":"reference/types/#functions","title":"Functions","text":"contains(s: String):Bool
Tests whether s is a substring of this string.
length():Int
Returns the length of this string.
parseInt():Int
Returns the Int value in this string. If this string does not contain a valid Int value, this function returns null.
parseUUID():UUID
Returns the UUID value in this string. If this string does not contain a valid UUID value, this function returns null.
toUpperCase():String
Returns this string in uppercase.
toLowerCase():String
Returns this string in lowercase.
split():[String]
Returns the characters in this string as separate strings in a list.
split(separator:String):[String]
Returns a list of strings produced by splitting this string around matches of separator.
makePatch(new : String):Patch
Creates a Patch from this String to the new String, see Patch.
diff(new : String):[String]
Creates a list of String
s describing the differences between this String and the new String.
trim(): String
Returns this string, with leading and trailing whitespace omitted.
"},{"location":"reference/types/#list-functions","title":"List Functions","text":"concat():String
Concatenates the strings in this list of strings.
concat(separator:String):String
Concatenates the strings in this list of strings, separated by separator.
"},{"location":"reference/types/#int","title":"Int","text":"Represents an integer number. Example:
var i : Int := 3;\n
The default value for Int properties and variables is 0.
"},{"location":"reference/types/#functions_1","title":"Functions","text":"floatValue():Float
Converts this value to a Float.
toString():String
Converts this value to a String.
"},{"location":"reference/types/#float","title":"Float","text":"Represents a floating point number. Example:
var f : Float := 3.5;\n
The default value for Float properties and variables is 0f.
"},{"location":"reference/types/#functions_2","title":"Functions","text":"round():Int
Rounds this value to the nearest Int value.
floor():Int
Returns the largest Int that is less than or equal to this value.
ceil():Int
Returns the smallest Int that is greater than or equal to this value.
toString():String
Converts this value to a String.
"},{"location":"reference/types/#static-functions","title":"Static Functions","text":"random():Float
Produces a random Float between 0 and 1.
"},{"location":"reference/types/#bool","title":"Bool","text":"Represents a truth value. Either true
or false
. Example:
var b : Bool := true;\n
The default value for Bool properties and variables is false.
"},{"location":"reference/types/#functions_3","title":"Functions","text":"toString():String
Converts this value to a String.
"},{"location":"reference/types/#list","title":"List","text":"Represents an ordered list of items of a certain type. Example:
var l : [Int] := [1, 2, 3, 4];\n
Sorted output of lists can be created using the for
loop filter in templates or actions:
for(u:User in [u1,u2,u3] order by u.name desc){\n output(u)\n}\n
"},{"location":"reference/types/#fields","title":"Fields","text":"length
Gives the number of items in the list.
"},{"location":"reference/types/#list-creation-expressions","title":"List Creation Expressions","text":"List()
Creates an empty list of type Entity
.
List(..., Entity, ...)
Creates a list of type Entity
with the elements resulting from the comma separated argument expressions.
Example:
var list := List<User>(User{},SubSubUser{},SubUser{})\n
[Entity, ...]
Creates a list of type Entity
(type of first element) with the elements resulting from the comma separated expressions between the [ ].
Example:
var list := [User{ name := \"test\" },SubUser{},uservar]\n
"},{"location":"reference/types/#functions_4","title":"Functions","text":"add(Entity)
Adds the entity to this list.
remove(Entity)
Removes the first occurence of Entity in this list.
clear()
Removes all entities in this list.
addAll(List/Set)
Adds all entities of the List/Set to this list.
set() : {Entity}
Creates a Set containing the unique elements in this list.
indexOf(Entity) : Int
Returns the index of the first occurence of Entity in this list. Returns -1 if the Entity is not in this list.
get(Int)
Returns the element at location Int in this list.
set(Int,Entity)
Sets the list element at Int to Entity. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
insert(Int,Entity)
Inserts the Entity at location Int in this list. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
removeAt(Int)
Removes the element at location Int in this list. If the Int is not within bounds, nothing is set, and a warning is shown in the log.
subList(from:Int,to:Int):[Entity]
Returns a portion of this list between the specified from, inclusive, and to, exclusive.
"},{"location":"reference/types/#set","title":"Set","text":"Represents an unordered collection of unique items of a certain type. Example
var s : Set<Int> := {1, 2, 3, 4};\n
Sorted output of sets can be created using the for
loop filter in templates or actions.
for(u:User in [u1,u2,u3] order by u.name desc){\n output(u)\n}\n
"},{"location":"reference/types/#fields_1","title":"Fields","text":"length
Gives the number of items in the set.
"},{"location":"reference/types/#set-creation-expressions","title":"Set Creation Expressions","text":"Set()
Creates an empty set of type Entity.
Set(..., Entity, ...)
Creates a set of type Entity with the elements resulting from the comma separated argument expressions.
Example:
var set := Set<Person>(Person{},SubSubPerson{},SubPerson{})\n
{Entity, ...}
Creates a set of type Entity (type of first element) with the elements resulting from the comma separated expressions.
Example:
var set : {Person} := {Person{ name := \"test\" },personvar}\n
"},{"location":"reference/types/#functions_5","title":"Functions","text":"add(Entity)
Adds the entity to this set.
remove(Entity)
Removes Entity in this set.
clear()
Removes all entities in this set.
addAll(List/Set)
Adds all entities of the List/Set to this set.
list() : [Entity]
Creates a List containing the elements in this set.
"},{"location":"reference/types/#secret","title":"Secret","text":"Represents a secret string (usually a password). The page input for a Secret is a masked textfield. The page output for a Secret is \"********\".
Example:
var pass : Secret := \"123\";\n
The default value for Secret properties and variables is \"\".
The Secret type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Secret typed vars. A Secret can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(pass:Secret){\n test1(pass as String); \n}\n
A String can also be cast to a Secret:
assert(pass == \"123\" as Secret)\n
"},{"location":"reference/types/#functions_6","title":"Functions","text":"all String functions
Secret is compatible with String.
check(input:Secret):Bool
Checks the input Secret (not digested) against the digest version contained in this Secret.
Example:
if (user.password.check(password)) {\n securityContext.principal := us;\n securityContext.loggedIn := true;\n}\n
digest():Secret
Generates a digest of the clear-text password contained in this Secret.
Example:
var s : Secret := \"123\";\ns := s.digest();\nassert(s.check(\"123\" as Secret));\n
"},{"location":"reference/types/#email","title":"Email","text":"Represents an e-mail address as a string. If you are interested in sending email from your application, have a look at the Send Email page. The page input for an Email is a textfield with the following validation:
validate(/[a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?/.match(this), \"Not a valid email address\")\n
The page output for Email is the same as String output.
Example:
var address : Email := \"webdslorg@gmail.com\";\n
The default value for Email properties and variables is \"\". Add the 'not empty' annotation (or a custom validation) to an Email type property in an entity to disallow the empty string.
The Email type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Email typed vars. An Email can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(address:Email){\n test1(address as String); \n}\n
A String can also be cast to an Email:
assert(address == \"123\" as Email)\n
"},{"location":"reference/types/#functions_7","title":"Functions","text":"all String functions
Email is compatible with String.
"},{"location":"reference/types/#text","title":"Text","text":"Represents a large string. The page input for a Text is a textarea. The page output for Text is the same as String output.
Example:
var t : Text := \"123\";\n
The default value for Text properties and variables is \"\".
The Text type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Text typed vars. A Text can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t:Text){\n test1(t as String); \n}\n
A String can also be cast to a Text:
assert(t == \"123\" as Text)\n
"},{"location":"reference/types/#functions_8","title":"Functions","text":"all String functions
Text is compatible with String.
"},{"location":"reference/types/#wikitext","title":"WikiText","text":"Represents a large string with Markdown syntax support and internal page links. Internal page links in WikiText can be created using [[page(arg)|caption]]
.
The page input for WikiText is a textarea. The page output for WikiText processes the Markdown and page links and produces html elements.
Example:
var t : WikiText := \"123\";\n
The default value for WikiText properties and variables is \"\".
The WikiText type is compatible with the String type, all the String functions can be used, and String literals can be assigned to WikiText typed vars. A WikiText can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t: WikiText){\n test1(t as String); \n}\n
A String can also be cast to a WikiText:
assert(t == \"123\" as WikiText)\n
"},{"location":"reference/types/#functions_9","title":"Functions","text":"all String functions
WikiText is compatible with String.
"},{"location":"reference/types/#patch","title":"Patch","text":"Represents a patch. The page input for a Patch is the same as for Text. The page output for Patch is the same as for Text.
Example:
var p : Patch := \"12345\".makePatch(\"24\");\n
The default value for Patch properties and variables is \"\".
The Patch type is compatible with the String type, all the String functions can be used, and String literals can be assigned to Patch typed vars. A Patch can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(p:Patch){\n test1(p as String); \n}\n
A String can also be cast to a Patch:
assert(p == \"123\" as Patch);\n
"},{"location":"reference/types/#functions_10","title":"Functions","text":"all String functions
Patch is compatible with String.
applyPatch(arg: String):String
Applies this patch to the arg String.
Example:
var s1 : Patch := \"12345\".makePatch(\"24\");\nassert(s1.applyPatch(\"12345\") == \"24\");\n
"},{"location":"reference/types/#datetime","title":"DateTime","text":"Represents both a date and a time. The page input for a DateTime is a textfield, the expected format is dd/MM/yyyy H:mm. The page output for a DateTime shows the DateTime formatted with dd/MM/yyyy H:mm. Use the format function to customize the output format.
The default value for DateTime properties and variables is null.
The DateTime type is compatible with the Time and Date types. A DateTime can be cast to these types.
"},{"location":"reference/types/#date-creation-functions","title":"Date Creation Functions","text":"DateTime(String):DateTime
Dates can be constructed using the Date constructor (expected format dd/MM/yyyy H:mm):
var dt : DateTime := DateTime(\"22/06/1983 22:08\");\n
DateTime(String, String):DateTime
var dt : DateTime := DateTime(\"12:12 05-1994-06\", \"mm:H MM-yyyy-dd\");\n
The second parameter represents the date/time formatting string.
now():DateTime
Creates a DateTime containing the current time and day.
"},{"location":"reference/types/#functions_11","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
getSecond():Int
Gets the second.
getMinute():Int
Gets the minute.
getHour():Int
Gets the hour.
getDay():Int
Gets the day of the month.
getDayOfYear():Int
Gets the day of the year.
getMonth():Int
Gets the month.
getYear():Int
Gets the year.
"},{"location":"reference/types/#date","title":"Date","text":"Represents a date (not including a time). The page input for a Date is a textfield, the expected format is dd/MM/yyyy. The page output for a Date shows the Date formatted with dd/MM/yyyy. Use the format function to customize the output format.
The default value for Date properties and variables is null. Note that all Date types are DateTime at run-time.
The Date type is compatible with the DateTime and Time types. A Date can be cast to these types.
"},{"location":"reference/types/#date-creation-functions_1","title":"Date Creation Functions","text":"Date(String):Date
Dates can be constructed using the Date constructor (expected format dd/MM/yyyy):
var d : Date := Date(\"04/09/2009\");\n
Date(String, String):Date
The second parameter represents the date formatting string.
var d1 : Date := Date(\"12-20-1990\", \"MM-dd-yyyy\");\n
today():Date
Creates a Date containing the current day and time 00:00.
age(Date):Int
Gets the age from a date of birth.
"},{"location":"reference/types/#functions_12","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
Represents a time (not including a date). The page input for a Time is a textfield, the expected format is H:mm. The page output for a Time shows the Time formatted with H:mm. Use the format function to customize the output format.
The default value for Time properties and variables is null. Note that all Date types are DateTime at run-time.
The Time type is compatible with the DateTime and Date types. A Time can be cast to these types.
"},{"location":"reference/types/#time-creation-functions","title":"Time Creation Functions","text":"Time(String):Time
Time can be constructed using the Time function (expected format H:mm):
var t : Time := Time(\"22:08\");\n
Time(String, String):Time
The second parameter represents the date formatting string.
var t1 : Time := Time(\"59:08\", \"mm:H\");\n
"},{"location":"reference/types/#functions_13","title":"Functions","text":"format(formatstring:String):String
Format this DateTime using the formatstring. See Java SimpleDateFormat class documentation for formatstring syntax.
before(arg:Date/Time/DateTime):Bool
Tests whether this date and time is before the arg date and time.
after(arg:Date/Time/DateTime):Bool
Tests whether this date and time is after the arg date and time.
addSeconds(amount:Int)
Adds seconds, amount
may be negative.
addMinutes(amount:Int)
Adds minutes, amount
may be negative.
addHours(amount:Int)
Adds hours, amount
may be negative.
addDays(amount:Int)
Adds days, amount
may be negative.
addMonths(amount:Int)
Adds months, amount
may be negative.
addYears(amount:Int)
Adds years, amount
may be negative.
Represents a hyperlink. The page input for a URL is the same as for String. The page output for URL is a hyperlink.
Example:
var u : URL := \"https://webdsl.org\";\n
The default value for URL properties and variables is \"\".
The URL type is compatible with the String type, all the String functions can be used, and String literals can be assigned to URL typed vars. A URL can be cast to a String, this is necessary when calling functions or templates with String arguments. For example:
function test1(s:String){}\nfunction test2(t: URL){\n test1(t as String); \n}\n
A String can also be cast to a URL:
assert(t == \"https://webdsl.org\" as URL);\n
"},{"location":"reference/types/#url-creation-functions","title":"URL Creation Functions","text":"url(arg : String):URL
Casts the arg String to a URL type, equivalent to 'arg as URL'.
You can also use this in a navigate templatecall in order to provide an absolute URL instead of an internal link.
Example:
navigate(url(\"https://webdsl.org\")){ \"powered by WebDSL\" }\n
"},{"location":"reference/types/#functions_14","title":"Functions","text":"all String functions
URL is compatible with String.
"},{"location":"reference/types/#file","title":"File","text":"Represents an (uploaded) file. The page input for a File is a file upload component. The page output is a file download link.
"},{"location":"reference/types/#functions_15","title":"Functions","text":"fileName():String
Returns the name of this file.
getContentAsString():String
Returns the content of this file as String.
download()
The current action will result in a download of this file.
"},{"location":"reference/types/#image","title":"Image","text":"Represents an (uploaded) image. The page input for an Image is a file upload component. The page output shows the image.
"},{"location":"reference/types/#functions_16","title":"Functions","text":"fileName():String
Returns the name of this image file.
getContentAsString():String
Returns the content of this file as String.
download()
The current action will result in a download of this image file.
getWidth()
Returns the calculated width of this image.
getHeight()
Returns the calculated height of this image.
resize(maxWidth : Int, maxHeight : Int)
Resizes the image to the set dimensions. Note that currently, images can only be downscaled.
crop(x : Int, y : Int, width : Int, height : Int)
Crops the image to the specified size and coordinates.
clone() : Image
Makes a copy of the image and returns it.
"},{"location":"reference/using-the-editor/","title":"Using the Editor","text":"This is the WebDSL language reference. For more background information on the ideas, architecture, and design decisions behind WebDSL, see the Background section.
"},{"location":"reference/using-the-editor/#download-and-installation","title":"Download and Installation","text":"See Install WebDSL.
"},{"location":"reference/using-the-editor/#new-project-wizard","title":"New Project Wizard","text":"See Hello World! in WebDSL.
The plugin includes a new project wizard which will help you get started using WebDSL:
http://localhost:8080/{projectname}
to see the result.Use the \u2018Convert to a WebDSL Project\u2019 wizard to regenerate the project build files (this will overwrite the old files, including application.ini).
"},{"location":"reference/using-the-editor/#troubleshooting","title":"Troubleshooting","text":"If you encounter issues when running the plugin, here are a few things that you should check or try:
Checking user inputs and providing clear feedback is essential for the usability of web applications. WebDSL allows declarative specification of such input validation rules using the validate feature.
Validation rules in WebDSL are of the form validate(e,s) and consist of a Boolean expression e to be validated, and a String expression s to be displayed as error message. Any globally visible functions or data can be accessed as well as any of the properties and functions in scope of the validation rule context.
Value well-formedness checks (e.g. whether the user enters a valid integer in an Int input) are added automatically to each input field.
Validation can be specified on entities in property annotations:
entity User { \n username : String (id, validate(isUniqueUser(this), \"Username is taken\")) \n password : Secret (validate(password.length() >= 8, \"Password needs to be at least 8 characters\") \n , validate(/[a-z]/.find(password), \"Password must contain a lower-case character\") \n , validate(/[A-Z]/.find(password), \"Password must contain an upper-case character\") \n , validate(/[0-9]/.find(password), \"Password must contain a digit\"))\n email : Email\n} \nextend entity User { \n username(validate(isUniqueUser(this),\"Username is taken\")) \n password(validate(password.length() >= 8, \"Password needs to be at least 8 characters\") \n ,validate(/[a-z]/.find(password), \"Password must contain a lower-case character\") \n ,validate(/[A-Z]/.find(password), \"Password must contain an upper-case character\") \n ,validate(/[0-9]/.find(password), \"Password must contain a digit\")) \n}\n
Validation can be specified directly in pages:
page editUser( u: User ){ \n var p: Secret; \n form { \n group( \"User\" ){ \n label( \"Username\" ){ input( u.username ) } \n label( \"Email\" ){ input( u.email ) } \n label( \"New Password\" ){ \n input( u.password )\n } \n label( \"Re-enter Password\" ){ \n input( p ){ \n validate( u.password == p, \"Password does not match\" ) \n } \n } \n submit action{} { \"Save\" }\n } \n }\n}\n
Validation can be specified in actions:
page createGroup { \n var ug := UserGroup{}\n form { \n group( \"User Group\" ){ \n label( \"Name\" ){ input( ug.name ) } \n label( \"Owner\" ){ input( ug.owner ) } \n submit save() { \"Save\" }\n }\n }\n action save() {\n validate( ug.owner != null, \"A group must have an owner\" );\n validate( ug.owner.email != \"\", \"Owner has not provided an email address\" ); \n ug.save();\n email newGroupNotify( ug );\n return userGroup( ug );\n }\n}\nemail newGroupNotify( u: UserGroup ){\n from( \"info@example.com\" )\n to( u.owner.email )\n subject( \"Usergroup created: ~u.name\" )\n \"Your usergroup ~u.name has been created.\"\n}\n
"},{"location":"reference/validation/#customizing-validation-output","title":"Customizing Validation Output","text":"Validation output can be customized by overriding the templates used to display validation messages. Currently, there are 4 global validation templates:
ignore-access-control template errorTemplateInput(messages : [String])\n
Displays validation message related to an input.
ignore-access-control template errorTemplateForm(messages : [String])\n
Displays validation message for validation in a form.
ignore-access-control template errorTemplateAction(messages : [String])\n
Displays validation message for validation in an action.
ignore-access-control template templateSuccess(messages : [String])\n
Displays validation message for success messages.
When overriding these validation templates, use an elements
templatecall to refer to the element being validated.
Example:
ignore-access-control template errorTemplateInput(messages : [String]){\n elements\n for(ve: String in messages){\n output(ve)\n } \n}\n
"},{"location":"reference/validation/#ajax-validation","title":"Ajax Validation","text":"WebDSL provides input components that validate the inputs using ajax.
built-in value types:
inputajax(String/Secret/URL/Email/Text/WikiText)\ninputajax(Int/Bool/Float/Long)\n
reference types:
inputajax(Entity / [Entity] / {Entity})\nselectajax(Entity / {Entity})\nradioajax(Entity)\n
provide selection options:
inputajax(Entity / [Entity] / {Entity} , [Entity])\nselectajax(Entity / {Entity} , [Entity])\nradioajax(Entity , [Entity])\n
Selection options can also be provided using the allowed
annotation on an entity property. Example:
entity Person{\n parent : Person (allowed=from Person as p where p != this)\n}\n
"},{"location":"reference/webdsl-apps/","title":"WebDSL Apps","text":""},{"location":"reference/webdsl-apps/#the-structure-of-a-webdsl-application","title":"The structure of a WebDSL application","text":"WebDSL application are organized in *.app files. Each .app file has a header, that either declares the name of a module or the name of an application. The declared name should be identical to the filename. Each application needs an .app file that declares the name of the application. This is the name refered to in the application.ini file (see below).
An application can be organized in different modules. In a typical .app file the header is followed by a list of import statements, which contain a path to other modules (without extension). In this way your application can be separated over several files, and modules can be reused.
Within .app files one can define sections. A section is merely a label to identify the structure of a file. Most section names have no influence on the program itself, some have however, for example in styling definitions.
The real contents of a .app file are a list of definitions. This might be page-, template-, action- or entity definitions. Other kinds of definitions might be introduced by WebDSL modules. A module might either refer to a module of an WebDSL application, or to an module of the WebDSL compiler itself. In this case the latter one is refered to. Those definitions will be examined in detail in the next chapters.
A very simple application might look like:
HelloWorld.app:
application HelloWorld\n\nimports MyFirstImport\n\nsection pages\n\npage root () { \n \"hello world\" \n IAmImported() \n}\n
MyFirstImport.app:
module MyFirstImport\n\ntemplate IAmImported() { \n spacer \n \"I am imported from a module file\" \n}\n
In the second file the section declaration is omitted, since an application may start with a list of declarations as well. The page that is shown when no page is specified (e.g. when visiting http://localhost:8080/yourapp) is named \"root\" and has no arguments.
"},{"location":"releases/","title":"WebDSL Releases","text":""},{"location":"releases/#webdsl-plugin-for-eclipse","title":"WebDSL Plugin for Eclipse","text":"Download the latest release of the WebDSL Plugin for Eclipse bundle for your platform:
WebDSL in Eclipse bundle (Linux)
WebDSL in Eclipse bundle (MacOS)
WebDSL in Eclipse bundle (Windows)
Installation instructions.
Or install the WebDSL plugin in an existing Eclipse 3.5+ installation using this update site:
https://update.webdsl.org/update\n
Installation instructions.
"},{"location":"releases/#webdsl-command-line-interface","title":"WebDSL Command-Line Interface","text":"Download the latest release of the WebDSL CLI:
WebDSL CLI
Installation instructions.
"},{"location":"support/","title":"Support","text":"If you need help with installing or using WebDSL, or you are interested in contributing to WebDSL, here is how to get involved:
"},{"location":"support/#mailing-list","title":"Mailing list","text":"You can subscribe to the mailing list for asking questions:
https://groups.google.com/d/forum/webdsl
"},{"location":"support/#issue-tracker","title":"Issue Tracker","text":"Issues are tracked in YellowGrass:
http://yellowgrass.org/project/WebDSL
"},{"location":"support/#search-the-webdsl-source-code","title":"Search the WebDSL Source Code","text":"The WebDSL repositories are indexed on reposearch:
http://codefinder.org/search/WebDSL
"},{"location":"tutorials/","title":"Tutorials","text":"This page lists tutorials that take you step-by-step through a project to learn a variety of concepts and aspects of WebDSL in a specific scope. For guides on achieving specific tasks, see the How To's section. For the WebDSL language reference, see the Reference section.
"},{"location":"tutorials/#setup-for-the-tutorials","title":"Setup for the Tutorials","text":"Installing WebDSL is necessary to follow the tutorials. Use the installation guide and start to learn by example using the tutorials.
"},{"location":"tutorials/#list-of-available-tutorials","title":"List of Available Tutorials","text":"To create a Hello World application in WebDSL, we have to:
Open the WebDSL editor and right-click the Package Explorer.
Select New WebDSL Project to make the project configuration show up.
Enter project name: HelloWorld
and press Finish.
The generated project contains lots of files but we only need the main application file for this tutorial. Open HelloWorld.app
in the root directory of the new project. It should look as follows:
application HelloWorld\n\n page root(){ \"Hello world\" }\n
"},{"location":"tutorials/hello-world/#build-and-deploy-the-application","title":"Build and Deploy the Application","text":"To see the resulting web application in the browser, we have to compile and deploy the WebDSL code.
Navigate to the project directory in the terminal (e.g. ~/eclipse-workspace/HelloWorld/
). To compile and run the application in one command, execute webdsl run
:
$ webdsl run\n# This should start a local web server and generate the following output:\n# ...\n# [ Main | info ] stage 1: parsing HelloWorld.app [ 0.78s ]\n# ...\n# [ Main | info ] stage 11: writing files [ 0.04s ]\n# ...\n# Now compiling generated Java code\n# ...\n# Some file copying\n# ...\n# Some SQL DROP/CREATE table statements\n# ...\n# Some Tomcat commands\n# ...\n# [XX <Month> XX:XX:XX HelloWorld] Tomcat web server started, your web application is available at http://localhost:8080/HelloWorld. Press ctrl+c to stop the web server.\n
Browsing to http://localhost:8080/HelloWorld/
opens the root page of your application and should output Hello world as we specified in HelloWorld.app
.
To stop the application server, simply press Ctrl+C in the terminal where the app is running.
"},{"location":"tutorials/hello-world/#summary","title":"Summary","text":"In this tutorial we created a web application with WebDSL, consisting of a single page that outputs Hello world.
"},{"location":"tutorials/pages-and-navigation/","title":"Pages and navigation","text":"In this tutorial we will create an online cookbook. To achieve this, we have to:
We start this tutorial with a project named cookbook
. To learn how to set up a WebDSL project, follow our Hello World tutorial.
Our homepage will list all recipes available in the cookbook. Open cookbook.app
and change the root page accordingly.
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem { \"Lasagne\" }\n listitem { \"Pancakes\" }\n listitem { \"Tomato Soup\" }\n }\n }\n
Built-in templates In the code snippet above, header
, list
and listitem
are templates defined in the built-in.app
. header { \"Recipes\" }
is syntactic sugar for <h1> \"Recipes\" </h1>
.
Once built and ran, the result (visible at http://localhost:8080/cookbook/
) is a homepage with the static elements we defined.
Next, we would like to have an additional page in our application:
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem { \"Lasagne\" }\n listitem { \"Pancakes\" }\n listitem { \"Tomato Soup\" }\n }\n }\n\n page recipe() {\n header { \"I'm a recipe!\" }\n }\n
In this updated version, visiting http://localhost:8080/cookbook/
will still show the home page as we have seen before. The new recipe page is available on http://localhost:8080/cookbook/recipe
.
In our previous build, we have two separate pages but no link from one to the other. To add links to other pages, we can use the navigate
construct that WebDSL provides:
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem {\n navigate recipe() { \"Lasagne\" }\n }\n listitem {\n navigate recipe() { \"Pancakes\" }\n }\n listitem {\n navigate recipe() { \"Tomato Soup\" }\n }\n }\n }\n\n page recipe() {\n navigate root() { \"Back to homepage\" }\n\n header { \"I'm a recipe!\" }\n }\n
A navigate
call consists of:
recipe()
in our case)\"Lasagne\"
in our case)The updated version of our application shows the list items as links to the recipe page, which now also includes a link back to the homepage.
"},{"location":"tutorials/pages-and-navigation/#add-page-parameters","title":"Add Page Parameters","text":"The last thing to do, in order to complete our cookbook, is to show which recipe you are currently viewing. In the previous build, we could link from our homepage to the recipe page, but the recipe page was always the same.
To make the recipe page aware of the current recipe, we will add a page parameter to its definition, and update the page calls to the recipe page accordingly.
application cookbook\n\n page root() {\n header { \"Recipes\" }\n\n list {\n listitem {\n navigate recipe(\"Lasagne\") { \"Lasagne\" }\n }\n listitem {\n navigate recipe(\"Pancakes\") { \"Pancakes\" }\n }\n listitem {\n navigate recipe(\"Tomato Soup\") { \"Tomato Soup\" }\n }\n }\n }\n\n page recipe(name : String) {\n navigate root() { \"Back to homepage\" }\n\n header { \"Recipe: ~name\" }\n }\n
String interpolation In the code snippet above, String interpolation (with a tilde ~
) is used to insert the parameter name
in the header: \"Recipe: ~name\"
.
The updated version of our application passes the recipe name as page pararmeter. This change is reflected in the URL. Instead of http://localhost:8080/cookbook/recipe
, the name
parameter is now a part of the new URL: http://localhost:8080/cookbook/recipe/<name>
.
In this tutorial, we created an online cookbook using multiple page definitions, page parameters and navigation between pages.
"},{"location":"tutorials/templates/","title":"Templates","text":"In this tutorial we will change the online cookbook from previous tutorial to have less code duplication. To achieve this, we have to:
WORK IN PROGRESS
"}]} \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 3433520..461d7bf 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ