Slimweb
Slimweb is a lightweight servlet-based Java web application framework.
Why yet another web framework, when we have popular frameworks already? Because sometimes, You just don't want the complexity of those popular frameworks. Instead, You need basic request handling and HTML-s, which would be simple to set up, simple to use, have minimal dependencies and have shallow learning curve. For large and complex enterprise projects, Slimweb probably lacks features and flexibility.
A Slimweb application consists of 4 kinds of artifacts:
- Application configuration
- Component service(s) - (optional)
- View template(s) - (optional)
- Label translation files - (optional)
The main concept is that You have one or several views aka HTML web page(s). To provide data and to handle interaction (for example button clicks) with these pages, You provide components with service methods. Slimweb handles HTML page data mapping and routing to/from components.
Features
- HTML page data mapping and routing to/from components
- Language-specific views: a single template for multiple languages
- Automatic request logging
- Automatic CSRF attack detection
- Strongly typed session data management
- Declarative and custom validation
- Server push via websocket
- Requires Java 11 or later
Basic Usage
The minimum web application consists of a build script and 2 Java classes:
- A component
- An application configuration
Provide a component class, which would handle GET requests at https://<my.host>/controller/my-component:
package mypackage.components;
@Component
public class MyComponent {
public String[] get() {
return new String[] {"abc", "xyz"};
}
}
Provide an application configuration, which implements interface ApplicationConfiguration and is named SlimwebConfiguration. A name other than SlimwebConfiguration is not recognized by Slimweb and initialization would fail.
package mypackage.components;
// For convenience, we are using ApplicationConfigurationAdapter instead of ApplicationConfiguration
public class SlimwebConfiguration extends ApplicationConfigurationAdapter {
@Override
public String[] getComponentPackages() {
//Slimweb will only scan components in this Java package and its subpackages
return new String[] {"com.mypackage.components"};
}
}
Build Dependencies
Add Slimweb dependency into build.gradle:
apply plugin: 'war'
dependencies {
implementation 'eu.miltema:slimweb:0.4.3'
}
These 3 files are all You need (SlimwebConfiguration.java, MyComponent.java, build.gradle). Now, build the war and You are ready to go!
Slimweb itself depends on couple of libraries, which are resolved by build system automatically.
Components
In "Basic Usage", session argument injector was introduced. In fact, it is possible to declare methods with any argument type as long as appropriate injector has been registered with ApplicationConfiguration. By default, Slimweb supports these method argument types: HttpSession, HttpServletRequest, HttpServletResponse, HttpAccessor, LanguageLabels.
In component, methods have special naming convention. Below is a table with some url-to-method mapping examples (in class MyComponent):
http | url | Java method |
---|---|---|
GET | /controller/my-component/users | getUsers() |
GET | /controller/my-component | get() |
POST | /controller/my-component/user | postUser() |
PUT | /controller/my-component | put() |
DELETE | /controller/my-component/user | deleteUser() |
Dependency Injection
Each of the methods can be declared with parameters. By default, these built-in parameter types are supported:
- HttpSession
- HttpServletRequest
- HttpServletResponse
- HttpAccessor
- LanguageLabels
Example:
public MyComponent get(HttpAccessor htAccessor) {
String urlParam = htAccessor.getParameter("my_url_parameter");
...
}
Often You need to inject Your custom dependencies. For example, You might want to use SlimORM database link like this:
public MyComponent get(Database db) {
...
}
Then You must declare appropriate injector in SlimwebConfiguration:
public class SlimwebConfiguration implements ApplicationConfiguration {
@Override
public void registerInjectors(Map<Class<?>, ArgumentInjector> mapInjectors) {
mapInjectors.put(Database.class, hta -> new Database("java:comp/env/jdbc/mydb")); // register Database injector
}
}
Views
A view is an HTML web page. These can be defined in project's src/main/webapp folder as usual. Web server will serve such pages itself and Slimweb is unaware of these.
However, sometimes You need locale-specific views (English, Spanish, German) and You want to avoid the translation hassle in front-end technologies like Angular, React or Vue. Then You place HTML and JS templates into project's src/main/resources/templates folder, to become accessible to Slimweb template engine. Valid extensions are .html, .htm and .js. Translation files go into project's src/main/resources/labels folder. There is a separate label file for each language, for example en.lbl, de.lbl and es.lbl (notice the .lbl extension).
Here is an example template file:
<h1>{-frontpage.title-}</h1>
<p>{-frontpage.hellotext-}</p>
{-file:footer.html-}
Slimweb template engine will replace {-frontpage.title-} and {-frontpage.html-} placeholders with proper values from label file. Placeholder {-file:footer.html-} indicates, that it must be replaced with contents from file footer.html.
Here is an example label file:
frontpage.title=Slimweb Demo
frontpage.hellotext=Hello, world!
Usually, views share a common frame (perhaps with header, footer and other components). The frame inclusion logic is declared in SlimwebConfiguration and the frame itself in templates-folder:
public class SlimwebConfiguration extends ApplicationConfigurationAdapter {
@Override
public String getFrameForTemplate(String templateFile, HttpAccessor htAccessor) {
//NB! both file names without .html extension
return htAccessor.getSessionObject() == null ? "loginframe" : "frame";
}
}
<html>
<body>
{-template:-}
</body>
<footer>
<a href="mailto:[email protected]">Contact</a>
</footer>
</html>
In the above example, {-template:-} in frame file indicates the placeholder for page content
Template File Rules
- Each template must be an .html, .htm or .js file in project's src/main/resources/templates folder
- Each template file is mapped to a URL, for example file mytemplate.html is mapped to http://myserver.com/view/mytemplate
- Templates can be grouped into subfolders. However, each template name (without file extension) must be globally unique and subfolder names are excluded from URL mapping
- Template engine replaces each {-xyz-} occurence in template with a label from labels file, where xyz is a key in labels file
- Labels files reside in project's resources/labels folder and have a name en.lbl, de.lbl, es.lbl or other similar locale-specific name
- To copy contents of another template into current template, use {-file:myfile.html-} syntax. If referring to files in other folders, don't use folder in file path. For example,
{-file:otherfolder/myfile.html-}is invalid
Session
To store a single piece of information in session, these patterns can be used in a component class:
public Integer get(HttpAccessor htAccessor) {
Integer userId = (Integer) htAccessor.request.getSession().getAttribute("userId"); //fetch userId from session
htAccessor.request.getSession().setAttribute("userId", userId); //save userId in session
}
In a more complex application, multiple attributes have to be stored in session. Then it sometimes makes sense to declare a dedicated session object and register its injector. Injector makes it possible to have that session object as method parameter.
public class MySession { // this class holds session data
public int userId;
public String userFullName;
}
public class SlimwebConfiguration implements ApplicationConfiguration {
@Override
public void registerInjectors(Map<Class<?>, ArgumentInjector> mapInjectors) {
mapInjectors.put(MySession.class, HttpAccessor::getSessionObject); // register MySession injector
}
}
public class MyComponent {
public Integer get(HttpAccessor htAccessor, MySession session) {
if (session == null)
htAccessor.setSessionObject(session = new MySession());// put MySession details into session
return session.userId;
}
}
By default, all components are expected to require session existence. If session does not exist, browser is redirected to login page (declared in SlimwebConfiguration). Some components (like the login page itself) do not require session existence. Then declare the component with @Component(requireSession = false).
Redirecting
Especially after PUT and POST, there is often need to redirect to another view. In a component, use Redirect exception to do that. Slimweb sends an HTTP redirect (303) as a response. However, if client is accepting content-type application/json, Slimweb responds with HTTP 250. This is because application/json request initiator is usually browser javascript and it is unable to catch 303 response.
public void post() {
throw new Redirect("login.html");
}
Server Push
Any component can be made to support server push, when it implements ServerPush interface. For example, here is a component that pushes a message with 2sec delay to a client, which makes a websocket connection to ws://myhost.com/push/my-component:
@Component
public class MyComponent implements ServerPush {
@Override
public void pushStarted(PushHandle pushHandle, Map<String, String> urlParameters) throws Exception {
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
pushHandle.pushObject(new int[] {3, 5});
}).start();
}
@Override
public void pushTerminated(PushHandle pushHandle) throws Exception {
}
}
By default, a component requires session existence or it won't accept websocket connection. @Component(requireSession = false) can be used to suppress session requirement.
Validation
Add server-side validation this way:
@Component
public class MyComponent {
@Validate({V.MANDATORY, V.EMAIL})
public String email = "[email protected]";
@ValidateInput
public MyForm post() {
...
}
public MyForm post2() {
...
}
}
In the example above, Slimweb will validate field email before entering method post. However, validation will not be performed for method post2. Slimweb is using its default validators.
To add custom validators, add a validator class, implementing Validator-interface. If willing to use mixed validators (Slimweb built-in and custom), then extend custom class from ValidatorAdapter:
@Component(validator = MyValidator.class)
public class MyComponent {
}
public class MyValidator extends ValidatorAdapter {
public MyValidator() {
super(MyComponent.class);// class to be validated
}
@Override
public Map<String, String> validate(Object object, Map<String, String> labels) throws Exception {
Map<String, String> errors = super.validate(object, labels);
if (/* field validation logic */)
addError(errors, "myField", labels.get("error.myError"));
return errors;
}
}