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 @@

WebDSL Releases

WebDSL Plugin for Eclipse

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)

Installation instructions.

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":""},{"location":"background/about/#software","title":"Software","text":""},{"location":"background/about/#developers","title":"Developers","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":""},{"location":"background/about/#non-active-developers","title":"Non-active developers","text":""},{"location":"background/meeting-notes/","title":"Meeting Notes","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":""},{"location":"background/meeting-notes/#development-experience","title":"Development Experience","text":""},{"location":"background/meeting-notes/#new-language-features","title":"New Language Features","text":""},{"location":"background/meeting-notes/#enhancement-proposals","title":"Enhancement Proposals","text":""},{"location":"background/meeting-notes/#syntax","title":"Syntax","text":""},{"location":"background/meeting-notes/#improve-built-in-functionality","title":"Improve Built-in Functionality","text":""},{"location":"background/meeting-notes/#new-language-constructs","title":"New Language Constructs","text":""},{"location":"background/meeting-notes/#lucene-search","title":"Lucene Search","text":""},{"location":"background/meeting-notes/#development-environment","title":"Development Environment","text":""},{"location":"background/publications/","title":"Publications","text":"

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":""},{"location":"howtos/install-cli/","title":"Install and Use the WebDSL CLI","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.

  1. Extract the zip file.
  2. Add the webdsl/bin directory to your $PATH.
"},{"location":"howtos/install-cli/#usage","title":"Usage","text":"

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-line

You 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.

  1. In Eclipse, go to menu Help \u2192 Install New Software.
  2. In the Work with: text area, type:

    https://update.webdsl.org/update\n
  3. Uncheck Group items by category to make the plugin visible.

  4. Check WebDSL Editor.
  5. Click Install and go through the remaining steps.
  6. Restart Eclipse.

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.

"},{"location":"howtos/install/","title":"Install WebDSL","text":"

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 Bundle

Download an Eclipse instance with the latest WebDSL plugin pre-installed for your platform:

WebDSL in Eclipse bundle

Installation instructions.

Alternative: Eclipse Plugin

Perform 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 Build

Download the WebDSL CLI for your platform:

WebDSL CLI

Installation instructions.

Alternative: Build WebDSL Yourself

Clone 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).

"},{"location":"reference/access-control/#authentication","title":"Authentication","text":"

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:

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:

operators The following operators are supported:

binary operators

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:

"},{"location":"reference/advanced-search/#making-your-data-searchable","title":"Making your data searchable","text":"

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":"as name","text":"

Override the default search field name. Default: property name

"},{"location":"reference/advanced-search/#using-analyzer","title":"using analyzer","text":"

Indexed using analyzer analyzer instead of the default analyzer.

"},{"location":"reference/advanced-search/#boosted-to-floatfloat","title":"boosted to Float|^Float","text":"

Search field is boosted to Float at index time (default 1.0).

"},{"location":"reference/advanced-search/#spellcheckautocompletespellcheckautocomplete","title":"(spellcheck)|(autocomplete)|(spellcheck,autocomplete)","text":"

Indicate that this search field can be used for spell checking/autocompletion.

"},{"location":"reference/advanced-search/#for-subclass-entity","title":"for subclass entity","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":"depth Int|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.

search mapping <-> searchable annotation subject <-> searchablesearchable() subject as sbj <-> searchable(name = sbj) subject using defaultNoStop <-> searchable(analyzer = defaultNoStop) subject^2.0 <-> searchable()^2.0 subject boosted to 2.0 <-> searchable(boost = 2.0) subject as sbjTriGram using trigram boosted to 0.5 <-> searchable(analyzer = trigram, name = sbjTriGram)^0.5 subject as sbjUntokenized using none <-> searchable(analyzer = none, name = sbjUntokenized) message as sbjAC using kwAnalyzer (autocomplete) <-> searchable(analyzer = kwAnalyzer, name = sbjAC, autocomplete) user as forumuser for subclass ForumUser <-> searchable(name = forumuser, subclass = ForumUser) user with depth 2 <-> searchable(depth=2) + text as txt <-> searchable(name = txt, default)"},{"location":"reference/advanced-search/#which-properties-can-be-made-searchable","title":"Which properties can be made searchable ?","text":"

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.

"},{"location":"reference/advanced-search/#numeric-properties-floatintdatedatetimetime","title":"Numeric properties (Float,Int,Date,DateTime,Time)","text":"

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).

"},{"location":"reference/advanced-search/#searching-the-data","title":"Searching the data!","text":"

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:

"},{"location":"reference/advanced-search/#filtering-with-filters-filterconstraint","title":"Filtering: 'with filter(s) {filterconstraint* ,}'","text":"

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:

"},{"location":"reference/advanced-search/#filtering-on-facet","title":"Filtering on facet","text":"

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 Messages 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.

"},{"location":"reference/advanced-search/#search-namespaces","title":"Search namespaces","text":"

Search namespaces become usefull if you want to allow searches on entities with some specific property value. For example searching Messages 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).

"},{"location":"reference/advanced-search/#result-highlighting","title":"Result highlighting","text":""},{"location":"reference/advanced-search/#spell-checking","title":"Spell checking","text":""},{"location":"reference/advanced-search/#auto-completion","title":"Auto completion","text":""},{"location":"reference/advanced-search/#faceted-search","title":"Faceted search","text":"

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.

"},{"location":"reference/ajax/#ajax-targets","title":"Ajax Targets","text":"

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.

"},{"location":"reference/ajax/#forms-and-ajax","title":"Forms and Ajax","text":"

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&amp;characterEncoding=UTF-8&amp;useUnicode=true&amp;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.

"},{"location":"reference/app-configuration/#search-configuration","title":"Search Configuration","text":"

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)

"},{"location":"reference/app-configuration/#deploy-with-tomcat-manager","title":"Deploy with Tomcat Manager","text":"

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

"},{"location":"reference/cli-usage/","title":"CLI Usage","text":""},{"location":"reference/cli-usage/#running-an-application-using-the-command-line-interface","title":"Running an application using the command-line interface","text":"

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:

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()

"},{"location":"reference/entities/#name-property","title":"Name Property","text":"

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":""},{"location":"reference/entities/#allowed-property-annotation","title":"Allowed Property Annotation","text":"

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()

"},{"location":"reference/entities/#entity-inheritance","title":"Entity Inheritance","text":"

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:

  1. 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.

  2. If a property of the entity is called 'name' and is of type String, this property determines the entity name.

  3. Otherwise, the id of the entity (converted to its string-value) is used.

"},{"location":"reference/entities/#example","title":"Example","text":"

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)

"},{"location":"reference/forms/#null","title":"Null","text":"

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\"

"},{"location":"reference/forms/#radio-buttons","title":"Radio Buttons","text":"

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{\"&nbsp;\"}\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":""},{"location":"reference/queries/#example-a-paginated-view","title":"Example: a paginated view","text":"
// 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:

"},{"location":"reference/search/","title":"Search","text":"

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.

"},{"location":"reference/search/#indexing","title":"Indexing","text":"

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.

"},{"location":"reference/styling/","title":"Styling","text":"

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:

"},{"location":"reference/synchronization-framework/#application","title":"Application","text":"

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:

"},{"location":"reference/synchronization-framework/#toplevel-entity","title":"TopLevel Entity","text":"

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:

"},{"location":"reference/synchronization-framework/#synchronization-core","title":"Synchronization core","text":"

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: