Skip to content
Open

docs #58

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/Caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
### Proposal for Implementing Caching

Caching Method Calls (Typically for DAO)

Two annotations are used: `@CacheCall` and `@InvalidateCacheCall`. Examples:

```java
public class UserDaoImpl extends ApplicationDaoImpl<User> implements UserDao {
...
@CacheCall(cacheName = "User")
public User find(long id) {
...
}
...
@InvalidateCacheCall("User")
public void update(User user) {
...
}
...
}
```

@CacheCall expects the following parameters:

1. cacheName - the cache name. The value will be cached in (or retrieved from) the cache with this name.

2. ttl - time to live (in seconds) for the cache, i.e., the maximum lifetime of the cached method execution result.

`@InvalidateCacheCall` expects a value parameter, which can be a string or an array containing the names of caches to be invalidated (completely cleared).

How it works:

1. If a class is loaded via IoC, Google Guice processes it using an interceptor that handles these annotations.

2. When a method annotated with `@CacheCall` is called, a hash value is computed from its arguments array + method name + class name. If the specified cache contains an entry with such a key, the method is not called, and the value from the cache is returned as the result. Otherwise, the method is called, and its return value is stored in the cache under the computed hash key.

3. When a method annotated with `@InvalidateCacheCall` is called, entries in all caches specified as arguments in the annotation are cleared.

If the application is running in debug mode, when processing `@CacheCall`, the method is always called, but its return value is compared with the value stored in the cache. If they do not match, a log entry is written, and a `CacheLogicException` is thrown.

Caching Action Methods of Components (Controllers):

```java
public class UserPage extends Page {
...
@Cacheable
public void onShowStatus() {
...
}
...
}
```
34 changes: 34 additions & 0 deletions docs/Captions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### Captions

Nocturne makes it very easy to write multilingual applications and conveniently edit various captions, labels, and other text elements.

This mechanism is called Captions, and its essence is that the output of any textual information is wrapped in a caption. For example, in a template, instead of `<td>Login:</td><td><input name="login" class="login"/></td>`, you should write `<td>{{Login}}:</td><td><input name="login" class="login"/></td>`. One of the advantages of this approach is that the templates remain easy to read and are not cluttered with constants like `messages.password-change-confirmation-label`.

In this case, during the template loading stage, Nocturne will process the `{{Login}}` instruction and replace it with the value of the caption named `Login` in the required locale.

Everything that is output and can be language-dependent should go through captions. For example, sometimes this needs to be done in the application code as well. Validator messages should be wrapped, and they are often taken from `ValidationException`:

```java
if (!value.matches("\\w+")) {
throw new ValidationException($("Field should contain letters, digits and underscore characters"));
}
```

Note the magic method `$()`, which is available to all validators and controllers and can be accessed from any part of the code via `ApplicationContext.getInstance().$()`. It has an overload that allows passing parameters, for example: `$("Field should contain at least {0} characters", minimalLength)`.

In templates, besides the syntax with double curly braces, you can use the caption directive. Examples:

```html
<@caption params=["Mike"]>Hello, {0}</@caption>
<@caption>Login</@caption>
<@caption key="Login"/>
<@caption key="Hello, {0}" params=["Mike"]/>
```

The caption mechanism can also be useful for a monolingual application, as the programmer may not need to think about exact wordings or grammatical correctness, while a specialist can correct or rewrite captions by editing (or translating) them via a web interface.

### interface Captions

All access to captions goes through an implementation of the `org.nocturne.caption.Captions` interface, specific to your application. The simplest implementation is called `org.nocturne.caption.CaptionsImpl` and is the default. It stores captions in properties files depending on the locale (e.g., captions_ru.properties). Furthermore, by analyzing the value of nocturne.debug-captions-dir in debug mode, these files are automatically created and maintained in the corresponding directory. If a value is missing for a locale, then, if it is the default locale, a value matching the caption name is set. Otherwise, the value `nocturne.null` is set, indicating that the value should be taken from the default locale. In production mode, this implementation does not write to properties files but only reads values.

You can write your own implementation of the `org.nocturne.caption.Captions` interface.
48 changes: 48 additions & 0 deletions docs/Frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
### Frames

Often, some complex pages need to be decomposed because different pages contain identical elements (for example, a news panel). Nocturne supports extracting such components into frames. Each frame has its own controller (a class inherited from org.nocturne.main.Frame) and can have its own template. The frame's lifecycle mirrors that of the page and begins when code like `parse("parsedFrame", someFrame)` (or simply `parse(someFrame)`) is called for a given frame named someFrame. In short:

1. The frame is initialized if necessary (exactly once per instance).

2. initializeAction()

3. An event is fired, triggering subscribers of Events.beforeAction(SomeFrame.class, ...).

4. The validation method runs if needed. It uses the current page's action.

5. The action method runs (if validation passed).

6. The invalid method runs (if validation failed).

7. An event is fired, triggering subscribers of Events.afterAction(SomeFrame.class, ...).

8. finalizeAction()

The component that called `parse("parsedFrame", someFrame)` now has the frame's rendering result stored in its internal map under the key "parsedFrame". Now, if the template of this component contains `<@frame name="parsedFrame"/>`, the frame's rendering result will be inserted in that place. Thus, pages and other frames can include frames.

In components that contain frames, it is best to instantiate them via IoC using the @Inject annotation.

### Example of Working with a Frame

```java
@Link(";home")
public class IndexPage extends Page {
@Inject
private LoginFormFrame loginForm;

public void action() {
parse("loginForm", loginForm);
}
}
```

And the IndexPage.ftl template:

```ftl
<#import "macros/common.ftl" as common>

<@common.page>
<@frame name="loginForm"/>
...
</@common.page>
```
27 changes: 27 additions & 0 deletions docs/NocturneParams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
### Introduction

Parameters description

### Details

| Parameter name | Default | Values / example | Meaning | Type in Java |
| :------------------- | :------------ | :--------------------- | :------------ | :----------------- |
| nocturne.debug | Default value is "false" | "true" or "false" | Use it in the development mode to switch on nocturne main feature: class reloading | boolean |
| nocturne.templates-update-delay | Default value is 60 | 0 | Delay between checks of template files to be changed (in seconds) | int |
| nocturne.templates-path | Required | WEB-INF/templates | Directory where to find templates in the deployed application, all the freemarker templates for view layer should be in it. Deprecated since Nocturne version 1.2.6: use nocturne.template-paths | String |
| nocturne.template-paths | Required | /opt/templates;WEB-INF/templates | Directory list where to find templates in the deployed application, all the freemarker templates for view layer should be in it | List<String> |
| nocturne.sticky-template-paths | Default value is "true" | "true" or "false" | Indicates if template loader should stick to last successful template path or always check template paths in the configured order | boolean |
| nocturne.reloading-class-paths | Default value is empty string (no directories for class reloading) | Values depends on development home directory and IDE you are using | In the debug mode the application will load classes from specified directories on each request | List<File> |
| nocturne.page-request-listeners | Default value is empty string (no request listeners) | Classes separated with ";" | Listener classes for requests dispatching by nocturne | List<String> |
| nocturne.guice-module-class-name | Required | Class name (implements com.google.inject.Module) | Nocturne uses google-guice as default IoC container | String |
| nocturne.skip-regex | Default value is empty string (do not skip) | Example: `/(css\|image\|js)/.` | Regex to describe URLs to be skipped by nocturne | Pattern |
| nocturne.class-reloading-packages | Default value is empty string (do not reload) | You can specify several packages separated with ";" | All classes in specified packages will be reloaded | List<String> |
| nocturne.class-reloading-exceptions | Default value is empty string (no exceptions) | You can specify several packages or class names separated with ";" | Exceptions for nocturne.class-reloading-package: specified classes (or classes in specified directories) will not be reloaded | List<String> |
| nocturne.request-router | Required | Class name (extends org.nocturne.main.RequestRouter) | Class to route request to page | String |
| nocturne.debug-captions-dir | Optional | Example: `${development.home}/web/src/main/resources` | Directory where to store property files for captions if you are using org.nocturne.caption.CaptionsImpl as Captions backend | String |
| nocturne.debug-web-resources-dir | Optional | Example: `${development.home}/web/src/main/webapp` | Directory where to locate web resources (such as css, js, images) which can be modified during the development and which should be processed by DebugResourceFilter | String |
| nocturne.nocturne.captions-impl-class | Default value is org.nocturne.caption.CaptionsImpl | Example: your.application.DatabaseCaptions | Class implemented org.nocturne.caption.Captions | String |
| nocturne.caption-files-encoding | Default value is UTF-8 | Example: windows-1251 | How to store caption properties files if you are using org.nocturne.caption.CaptionsImpl as Captions backend | String |
| nocturne.allowed-languages | Default value is "en" | Example: "en,ru" | Application can set language only to one of the listed languages. Application uses lang, language, locale parameters to find 2-letters language code. If it specified once, stores current locale in the session. | String |
| nocturne.default-language | Default value is "en" | Example: "ru" | Default language if no language specified, should be in nocturne.allowed-languages | String |

3 changes: 3 additions & 0 deletions docs/PageRequestListener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Using PageRequestListeners

You can register listeners by specifying a list of fully qualified class names in nocturne.page-request-listeners. Classes must be separated by semicolons and should implement PageRequestListener. These classes are instantiated with IoC support (i.e., you can use @Inject in them). Each object is notified before page processing and after it. If processing results in an exception, the corresponding exception is passed as the second parameter in afterProcessPage.
35 changes: 35 additions & 0 deletions docs/RequestLifeCycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Nocturne only processes requests that match the url-pattern for the org.nocturne.main.DispatchFilter.

If the request URL matches the nocturne.skip-regex parameter, Nocturne stops processing the request and passes it to the filterChain.

Otherwise, the mechanism for finding a suitable page (Page) to handle the request is initiated. For this, the URL and request.getParameterMap() are passed to the class specified in nocturne.request-router, which returns a special Resolution object with three fields: pageClassName, action, and overrideParameters. The first denotes the page (controller) class name, the second the action within that page, and the third is a Map that overrides (or adds) parameters to the request.

Once the Page is identified, an instance of the page is either created or taken from the page pool, and then work proceeds with it. Note that pages are allocated from the pool in such a way that a single page instance is not processed simultaneously by two or more threads. Thus, there is no need to worry about thread safety for pages.

Next:

1. The beforeProcessPage() methods of the registered PageRequestListeners are called.

2. If the page was just created, its init() method is called. Therefore, this method is called exactly once for each page (upon the first request after creation).

3. The initializeAction() method is called (regardless of the expected action). At this point, the page is ready for use, and parameters have been injected into it.

4. The Events.beforeAction() event is generated, and all subscribers to this class of the page or its ancestor will be notified.

5. The appropriate validation method for the action is called. This is either a method named validate() or a method annotated with @Validate("action name"). If the annotation is specified without a parameter, the annotated method becomes the default. If the required action is not found, no method is called. For details on validation, see [ValidationFlow](ValidationFlow.md). If the validation method returns true, control proceeds to the first of the next two items; otherwise, it proceeds to the second:

* The required action is called: this is either the action() method (by default) or any void method with an empty parameter list annotated with @Action("action name"). If the annotation is specified without a parameter, the annotated method becomes the default. If the required action is not found, the default action is called. Pages may parse frames at this stage (though they can also do so at other stages). For more information, see [Frames](Frames.md).

* The required invalid method is called: this is either the invalid() method (by default) or any void method with an empty parameter list annotated with @Invalid("action name"). If the annotation is specified without a parameter, the annotated method becomes the default. If the required invalid method is not found, the default invalid method is called. If that is also absent, nothing is called.

6. The Events.afterAction() event is generated, and all subscribers to this class of the page or its ancestor will be notified.

7. The finalizeAction() method is called.

8. If the page has not refused template processing (i.e., has not called skipTemplate()), the template processing is initiated. By default, the template name matches the short class name of the page plus the ".ftl" suffix, but the name can be changed using setTemplateName.

9. The afterProcessPage() methods of the registered PageRequestListeners are called.

10. If the page has called the setProcessChain(true) method, the request-response pair is passed to the filterChain.

If an exception occurs, control is transferred to step 9, but step 10 is skipped. The abortXXX() methods trigger an AbortException, after writing the corresponding redirection to the response.
63 changes: 63 additions & 0 deletions docs/ValidationFlow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
### Validation

If there is a corresponding validation method for an action, it is called first. Typically, such methods add a validator to the controller and call runValidation(). A typical validation method looks like this:

```java
@Validate("addUser")
public boolean validateAddUser() {
addValidator("name", new LengthValidator(2, 32));
addValidator("age", new IntegerValidator(1, 200));
return runValidation();
}
```

When the runValidation() method is called, all parameterNames for which at least one validator has been set are iterated, and for each of them, the validators are processed in the order they were added. They are executed (with the values of the corresponding parameters passed to them). These parameters are always placed into the view (i.e., `put(parameter, getString(parameter))` is executed), and if a validator for a field fails, a value equal to the message from the failed validator is placed into the view under the key `"error__" + parameterName`.

Thus, in forms within the view, the code often looks like this (of course, it can be wrapped in a macro to avoid copy-paste):

```ftl
<tr>
<td>
Login:
</td>
<td>
<input name="login" value="${login!}"/>
</td>
</tr>
<#if error__login??>
<tr>
<td>&nbsp;</td>
<td><span class="field-error">${error__login!}</span></td>
</tr>
</#if>
```

In the code above, if the form is invalid, the field value will be preserved, and if the field is invalid, an error message will be added to it.

Here is the complete code for a simple controller for a form to add a user with a given name and age. The action expected for this operation is "add":

```java
@Link("user/{action}")
public class UserChangePage extends Page {
@Parameter(stripMode = Parameter.StripMode.SAFE)
private String name;

@Parameter
private int age;

@Inject
private UserDao userDao;

@Validate("add")
public boolean validateAddUser() {
addValidator("name", new LengthValidator(2, 32));
addValidator("age", new IntegerValidator(1, 200));
return runValidation();
}

@Action("add")
public void onAddUser() {
userDao.add(new User(name, age));
}
}
```