All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.tornado.web.engine.Controller Maven / Gradle / Ivy

Go to download

A modern HTML5 Full Stack Web Framework that leverages Java 8 features for expressiveness and beautiful syntax.

The newest version!
package no.tornado.web.engine;

import no.tornado.inject.ApplicationContext;
import no.tornado.web.annotations.Action;
import no.tornado.web.annotations.Database;
import no.tornado.web.annotations.Input;
import no.tornado.web.annotations.Page;
import no.tornado.web.database.Connection;
import no.tornado.web.exceptions.AbortException;
import no.tornado.web.exceptions.HttpException;
import no.tornado.web.exceptions.RedirectException;
import no.tornado.web.html.Form;
import no.tornado.web.html.HtmlSupport;
import no.tornado.web.html.Script;
import no.tornado.web.resources.Content;
import no.tornado.web.resources.Resource;
import no.tornado.web.resources.Template;
import no.tornado.web.tools.ReflectionTools;
import no.tornado.web.tools.Strings;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.log4j.Logger;

import javax.naming.NameNotFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_PERMANENTLY;
import static no.tornado.web.engine.AbstractLifeCycleListener.LIFECYCLE_LISTENER_ATTRIBUTE;

/**
 * 

All page controllers should extend this abstract controller class and implement at least the {@link #execute()} method to provide functionality.

* *

You must also add the {@link no.tornado.web.annotations.Page Page annotation} to make sure the {@link no.tornado.web.processors.PageProcessor PageProcessor} adds a route/path for your page unless you are using it as a {@link no.tornado.web.resources.Content Content controller} only.

* *

If you intend to render Content using a {@link no.tornado.web.resources.Template} you must also add a type parameter representing that template. A simple example of an implemented controller:

* *
 *{@literal @}Page
 * public class MyController extends Controller<MyTemplate> {
 *     public void execute() throws Exception {
 *
 *     }
 * }
 * 
* *

You add template content to the template slots as described in the {@link no.tornado.web.annotations.TemplateFolder} annotation or you can add content directly using the {@link no.tornado.web.html.Element HTML Element} classes:

* *
 * template.maincontent.add(div(p("Hello world")));
 * 
* *

You can also add Freemarker {@link no.tornado.web.resources.Content} the same way:

* *
 * template.maincontent.add(MyArticle.class)
 * 
* *

See {@link no.tornado.web.annotations.Database} for information about how to configure databases, and {@link no.tornado.web.annotations.Input} for how to access input parameters in a typesafe manner.

* * @see no.tornado.web.annotations.Page * @see no.tornado.web.resources.Content * @see no.tornado.web.annotations.ContentFolder * @see no.tornado.web.resources.Template * @see no.tornado.web.annotations.TemplateFolder * * @author edvin * @version $Id: $Id */ @SuppressWarnings({"unchecked", "UnusedDeclaration"}) public abstract class Controller extends Resource implements HtmlSupport { /** Constant log */ protected static final Logger log = Logger.getLogger(Controller.class); protected transient Map args = new HashMap<>(); protected transient HttpServletRequest request; protected transient HttpServletResponse response; protected transient Session session; protected transient T template; protected transient Controller parent; protected Content content; private Long uniqueId; /** *

Called by the Framework to initialize and execute a Controller. This method should not be called by user code.

* * @throws java.lang.Exception an Exception occurs during processing. The Exception is handled and presented to the user via the {@link no.tornado.web.exceptions.ExceptionHandler}. */ public void dispatch() throws Exception { try { init(); execute(); postExecute(); render(); } finally { Conversation.setController(parent); } } /** *

getVersionedControllers.

* * @return a {@link java.util.Map} object. */ public static Map getVersionedControllers() { Map controllers = (Map) Conversation.getRequest().getSession().getAttribute("controllers"); if (controllers == null) { controllers = new HashMap<>(); Conversation.getRequest().getSession().setAttribute("controllers", controllers); } return controllers; } /** *

storeControllerVersionInSession.

*/ protected void storeControllerVersionInSession() { getVersionedControllers().put(getVersionId(), this); } /** *

getVersionId.

* * @return a {@link java.lang.Long} object. */ protected Long getVersionId() { if (uniqueId == null) { AtomicLong versionCounter = (AtomicLong) session.get("versionCounter"); if (versionCounter == null) { versionCounter = new AtomicLong(); session.put("versionCounter", versionCounter); } uniqueId = versionCounter.incrementAndGet(); } return uniqueId; } /** * Execute a public method annotated with @Action in this controller, reachable as page-path:method-name * * @param methodName a {@link java.lang.String} object. * @throws java.lang.Exception if any. */ public void executeControllerAction(String methodName) throws Exception { Method method = getClass().getDeclaredMethod(methodName); if (method.getAnnotation(Action.class) != null) { init(); method.setAccessible(true); method.invoke(this); } } /** *

Sign an object with a per-session key, to avoid XSS attacks when creating links. You can either verify the * signature manually with the {@link #sign(Object)} method, or use the {@link no.tornado.web.annotations.Input Input annotation} * with the signed=true parameter for convenience.

* * @param value an {@link java.lang.Object} to sign, for examle a String, or an Integer * @return a {@link java.lang.String} containing a per-session signature for the value * @throws java.security.NoSuchAlgorithmException if no SHA1 implementation is available in the JVM */ protected String sign(Object value) throws NoSuchAlgorithmException { return Strings.sign(getSignerKey(), value); } /** *

Verify a per-session signature created with the {@link #sign(Object)} method. Normally you don't need to call this * method manually, instead you should use the signed=true parameter of the {@link no.tornado.web.annotations.Input Input annotation} * to create safe HTTP parameters.

* * @param signedValue a {@link java.lang.String} signature created with the {@link #sign(Object)} method * @return the signed {@link java.lang.String} object if the signature was created in the current session, or null if the signature fails * @throws java.security.NoSuchAlgorithmException if no SHA1 implementation is available in the JVM */ protected String verify(String signedValue) throws NoSuchAlgorithmException { return Strings.verify(getSignerKey(), signedValue); } /** *

Create a random, per-session key to use for the {@link #sign(Object)} and {@link #verify(String)} methods. Normally * you don't need to call this method directly.

* * @return a {@link java.lang.String} containing a unique per-session signer-key */ protected String getSignerKey() { String signerKey = (String) session.get("signerKey"); if (signerKey == null) { signerKey = UUID.randomUUID().toString(); session.put("signerKey", signerKey); } return signerKey; } /** *

Called by the framework to facilitate advanced formed functionality like creating a versioned instance * of forms found in the controller, and giving forms names and ids based on the field member name the form is stored in.

* * @throws java.lang.Exception the form field members cannot be manipulated via Reflection or if processing fails */ public void postExecute() throws Exception { for (Field field : getClass().getDeclaredFields()) { if (Form.class.isAssignableFrom(field.getType())) { field.setAccessible(true); Form form = (Form) field.get(this); getLifeCycleListener().processForm(this, form, field.getName()); } } } private LifeCycleListener getLifeCycleListener() { return (LifeCycleListener) Objects.requireNonNull( request.getServletContext().getAttribute(LIFECYCLE_LISTENER_ATTRIBUTE), "No LifeCycleListener configured in Servlet Context"); } /** *

Retrieve a named input parameter as a String.

* * @param key The input parameter name * @return a {@link java.lang.String} object. */ protected String arg(String key) { return (String) args.get(key); } /** *

Bind is called from {@link #init() and processes the {@link no.tornado.web.annotations.Page Page annotation} along with * any annotated field members for resource injection, like Databases, Input parameters, Page parameters and more. * Refer to each annotation for more information.

* * @throws Exception if bind fails * * @see no.tornado.web.annotations.Database * @see no.tornado.web.annotations.Input * @see no.tornado.web.annotations.Page */ private void bind() throws Exception { Page page = getClass().getAnnotation(Page.class); if (page != null) { if (!"".equals(page.filename())) filename(page.filename()); if (response.getContentType() == null) response.setContentType(page.contentType()); } for (Field field : getClass().getDeclaredFields()) { Input input = field.getAnnotation(Input.class); if (input != null) { try { String inputValue = arg(field.getName()); if (input.signed()) inputValue = verify(inputValue); if (inputValue == null && !input.ifnull().equals("#null#")) inputValue = input.ifnull(); if (inputValue != null) { if (!field.isAccessible()) field.setAccessible(true); if (field.getType().equals(String.class)) field.set(this, inputValue); else if (field.getType().equals(Integer.class)) field.set(this, Integer.valueOf(inputValue)); else if (field.getType().equals(Long.class)) field.set(this, Long.valueOf(inputValue)); else if (field.getType().equals(Boolean.class)) field.set(this, "true".equals(inputValue.toLowerCase())); else if (field.getType().equals(Double.class)) field.set(this, Double.valueOf(inputValue)); else if (field.getType().equals(Float.class)) field.set(this, Float.valueOf(inputValue)); else if (field.getType().equals(Date.class) && !"".equals(input.format())) field.set(this, new SimpleDateFormat(input.format()).parse(inputValue)); else if (field.getType().equals(LocalDateTime.class) && !"".equals(input.format())) field.set(this, LocalDateTime.parse(inputValue, DateTimeFormatter.ofPattern(input.format()))); else if (field.getType().equals(LocalTime.class) && !"".equals(input.format())) field.set(this, LocalTime.parse(inputValue, DateTimeFormatter.ofPattern(input.format()))); else if (field.getType().equals(LocalDate.class) && !"".equals(input.format())) field.set(this, LocalDate.parse(inputValue, DateTimeFormatter.ofPattern(input.format()))); } } catch (Exception ex) { log.error("Failed to bind @Input for field " + field.getName(), ex); } } Database database = field.getAnnotation(Database.class); if (database != null) { String name; if (!"".equals(database.name())) name = database.name(); else if (!"".equals(database.value())) name = database.value(); else name = field.getName(); Object connection = null; try { if (field.getType().equals(Connection.class)) { Connection tc = new Connection(name); tc.getDelegate().setAutoCommit(database.autocommit()); connection = tc; } else if (field.getType().equals(java.sql.Connection.class)) { connection = Connection.getRawConnection(name); } } catch (NameNotFoundException ex) { throw new IllegalArgumentException(getClass().getName() + " tried to bind non-existent database " + name + " to field " + field.getName(), ex); } if (connection != null) { field.setAccessible(true); field.set(this, connection); } } } } /** *

Convenience method to override the Content-Disposition HTTP header.

* *

You can configure a Content-Disposition of type attachment directly by using the {@link no.tornado.web.annotations.Page#filename() filename} method in the {@link no.tornado.web.annotations.Page} annotation on the controller.

* *

Use this in conjunction with the {@link no.tornado.web.annotations.Page#contentType()} parameter or by setting the response header Content-Type programatically.

* @param filename a filename to present to the user as a download link */ public void filename(String filename) { response.setHeader("Content-Disposition", "attachment; filename=\"" + filename.replace("\"", "-") + "\""); } /** *

Render all content added to the {@link no.tornado.web.resources.Template Template slots} of the controller instance.

* *

This method is called by the framework and should under normal circumstances never be called by the user.

* * @throws java.lang.Exception if an exception occurs during rendering */ public void render() throws Exception { if (template != null) { String result = template.render(); if (result != null) response.getOutputStream().write(result.getBytes(UTF_8)); } } /** *

Initialization of the controller. This is done automatically by the framework before a controller is executed and rendered.

* *

This step connects the controller to it's parent, binds request, response, session and args parameters and instantiates the {@link no.tornado.web.resources.Template} * configured as a type parameter to the Controller.

* *

The contentType found in the {@literal @}Page annotation is applied to the response. You can override this programmatically by calling * {@link javax.servlet.http.HttpServletResponse#setContentType(String) response#setContentType()}.

* * @throws Exception if initialization fails */ public void init() throws Exception { setParent(Conversation.getController()); Conversation.setController(this); ApplicationContext.inject(this); this.request = Conversation.getRequest(); this.response = Conversation.getResponse(); this.session = new Session(request); extractArgs(); instantiateTemplate(); bind(); } private void instantiateTemplate() { if (template == null) { List> controllerTypeArguments = ReflectionTools.getTypeArguments(Controller.class, getClass()); if (!controllerTypeArguments.isEmpty()) { try { Class templateClass = controllerTypeArguments.get(0); if (templateClass != null) template = (T) templateClass.newInstance(); } catch (Exception ex) { log.error("Failed to instantiate template for " + getClass(), ex); } } } } @SuppressWarnings("ConstantConditions") private void extractArgs() throws FileUploadException { args.clear(); if (ServletFileUpload.isMultipartContent(Conversation.getRequest())) { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setRepository((File) request.getServletContext().getAttribute("javax.servlet.context.tempdir")); ServletFileUpload upload = new ServletFileUpload(factory); List items = upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { args.put(item.getFieldName(), item.getString()); } else { if (args.get(item.getFieldName()) instanceof FileItem) { FileItem existing = (FileItem) args.get(item.getFieldName()); List fileitems = new LinkedList<>(); fileitems.add(existing); fileitems.add(item); args.put(item.getFieldName(), fileitems); } else if (args.get(item.getFieldName()) instanceof List) { List fileitems = (List) args.get(item.getFieldName()); fileitems.add(item); } else { args.put(item.getFieldName(), item); } } } } else { Map p = request.getParameterMap(); for (String key : p.keySet()) { Object value = p.get(key); if (String[].class.isInstance(value)) { String[] avalue = (String[]) value; if (avalue.length > 0) args.put(key, avalue[0]); } else if (value instanceof String) { args.put(key, value); } } } } /** *

Implement this method to provide functionality to your controller. You should either add content to the template or * call {@link #echo(Object...)} or {@link #die()} from this method along with any other processing you want to perform.

* * @throws java.lang.Exception if your code throws any exceptions */ public abstract void execute() throws Exception; /** *

Send a HTTP 302 Location header with the desired url and stop further execution.

* * @param url An url to redirect to with a 302 temporary location header */ public void redirect(String url) { throw new RedirectException(url); } /** *

Send a HTTP 301 Location header with the desired url and stop further execution.

* * @param url An url to redirect to with a 301 permanent location header */ public void redirectPermanent(String url) { throw new RedirectException(url, SC_MOVED_PERMANENTLY); } /** *

Send a HTTP 302 Location header with the url of the given {@link no.tornado.web.engine.Controller Controller} in the current language and stop further execution.

* * @param controllerClass A class that extends {@link no.tornado.web.engine.Controller} that you want to redirect to * @throws Exception that is handled by the framework to provide the actual Location header */ public void redirect(Class controllerClass) throws Exception { throw new RedirectException(getPath(controllerClass)); } /** *

Send a HTTP 301 Location header with the url of the given {@link no.tornado.web.engine.Controller Controller} in the current language and stop further execution.

* * @param controllerClass A class that extends {@link no.tornado.web.engine.Controller} that you want to redirect to * @throws Exception that is handled by the framework to provide the actual Location header */ public void redirectPermanent(Class controllerClass) throws Exception { throw new RedirectException(getControllerFor(controllerClass).getPath(), SC_MOVED_PERMANENTLY); } /** *

Send a HTTP 302 Location header with the url of the given {@link no.tornado.web.engine.Controller Controller} in the current language plus additional arguments and stop further execution.

* * @param args The extra arguments you want to add to the resolved url * @param controllerClass A class that extends {@link no.tornado.web.engine.Controller} that you want to redirect to * @throws Exception that is handled by the framework to provide the actual Location header */ public void redirect(Class controllerClass, String args) throws Exception { throw new RedirectException(getControllerFor(controllerClass).getPath() + "?" + args); } /** *

Send a HTTP 301 Location header with the url of the given {@link no.tornado.web.engine.Controller Controller} in the current language plus additional arguments and stop further execution.

* * @param args The extra arguments you want to add to the resolved url * @param controllerClass A class that extends {@link no.tornado.web.engine.Controller} that you want to redirect to * @throws Exception that is handled by the framework to provide the actual Location header */ public void redirectPermanent(Class controllerClass, String args) throws Exception { throw new RedirectException(getControllerFor(controllerClass).getPath() + "?" + args, SC_MOVED_PERMANENTLY); } /** *

Instantiate and initialize a controller with the given class in the current language.

* * @param controllerClass the controller class you want to instantiate * @return an initialized {@link no.tornado.web.engine.Controller} object * @throws java.lang.Exception if instantiation or initialization fails */ public Controller getControllerFor(Class controllerClass) throws Exception { Controller controller = controllerClass.newInstance(); controller.init(); return controller; } /** *

Get the path of the current controller in the current language.

* * @return a {@link java.lang.String} representing the path of this controller in the current language */ public String getPath() { return getPath(getClass()); } /** *

Get the path of the given controller class in the current language.

* * @param controllerClass the controller class to find the path for * @return a {@link java.lang.String} representing the path of the given controller class in the current language */ public static String getPath(Class controllerClass) { ResourceBundle bundle = Resource.loadBundle(controllerClass.getName()); if (bundle.containsKey("path")) return bundle.getString("path"); Page page = controllerClass.getAnnotation(Page.class); if (page != null && !"".equals(page.path())) return page.path(); throw new IllegalStateException(controllerClass + " has no path for language " + Conversation.getLookupLocale()); } /** *

Stop further execution and close the response stream. This does not change the HTTP Response Code

*/ public void die() { throw new AbortException(); } /** *

Stop further execution after rendering the given message to the user and close the response stream, adding the given HTTP Response Code

* * @param statusCode a {@link java.lang.Integer} representing the response code. * @param message a {@link java.lang.Object} message to be rendered to the user. */ public void die(Integer statusCode, Object message) { throw new HttpException(statusCode, message == null ? null : message.toString()); } /** *

Stop further execution after rendering the given message to the user and close the response stream.

* * @param message a {@link java.lang.Object} message to be rendered to the user. * @throws Exception that is handled by the framework to close the ongoing Conversation */ public void die(Object message) throws Exception { echo(message); die(); } /** *

Echo output directly to the response outputstream.

* *

Known objects like Classes that extends {@link no.tornado.web.resources.Content} or Content instances are unwrapped before written out.

* *

Throwables are printed as the complete stacktrace.

* * @param output varargs list of output objects * @throws Exception if rendering of any object fails */ public void echo(Object... output) throws Exception { for (Object o : output) { if (o instanceof Content) { echo(((Content) o).render()); } else if (o instanceof Class) { echo(((Class)o).newInstance()); } else if (o instanceof Class[]) { for (Class c : (Class[]) o) echo(c.newInstance()); } else if (o != null) { String content = o instanceof Throwable ? Strings.fromException((Throwable) o) : o.toString(); response.getOutputStream().write(content.getBytes(UTF_8)); } } } /** *

Get the HTTP URL parameters as a Map.

* * @return a {@link java.util.Map} with the given HTTP URL parameters. * * @see no.tornado.web.engine.Controller#get(String) */ public Map getArgs() { return args; } /** *

Return the current {@link javax.servlet.http.HttpServletRequest}

* * @return a {@link javax.servlet.http.HttpServletRequest} for the ongoing http conversation */ public HttpServletRequest getRequest() { return request; } /** *

Return the current {@link javax.servlet.http.HttpServletResponse}

* * @return a {@link javax.servlet.http.HttpServletResponse} for the ongoing http conversation */ public HttpServletResponse getResponse() { return response; } /** *

Get a wrapper for the current session. This is mainly a Freemarker support and a convenience, you can also access Request attributes directly via {@link #getRequest()} and it's {@link javax.servlet.http.HttpServletRequest#getSession()}.

* * @return a {@link no.tornado.web.engine.Session} object. */ public Session getSession() { return session; } /** *

Return the template for this controller, as configured in the Controller type parameter.

* * @return a {@link no.tornado.web.resources.Template} object. */ public Template getTemplate() { return template; } /** *

Returns the parent controller, if this Controller belongs to a {@link no.tornado.web.resources.Content Content fragment}, null otherwise.

* * @return the parent {@link no.tornado.web.engine.Controller} or null of this is the outermost controller */ public Controller getParent() { return parent; } /** *

Sets the parent controller. This is called by the framework if a controller is assigned to a {@link no.tornado.web.resources.Content Content fragment}. Normally this method should not be called by the user.

* * @param parent a {@link no.tornado.web.engine.Controller} to act as this controller's parent. */ public void setParent(Controller parent) { if (parent == this) throw new IllegalArgumentException("A controller cannot have itself as its parent."); this.parent = parent; } /** *

Returns the root of the Controller hierarchy. The root controller will return itself as it's root.

* * @return a {@link no.tornado.web.engine.Controller} representing the outer most controller (the one representing the current url path) */ public Controller getRoot() { return parent == null ? this : parent.getRoot(); } /** *

Method for accessing the current {@link no.tornado.web.engine.Conversation}. Useful from Freemarker and for convenience inside a controller.

* * @return the ongoing {@link no.tornado.web.engine.Conversation} */ public Conversation getConversation() { return Conversation.INSTANCE; } /** *

Method for accessing the {@link no.tornado.web.engine.SiteConfig} singelton. Useful from Freemarker and for convenience inside a controller.

* * @return the singleton {@link no.tornado.web.engine.SiteConfig} */ public SiteConfig getSiteconfig() { return SiteConfig.INSTANCE; } /** *

Helper method for letting Freemarker templates access the its Java representative as ${this}.

* *

When you add a resource bundle property to a a corresponding Template, the {@link no.tornado.web.processors.ContentProcessor ContentProcessor} * will add properties for the resource key to the Java class representing the Template. These can then be accessed as ${this.propname} in the Freemarker templates.

* * @return the {@link no.tornado.web.resources.Content} object representing the current Content being rendered. This also applies to {@link no.tornado.web.resources.Template Templates}. */ public Content getThis() { return content; } /** *

Framework method for setting the this scope for Freemarker.

* * @param content the Java correspondent for the current {@link no.tornado.web.resources.Content} being rendered */ public void setThis(Content content) { this.content = content; } /** *

Accessor for the Page title. The resource bundle is first consulted, and in case of failer, the title is retrieved from the {@literal @}Page annotation.

* * @return a {@link java.lang.String} title from the resource bundle for this {@link no.tornado.web.engine.Controller}. */ public String getTitle() { String title = getString("title", ""); if (title.equals("")) { Page page = getClass().getAnnotation(Page.class); if (page != null) return page.title(); } return title; } /** *

When using advanced {@link no.tornado.web.html.Form Forms code}, you need to inject a JavaScript for added functionality. This getter makes it easy to add this Javascript reference to your page or template:

* * template.head.add(formsScript()); * * *

You also need to add the {@link #jqueryScript()} or your own custom version of JQuery in the same way.

* * @return a {@link no.tornado.web.html.Script} object representing the tornado.forms.js script. */ public Script formsScript() { return script().src(getRequest().getContextPath() + "/tornado.forms.js"); } /** *

When using advanced {@link no.tornado.web.html.Form Forms code}, you need to inject a JQuery for added functionality. This getter makes it easy to add this Javascript reference to your page or template:

* * template.head.add(jqueryScript()); * * *

In most cases you also need to add the {@link #formsScript()} in the same way.

* * @return a {@link no.tornado.web.html.Script} object representing a hosted version of JQuery latest. */ public Script jqueryScript() { return script().src(request.getScheme() + "://code.jquery.com/jquery-latest.min.js"); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy