Skip to content

Latest commit

 

History

History
319 lines (249 loc) · 11.4 KB

README.md

File metadata and controls

319 lines (249 loc) · 11.4 KB

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:

  1. Application configuration
  2. Component service(s) - (optional)
  3. View template(s) - (optional)
  4. 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

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:someone@example.com">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 = "john.smith@domain.com";

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