no.tornado.web.engine.Controller Maven / Gradle / Ivy
Show all versions of web Show documentation
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 extends Controller> 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 extends Controller> 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 extends Controller> 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 extends Controller> 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 extends Controller> 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 extends Controller> 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");
}
}