blob: 523ccc81a5856addc1e86e8b5ec019b5405ab373 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.click;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.click.element.Element;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.Format;
import org.apache.click.util.HtmlStringBuffer;
import org.apache.click.util.PageImports;
import org.apache.commons.lang.StringUtils;
/**
* Provides the Page request event handler class.
* <p/>
* The Page class plays a central role in Click applications defining how the
* application's pages are processed and rendered. All application pages
* must extend the base Page class, and provide a no arguments constructor.
*
* <h4>Page Execution Sequence</h4>
*
* The default Page execution path for a GET request is:
* <ol>
* <li class="spaced">
* no-args constructor invoked to create a new Page instance.
* At this point no dependencies have been injected into the Page, and any
* request information is not available. You should put any "static"
* page initialization code, which doesn't depend upon request information,
* in the constructor. This will enable subclasses to have this code
* automatically initialized when they are created.
* </li>
* <li class="spaced">
* {@link #format} property is set
* </li>
* <li class="spaced">
* {@link #headers} property is set
* </li>
* <li class="spaced">
* {@link #path} property is set
* </li>
* <li class="spaced">
* {@link #onSecurityCheck()} method called to check whether the page should
* be processed. This method should return true if the Page should continue
* to be processed, or false otherwise.
* </li>
* <li class="spaced">
* {@link #onInit()} method called to complete the initialization of the page
* after all the dependencies have been set. This is where you should put
* any "dynamic" page initialization code which depends upon the request or any
* other dependencies.
* <p/>
* Form and field controls must be fully initialized by the time this method
* has completed.
* </li>
* <li class="spaced">
* ClickServlet processes all the page {@link #controls}
* calling their {@link Control#onProcess()} method. If any of these
* controls return false, continued control and page processing will be aborted.
* </li>
* <li class="spaced">
* {@link #onGet()} method called for any additional GET related processing.
* <p/>
* Form and field controls should <b>NOT</b> be created or initialized at this
* point as the control processing stage has already been completed.
* </li>
* <li class="spaced">
* {@link #onRender()} method called for any pre-render processing. This
* method is often use to perform database queries to load information for
* rendering tables.
* <p/>
* Form and field controls should <b>NOT</b> be created or initialized at this
* point as the control processing stage has already been completed.
* </li>
* <li class="spaced">
* ClickServlet renders the page merging the {@link #model} with the
* Velocity template defined by the {@link #getTemplate()} property.
* </li>
* <li class="spaced">
* {@link #onDestroy()} method called to clean up any resources. This method
* is guaranteed to be called, even if an exception occurs. You can use
* this method to close resources like database connections or Hibernate
* sessions.
* </li>
* </ol>
*
* For POST requests the default execution path is identical, except the
* {@link #onPost()} method is called instead of {@link #onGet()}. The POST
* request page execution sequence is illustrated below:
* <p/>
* <img src="post-sequence-diagram.png"/>
*
* <p/>
* A good way to see the page event execution order is to view the log when
* the application mode is set to <tt>trace</tt>:
*
* <pre class="codeConfig" style="padding:1em;background-color:#f0f0f0;">
* [Click] [debug] GET http://localhost:8080/quickstart/home.htm
* [Click] [trace] invoked: HomePage.&lt;&lt;init&gt;&gt;
* [Click] [trace] invoked: HomePage.onSecurityCheck() : true
* [Click] [trace] invoked: HomePage.onInit()
* [Click] [trace] invoked: HomePage.onGet()
* [Click] [trace] invoked: HomePage.onRender()
* [Click] [info ] renderTemplate: /home.htm - 6 ms
* [Click] [trace] invoked: HomePage.onDestroy()
* [Click] [info ] handleRequest: /home.htm - 24 ms </pre>
*
* <h4>Rendering Pages</h4>
*
* When a Velocity template is rendered the ClickServlet uses Pages:
* <ul>
* <li>{@link #getTemplate()} to find the Velocity template.</li>
* <li>{@link #model} to populate the Velocity Context</li>
* <li>{@link #format} to add to the Velocity Context</li>
* <li>{@link #getContentType()} to set as the HttpServletResponse content type</li>
* <li>{@link #getContentType()} to set as the HttpServletResponse content type</li>
* <li>{@link #headers} to set as the HttpServletResponse headers</li>
* </ul>
*
* These Page properties are also used when rendering JSP pages.
*/
public class Page implements Serializable {
private static final long serialVersionUID = 1L;
/**
* The global page messages bundle name: &nbsp; <tt>click-page</tt>.
*/
public static final String PAGE_MESSAGES = "click-page";
/**
* The Page action request parameter: &nbsp; "<tt>pageAction</tt>".
*/
public final static String PAGE_ACTION = "pageAction";
// Instance Variables -----------------------------------------------------
/** The list of page controls. */
protected List<Control> controls;
/**
* The list of page HTML HEAD elements including: Javascript imports,
* Css imports, inline Javascript and inline Css.
*/
protected List<Element> headElements;
/** The Velocity template formatter object. */
protected Format format;
/** The forward path. */
protected String forward;
/** The HTTP response headers. */
protected Map<String, Object> headers;
/** The map of localized page resource messages. **/
protected transient Map<String, String> messages;
/**
* The page model. For Velocity templates the model is used to populate the
* Velocity context. For JSP pages the model values are set as named
* request attributes.
*/
protected Map<String, Object> model = new HashMap<String, Object>();
/** The Page header imports. */
protected transient PageImports pageImports;
/** The path of the page template to render. */
protected String path;
/** The redirect path. */
protected String redirect;
/**
* The page is stateful and should be saved to the users HttpSession
* between requests, default value is false.
*
* @deprecated stateful pages are not supported anymore, use stateful
* Controls instead
*/
protected boolean stateful;
/** The path of the page border template to render.*/
protected String template;
/**
* Indicates whether Control head elements should be included in the
* page template, default value is true.
*/
protected boolean includeControlHeadElements = true;
// Event Handlers ---------------------------------------------------------
/**
* The on Security Check event handler. This event handler is invoked after
* the pages constructor has been called and all the page properties have
* been set.
* <p/>
* Security check provides the Page an opportunity to check the users
* security credentials before processing the Page.
* <p/>
* If security check returns true the Page is processed as
* normal. If the method returns then no other event handlers are invoked
* (except <tt>onDestroy()</tt> and no page controls are processed.
* <p/>
* If the method returns false, the forward or redirect property should be
* set to send the request to another Page.
* <p/>
* By default this method returns true, subclass may override this method
* to provide their security authorization/authentication mechanism.
*
* @return true by default, subclasses may override this method
*/
public boolean onSecurityCheck() {
return true;
}
/**
* The on Initialization event handler. This event handler is invoked after
* the {@link #onInit()} method has been called.
* <p/>
* Subclasses should place any initialization code which has dependencies
* on the context or other properties in this method. Generally light
* weight initialization code should be placed in the Pages constructor.
* <p/>
* Time consuming operations such as fetching the results of a database
* query should not be placed in this method. These operations should be
* performed in the {@link #onRender()}, {@link #onGet()} or
* {@link #onPost()} methods so that other event handlers may take
* alternative execution paths without performing these expensive operations.
* <p/>
* <b>Please Note</b> however the qualifier for the previous statement is
* that all form and field controls must be fully initialized before they
* are processed, which is after the <tt>onInit()</tt> method has
* completed. After this point their <tt>onProcess()</tt> methods will be
* invoked by the <tt>ClickServlet</tt>.
* <p/>
* Select controls in particular must have their option list values populated
* before the form is processed otherwise field validations cannot be performed.
* <p/>
* For initializing page controls the best practice is to place all the
* control creation code in the pages constructor, and only place any
* initialization code in the <tt>onInit()</tt> method which has an external
* dependency to the context or some other object. By following this practice
* it is easy to see what code is "design time" initialization code and what
* is "runtime initialization code".
* <p/>
* When subclassing pages which also use the <tt>onInit()</tt> method is
* is critical you call the <tt>super.onInit()</tt> method first, for
* example:
* <pre class="javaCode">
* <span class="kw">public void</span> onInit() {
* <span class="kw">super</span>.onInit();
*
* // Initialization code
* ..
* } </pre>
*/
public void onInit() {
}
/**
* The on Get request event handler. This event handler is invoked if the
* HTTP request method is "GET".
* <p/>
* The event handler is invoked after {@link #onSecurityCheck()} has been
* called and all the Page {@link #controls} have been processed. If either
* the security check or one of the controls cancels continued event
* processing the <tt>onGet()</tt> method will not be invoked.
*
* <h4>Important Note</h4>
*
* Form and field controls should <b>NOT</b> be created
* or initialized at this point as the control processing stage has already
* been completed. Select option list values should also be populated
* before the control processing stage is performed so that they can
* validate the submitted values.
*/
public void onGet() {
}
/**
* The on Post request event handler. This event handler is invoked if the
* HTTP request method is "POST".
* <p/>
* The event handler is invoked after {@link #onSecurityCheck()} has been
* called and all the Page {@link #controls} have been processed. If either
* the security check or one of the controls cancels continued event
* processing the <tt>onPost()</tt> method will not be invoked.
*
* <h4>Important Note</h4>
*
* Form and field controls should <b>NOT</b> be created
* or initialized at this point as the control processing stage has already
* been completed. Select option list values should also be populated
* before the control processing stage is performed so that they can
* validate the submitted values.
*/
public void onPost() {
}
/**
* The on render event handler. This event handler is invoked prior to the
* page being rendered.
* <p/>
* This method will not be invoked if either the security check or one of
* the controls cancels continued event processing.
* <p/>
* The on render method is typically used to populate tables performing some
* database intensive operation. By putting the intensive operations in the
* on render method they will not be performed if the user navigates away
* to a different page.
* <p/>
* If you have code which you are using in both the <tt>onGet()</tt> and
* <tt>onPost()</tt> methods, use the <tt>onRender()</tt> method instead.
*
* <h4>Important Note</h4>
*
* Form and field controls should <b>NOT</b> be created
* or initialized at this point as the control processing stage has already
* been completed. Select option list values should also be populated
* before the control processing stage is performed so that they can
* validate the submitted values.
*/
public void onRender() {
}
/**
* The on Destroy request event handler. Subclasses may override this method
* to add any resource clean up code.
* <p/>
* This method is guaranteed to be called before the Page object reference
* goes out of scope and is available for garbage collection.
*/
public void onDestroy() {
}
// Public Methods ---------------------------------------------------------
/**
* Add the control to the page. The control will be added to the page model
* using the control name as the key. The Controls parent property will
* also be set to the page instance.
* <p/>
* <b>Please note</b>: if the page contains a control with the same name as
* the given control, that control will be replaced by the given control.
* If a control has no name defined it cannot be replaced.
*
* @param control the control to add to the page
* @throws IllegalArgumentException if the control is null or if the name
* of the control is not defined
*/
public void addControl(Control control) {
if (control == null) {
throw new IllegalArgumentException("Null control parameter");
}
if (StringUtils.isBlank(control.getName())) {
throw new IllegalArgumentException("Control name not defined: "
+ control.getClass());
}
// Check if page already contains a named value
Object currentValue = getModel().get(control.getName());
if (currentValue != null && currentValue instanceof Control) {
Control currentControl = (Control) currentValue;
replaceControl(currentControl, control);
return;
}
// Note: set parent first as setParent might veto further processing
control.setParent(this);
getControls().add(control);
addModel(control.getName(), control);
}
/**
* Remove the control from the page. The control will be removed from the
* pages model and the control parent property will be set to null.
*
* @param control the control to remove
* @throws IllegalArgumentException if the control is null, or if the name
* of the control is not defined
*/
public void removeControl(Control control) {
if (control == null) {
throw new IllegalArgumentException("Null control parameter");
}
if (StringUtils.isBlank(control.getName())) {
throw new IllegalArgumentException("Control name not defined");
}
getControls().remove(control);
getModel().remove(control.getName());
control.setParent(null);
}
/**
* Return the list of page Controls.
*
* @return the list of page Controls
*/
public List<Control> getControls() {
if (controls == null) {
controls = new ArrayList<Control>();
}
return controls;
}
/**
* Return true if the page has any controls defined.
*
* @return true if the page has any controls defined
*/
public boolean hasControls() {
return (controls != null) && !controls.isEmpty();
}
/**
* Return the request context of the page.
*
* @return the request context of the page
*/
public Context getContext() {
return Context.getThreadLocalContext();
}
/**
* Return the HTTP response character encoding. By default this method returns
* the request character encoding via
* {@link javax.servlet.ServletRequest#getCharacterEncoding()}
* <p/>
* The ClickServlet uses the pages character encoding for setting the
* HttpServletResponse character encoding.
*
* @return the HTTP response content type
*/
public String getCharacterEncoding() {
return getContext().getRequest().getCharacterEncoding();
}
/**
* Return the HTTP response content type. By default this method returns
* <tt>"text/html"</tt>.
* <p/>
* The ClickServlet uses the pages content type for setting the
* HttpServletResponse content type.
*
* @return the HTTP response content type
*/
public String getContentType() {
return "text/html";
}
/**
* Return the Velocity template formatter object.
* <p/>
* The ClickServlet adds the format object to the Velocity context using
* the key <tt>"format"</tt> so that it can be used in the page template.
*
* @return the Velocity template formatter object
*/
public Format getFormat() {
return format;
}
/**
* Set the Velocity template formatter object.
*
* @param value the Velocity template formatter object.
*/
public void setFormat(Format value) {
format = value;
}
/**
* Return the path to forward the request to.
* <p/>
* If the {@link #forward} property is not null it will be used to forward
* the request to in preference to rendering the template defined by the
* {@link #path} property. The request is forwarded using the
* RequestDispatcher.
* <p/>
* See also {@link #getPath()}, {@link #getRedirect()}
*
* @return the path to forward the request to
*/
public String getForward() {
return forward;
}
/**
* Set the path to forward the request to.
* <p/>
* If the {@link #forward} property is not null it will be used to forward
* the request to in preference to rendering the template defined by the
* {@link #path} property. The request is forwarded using the Servlet
* RequestDispatcher.
* <p/>
* If forward paths start with a <span class="wr">"/"</span>
* character the forward path is
* relative to web applications root context, otherwise the path is
* relative to the requests current location.
* <p/>
* For example given a web application deployed to context <tt>mycorp</tt>
* with the pages:
* <pre class="codeConfig" style="color:navy">
* /index.htm
* /customer/search.htm
* /customer/details.htm
* /customer/management/add-customer.htm </pre>
*
* To forward to the customer <tt class="wr">search.htm</tt> page from
* the web app root you could set forward as
* <tt>setForward(<span class="navy">"/customer/search.htm"</span>)</tt>
* or <tt>setForward(<span class="navy">"customer/search.htm"</span>)</tt>.
* <p/>
* If a user was currently viewing the <tt class="wr">add-customer.htm</tt>
* to forward to customer <span class="wr">details.htm</span> you could
* set forward as
* <tt>setForward(<span class="navy">"/customer/details.htm"</span>)</tt>
* or <tt>setForward(<span class="navy">"../details.htm"</span>)</tt>.
* <p/>
* See also {@link #setPath(String)}, {@link #setRedirect(String)}
*
* @param value the path to forward the request to
*/
public void setForward(String value) {
forward = value;
}
/**
* The Page instance to forward the request to. The given Page object
* must have a valid {@link #path} defined, as the {@link #path} specifies
* the location to forward to.
*
* @see #setForward(java.lang.String)
*
* @param page the Page object to forward the request to.
*/
public void setForward(Page page) {
if (page == null) {
throw new IllegalArgumentException("Null page parameter");
}
if (page.getPath() == null) {
throw new IllegalArgumentException("Page has no path defined");
}
setForward(page.getPath());
getContext().setRequestAttribute(ClickServlet.FORWARD_PAGE, page);
}
/**
* Set the request to forward to the given page class.
*
* @see #setForward(java.lang.String)
*
* @param pageClass the class of the Page to forward the request to
* @throws IllegalArgumentException if the Page Class is not configured
* with a unique path
*/
public void setForward(Class<? extends Page> pageClass) {
String target = getContext().getPagePath(pageClass);
// If page class maps to a jsp, convert to htm which allows ClickServlet
// to process the redirect
if (target != null && target.endsWith(".jsp")) {
target = StringUtils.replaceOnce(target, ".jsp", ".htm");
}
setForward(target);
}
/**
* Return the map of HTTP header to be set in the HttpServletResponse.
*
* @return the map of HTTP header to be set in the HttpServletResponse
*/
public Map<String, Object> getHeaders() {
if (headers == null) {
headers = new HashMap<String, Object>();
}
return headers;
}
/**
* Return true if the page has headers, false otherwise.
*
* @return true if the page has headers, false otherwise
*/
public boolean hasHeaders() {
return headers != null && !headers.isEmpty();
}
/**
* Set the named header with the given value. Value can be either a String,
* Date or Integer.
*
* @param name the name of the header
* @param value the value of the header, either a String, Date or Integer
*/
public void setHeader(String name, Object value) {
if (name == null) {
throw new IllegalArgumentException("Null header name parameter");
}
getHeaders().put(name, value);
}
/**
* Set the map of HTTP header to be set in the HttpServletResponse.
*
* @param value the map of HTTP header to be set in the HttpServletResponse
*/
public void setHeaders(Map<String, Object> value) {
headers = value;
}
/**
* @deprecated use the new {@link #getHeadElements()} instead
*
* @return the HTML includes statements for the control stylesheet and
* JavaScript files
*/
public final String getHtmlImports() {
throw new UnsupportedOperationException("Use getHeadElements instead");
}
/**
* Return the list of HEAD {@link org.apache.click.element.Element elements}
* to be included in the page. Example HEAD elements include
* {@link org.apache.click.element.JsImport JsImport},
* {@link org.apache.click.element.JsScript JsScript},
* {@link org.apache.click.element.CssImport CssImport} and
* {@link org.apache.click.element.CssStyle CssStyle}.
* <p/>
* Pages can contribute their own list of HEAD elements by overriding
* this method.
* <p/>
* The recommended approach when overriding this method is to use
* <tt>lazy loading</tt> to ensure the HEAD elements are only added
* <tt>once</tt> and when <tt>needed</tt>. For example:
*
* <pre class="prettyprint">
* public MyPage extends Page {
*
* public List getHeadElements() {
* // Use lazy loading to ensure the JS is only added the
* // first time this method is called.
* if (headElements == null) {
* // Get the head elements from the super implementation
* headElements = super.getHeadElements();
*
* // Include the page's external Javascript resource
* JsImport jsImport = new JsImport("/mycorp/js/mypage.js");
* headElements.add(jsImport);
*
* // Include the page's external Css resource
* CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
* headElements.add(cssImport);
* }
* return headElements;
* }
* } </pre>
*
* Alternatively one can add the HEAD elements in the Page constructor:
*
* <pre class="prettyprint">
* public MyPage extends Page {
*
* public MyPage() {
* JsImport jsImport = new JsImport("/mycorp/js/mypage.js");
* getHeadElements().add(jsImport);
*
* CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
* getHeadElements().add(cssImport);
* }
* } </pre>
*
* One can also add HEAD elements from event handler methods such as
* {@link #onInit()}, {@link #onGet()}, {@link #onPost()}, {@link #onRender()}
* etc.
* <p/>
* The order in which JS and CSS files are included will be preserved in the
* page.
* <p/>
* <b>Note:</b> this method must never return null. If no HEAD elements
* are available this method must return an empty {@link java.util.List}.
* <p/>
* <b>Also note:</b> a common problem when overriding getHeadElements in
* subclasses is forgetting to call <em>super.getHeadElements</em>. Consider
* carefully whether you should call <em>super.getHeadElements</em> or not.
*
* @return the list of HEAD elements to be included in the page
*/
public List<Element> getHeadElements() {
if (headElements == null) {
headElements = new ArrayList<Element>(2);
}
return headElements;
}
/**
* Return the localized Page resource message for the given resource
* name or null if not found. The resource message returned will use the
* Locale obtained from the Context.
* <p/>
* Pages can define text properties files to store localized messages. These
* properties files must be stored on the Page class path with a name
* matching the class name. For example:
* <p/>
* The page class:
* <pre class="codeJava">
* <span class="kw">package</span> com.mycorp.pages;
*
* <span class="kw">public class</span> Login <span class="kw">extends</span> Page {
* .. </pre>
*
* The page class property filenames and their path:
* <pre class="codeConfig">
* /com/mycorp/pages/Login.properties
* /com/mycorp/pages/Login_en.properties
* /com/mycorp/pages/Login_fr.properties </pre>
*
* Page messages can also be defined in the optional global messages
* bundle:
*
* <pre class="codeConfig">
* /click-page.properties </pre>
*
* To define global page messages simply add <tt>click-page.properties</tt>
* file to your application's class path. Message defined in this properties
* file will be available to all of your application pages.
* <p/>
* Note messages in your page class properties file will override any
* messages in the global <tt>click-page.properties</tt> file.
* <p/>
* Page messages can be accessed directly in the page template using
* the <span class="st">$messages</span> reference. For examples:
*
* <pre class="codeHtml">
* <span class="blue">$messages.title</span> </pre>
*
* Please see the {@link org.apache.click.util.MessagesMap} adaptor for more
* details.
*
* @param name resource name of the message
* @return the named localized message for the page or null if no message
* was found
*/
public String getMessage(String name) {
if (name == null) {
throw new IllegalArgumentException("Null name parameter");
}
return getMessages().get(name);
}
/**
* Return the formatted page message for the given resource name and
* message format arguments or null if no message was found. The resource
* message returned will use the Locale obtained from the Context.
* <p/>
* {@link #getMessage(java.lang.String)} is invoked to retrieve the message
* for the specified name.
*
* @param name resource name of the message
* @param args the message arguments to format
* @return the named localized message for the page or null if no message
* was found
*/
public String getMessage(String name, Object... args) {
String value = getMessage(name);
return MessageFormat.format(value, args);
}
/**
* Return a Map of localized messages for the Page. The messages returned
* will use the Locale obtained from the Context.
*
* @see #getMessage(String)
*
* @return a Map of localized messages for the Page
* @throws IllegalStateException if the context for the Page has not be set
*/
public Map<String, String> getMessages() {
if (messages == null) {
if (getContext() != null) {
messages = getContext().createMessagesMap(getClass(), PAGE_MESSAGES);
} else {
String msg = "Context not set cannot initialize messages";
throw new IllegalStateException(msg);
}
}
return messages;
}
/**
* Add the named object value to the Pages model map.
* <p/>
* <b>Please note</b>: if the Page contains an object with a matching name,
* that object will be replaced by the given value.
*
* @param name the key name of the object to add
* @param value the object to add
* @throws IllegalArgumentException if the name or value parameters are
* null
*/
public void addModel(String name, Object value) {
if (name == null) {
String msg = "Cannot add null parameter name to "
+ getClass().getName() + " model";
throw new IllegalArgumentException(msg);
}
if (value == null) {
String msg = "Cannot add null " + name + " parameter "
+ "to " + getClass().getName() + " model";
throw new IllegalArgumentException(msg);
}
getModel().put(name, value);
}
/**
* Return the Page's model map. The model is used populate the
* Velocity Context with is merged with the page template before rendering.
*
* @return the Page's model map
*/
public Map<String, Object> getModel() {
return model;
}
/**
* Return the Page header imports.
* <p/>
* PageImports are used define the CSS and JavaScript imports and blocks
* to be included in the page template.
* <p/>
* The PageImports object will be included in the Page template when the
* following methods are invoked:
* <ul>
* <li>{@link ClickServlet#createTemplateModel(Page)} - for template pages</li>
* <li>{@link ClickServlet#setRequestAttributes(Page)} - for JSP pages</li>
* </ul>
* <p/>
* If you need to tailor the page imports rendered, override this method
* and modify the PageImports object returned.
* <p/>
* If you need to create a custom PageImports, override the method
* {@link ClickServlet#createPageImports(org.apache.click.Page)}
*
* @deprecated use the new {@link #getHeadElements()} instead
*
* @return the Page header imports
*/
public PageImports getPageImports() {
return pageImports;
}
/**
* Set the Page header imports.
* <p/>
* PageImports are used define the CSS and JavaScript imports and blocks
* to be included in the page template.
* <p/>
* The PageImports references will be included in the Page model when the
* following methods are invoked:
* <ul>
* <li>{@link ClickServlet#createTemplateModel(Page)} - for template pages</li>
* <li>{@link ClickServlet#setRequestAttributes(Page)} - for JSP pages</li>
* </ul>
* <p/>
* If you need to tailor the page imports rendered, override the
* {@link #getPageImports()} method and modify the PageImports object
* returned.
* <p/>
* If you need to create a custom PageImports, override the method
* {@link ClickServlet#createPageImports(org.apache.click.Page)}
*
* @deprecated use the new {@link #getHeadElements()} instead
*
* @param pageImports the new pageImports instance to set
*/
public void setPageImports(PageImports pageImports) {
this.pageImports = pageImports;
}
/**
* Return the path of the Template or JSP to render.
* <p/>
* If this method returns <tt>null</tt>, Click will not perform any rendering.
* This is useful when you want to stream or write directly to the
* HttpServletResponse.
* <p/>
* See also {@link #getForward()}, {@link #getRedirect()}
*
* @return the path of the Template or JSP to render
*/
public String getPath() {
return path;
}
/**
* Set the path of the Template or JSP to render.
* <p/>
* By default Click will set the path to the requested page url. Meaning
* if the url <tt>/edit-customer.htm</tt> is requested, path will be set
* to <tt>/edit-customer.htm</tt>.
* <p/>
* Here is an example if you want to change the path to a different Template:
* <p/>
* <pre class="prettyprint">
* public void onGet() {
* setPath("/some-other-template.htm");
* }</pre>
* And here is an example if you want to change the path to a different JSP.
* <pre class="prettyprint">
* public void onGet() {
* setPath("/some-other-jsp.jsp");
* }</pre>
* <p/>
* If path is set to <tt>null</tt>, Click will not perform any rendering.
* This is useful when you want to stream or write directly to the
* HttpServletResponse.
* <p/>
* See also {@link #setForward(String)}, {@link #setRedirect(String)}
*
* @param value the path of the Template or JSP to render
*/
public void setPath(String value) {
path = value;
}
/**
* Return the path to redirect the request to.
* <p/>
* If the {@link #redirect} property is not null it will be used to redirect
* the request in preference to {@link #forward} or {@link #path} properties.
* The request is redirected to using the HttpServletResponse.setRedirect()
* method.
* <p/>
* See also {@link #getForward()}, {@link #getPath()}
*
* @return the path to redirect the request to
*/
public String getRedirect() {
return redirect;
}
/**
* Return true if the page is stateful and should be saved in the users
* HttpSession between requests, default value is false.
*
* @deprecated stateful pages are not supported anymore, use stateful
* Controls instead
*
* @return true if the page is stateful and should be saved in the users
* session
*/
public boolean isStateful() {
return stateful;
}
/**
* Set whether the page is stateful and should be saved in the users
* HttpSession between requests.
* <p/>
* Click will synchronize on the page instance. This ensures that if
* multiple requests arrive from the same user for the page, only one
* request can access the page at a time.
* <p/>
* Stateful pages are stored in the HttpSession using the key
* <tt>page.getClass().getName()</tt>.
* <p/>
* It is worth noting that Click checks a Page's stateful property after
* each request. Thus it becomes possible to enable a stateful Page for a
* number of requests and then setting it to <tt>false</tt> again at which
* point Click will remove the Page from the HttpSession, freeing up memory
* for the server.
*
* @deprecated stateful pages are not supported anymore, use stateful
* Controls instead
*
* @param stateful the flag indicating whether the page should be saved
* between user requests
*/
public void setStateful(boolean stateful) {
this.stateful = stateful;
if (isStateful()) {
getContext().getSession();
}
}
/**
* Return true if the Control head elements should be included in the page
* template, false otherwise. Default value is true.
*
* @see #setIncludeControlHeadElements(boolean)
*
* @return true if the Control head elements should be included in the page
* template, false otherwise
*/
public boolean isIncludeControlHeadElements() {
return includeControlHeadElements;
}
/**
* Set whether the Control head elements should be included in the page
* template.
* <p/>
* By setting this value to <tt>false</tt>, Click won't include Control's
* {@link #getHeadElements() head elements}, however the Page head elements
* will still be included.
* <p/>
* This allows one to create a single JavaScript and CSS resource file for
* the entire Page which increases performance, since the browser only has
* to load one resource, instead of multiple resources.
* <p/>
* Below is an example:
*
* <pre class="prettyprint">
* public class HomePage extends Page {
*
* private Form form = new Form("form");
*
* public HomePage() {
* // Indicate that Controls should not import their head elements
* setIncludeControlHeadElements(false);
*
* form.add(new EmailField("email");
* addControl(form);
* }
*
* // Include the Page JavaScript and CSS resources
* public List getHeadElements() {
* if (headElements == null) {
* headElements = super.getHeadElements();
*
* // Include the Page CSS resource. This resource should combine
* // all the CSS necessary for the page
* headElements.add(new CssImport("/assets/css/home-page.css"));
*
* // Include the Page JavaScript resource. This resource should
* // combine all the JavaScript necessary for the page
* headElements.add(new JsImport("/assets/js/home-page.js"));
* }
* return headElements;
* }
* } </pre>
*
* @param includeControlHeadElements flag indicating whether Control
* head elements should be included in the page
*/
public void setIncludeControlHeadElements(boolean includeControlHeadElements) {
this.includeControlHeadElements = includeControlHeadElements;
}
/**
* Set the location to redirect the request to.
* <p/>
* If the {@link #redirect} property is not null it will be used to redirect
* the request in preference to the {@link #forward} and {@link #path}
* properties. The request is redirected using the HttpServletResponse.setRedirect()
* method.
* <p/>
* If the redirect location begins with a <tt class="wr">"/"</tt>
* character the redirect location will be prefixed with the web applications
* <tt>context path</tt>. Note if the given location is already prefixed
* with the <tt>context path</tt>, Click won't add it a second time.
* <p/>
* For example if an application is deployed to the context
* <tt class="wr">"mycorp"</tt> calling
* <tt>setRedirect(<span class="navy">"/customer/details.htm"</span>)</tt>
* will redirect the request to:
* <tt class="wr">"/mycorp/customer/details.htm"</tt>
* <p/>
* If the redirect location does not begin with a <tt class="wr">"/"</tt>
* character the redirect location will be used as specified. Thus if the
* location is <tt class="wr">http://somehost.com/myapp/customer.jsp</tt>,
* Click will redirect to that location.
* <p/>
* <b>JSP note:</b> when redirecting to a JSP template keep in mind that the
* JSP template won't be processed by Click, as ClickServlet is mapped to
* <em>*.htm</em>. Instead JSP templates are processed by the Servlet
* container JSP engine.
* <p/>
* So if you have a situation where a Page Class
* (<span class="navy">Customer.class</span>) is mapped to the JSP
* (<span class="navy">"/customer.jsp"</span>) and you want to redirect to
* Customer.class, you could either redirect to
* (<span class="navy">"/customer<span class="red">.htm</span>"</span>) or
* use the alternative redirect utility {@link #setRedirect(java.lang.Class)}.
* <p/>
* <b>Please note</b> that Click will url encode the location by invoking
* <tt>response.encodeRedirectURL(location)</tt> before redirecting.
* <p/>
* See also {@link #setRedirect(java.lang.String, java.util.Map)},
* {@link #setForward(String)}, {@link #setPath(String)}
*
* @param location the path to redirect the request to
*/
public void setRedirect(String location) {
setRedirect(location, null);
}
/**
* Set the request to redirect to the give page class.
*
* @see #setRedirect(java.lang.String)
*
* @param pageClass the class of the Page to redirect the request to
* @throws IllegalArgumentException if the Page Class is not configured
* with a unique path
*/
public void setRedirect(Class<? extends Page> pageClass) {
setRedirect(pageClass, null);
}
/**
* Set the request to redirect to the given <code>location</code> and append
* the map of request parameters to the location URL.
* <p/>
* The map keys will be used as the request parameter names and the map
* values will be used as the request parameter values. For example:
*
* <pre class="prettyprint">
* public boolean onSave() {
* // Specify redirect parameters
* Map parameters = new HashMap();
* parameters.put("customerId", getCustomerId());
*
* // Set redirect to customer.htm page
* setRedirect("/customer.htm", parameters);
*
* return false;
* } </pre>
*
* To render multiple parameter values for the same parameter name, specify
* the values as a String[] array. For example:
*
* <pre class="prettyprint">
* public boolean onSave() {
*
* // Specify an array of customer IDs
* String[] ids = {"123", "456", "789"};
*
* // Specify redirect parameters
* Map parameters = new HashMap();
* parameters.put("customerIds", ids);
*
* // Set redirect to customer.htm page
* setRedirect("/customer.htm", parameters);
*
* return false;
* } </pre>
*
* @see #setRedirect(java.lang.String)
*
* @param location the path to redirect the request to
* @param params the map of request parameter name and value pairs
*/
public void setRedirect(String location, Map<String, ?> params) {
Context context = getContext();
if (StringUtils.isNotBlank(location)) {
if (location.charAt(0) == '/') {
String contextPath = context.getRequest().getContextPath();
// Guard against adding duplicate context path
if (!location.startsWith(contextPath + '/')) {
location = contextPath + location;
}
}
}
if (params != null && !params.isEmpty()) {
HtmlStringBuffer buffer = new HtmlStringBuffer();
Iterator<? extends Map.Entry<String, ?>> i = params.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, ?> entry = i.next();
String paramName = entry.getKey();
Object paramValue = entry.getValue();
// Check for multivalued parameter
if (paramValue instanceof String[]) {
String[] paramValues = (String[]) paramValue;
for (int j = 0; j < paramValues.length; j++) {
buffer.append(paramName);
buffer.append("=");
buffer.append(ClickUtils.encodeUrl(paramValues[j], context));
if (j < paramValues.length - 1) {
buffer.append("&");
}
}
} else {
if (paramValue != null) {
buffer.append(paramName);
buffer.append("=");
buffer.append(ClickUtils.encodeUrl(paramValue, context));
}
}
if (i.hasNext()) {
buffer.append("&");
}
}
if (buffer.length() > 0) {
if (location.contains("?")) {
location += "&" + buffer.toString();
} else {
location += "?" + buffer.toString();
}
}
}
redirect = location;
}
/**
* Set the request to redirect to the given page class and and append
* the map of request parameters to the page URL.
* <p/>
* The map keys will be used as the request parameter names and the map
* values will be used as the request parameter values.
*
* @see #setRedirect(java.lang.String, java.util.Map)
* @see #setRedirect(java.lang.String)
*
* @param pageClass the class of the Page to redirect the request to
* @param params the map of request parameter name and value pairs
* @throws IllegalArgumentException if the Page Class is not configured
* with a unique path
*/
public void setRedirect(Class<? extends Page> pageClass,
Map<String, ?> params) {
String target = getContext().getPagePath(pageClass);
// If page class maps to a jsp, convert to htm which allows ClickServlet
// to process the redirect
if (target != null && target.endsWith(".jsp")) {
target = StringUtils.replaceOnce(target, ".jsp", ".htm");
}
setRedirect(target, params);
}
/**
* Return the path of the page border template to render, by default this
* method returns {@link #getPath()}.
* <p/>
* Pages can override this method to return an alternative border page
* template. This is very useful when implementing an standardized look and
* feel for a web site. The example below provides a BorderedPage base Page
* which other site templated Pages should extend.
*
* <pre class="codeJava">
* <span class="kw">public class</span> BorderedPage <span class="kw">extends</span> Page {
* <span class="kw">public</span> String getTemplate() {
* <span class="kw">return</span> <span class="st">"border.htm"</span>;
* }
* } </pre>
*
* The BorderedPage returns the page border template <span class="st">"border.htm"</span>:
*
* <pre class="codeHtml">
* &lt;html&gt;
* &lt;head&gt;
* &lt;title&gt; <span class="blue">$title</span> &lt;/title&gt;
* &lt;link rel="stylesheet" type="text/css" href="style.css" title="Style"/&gt;
* &lt;/head&gt;
* &lt;body&gt;
*
* &lt;h1&gt; <span class="blue">$title</span> &lt;/h1&gt;
* &lt;hr/&gt;
*
* <span class="red">#parse</span>( <span class="blue">$path</span> )
*
* &lt;/body&gt;
* &lt;/html&gt; </pre>
*
* Other pages insert their content into this template, via their
* {@link #path} property using the Velocity
* <a href="../../../../velocity/vtl-reference-guide.html#parse">#parse</a>
* directive. Note the <span class="blue">$path</span> value is automatically
* added to the VelocityContext by the ClickServlet.
*
* @return the path of the page template to render, by default returns
* {@link #getPath()}
*/
public String getTemplate() {
return template == null ? getPath() : template;
}
/**
* Set the page border template path.
* <p/>
* <b>Note:</b> if this value is not set, {@link #getTemplate()} will default
* to {@link #getPath()}.
*
* @param template the border template path
*/
public void setTemplate(String template) {
this.template = template;
}
// Private methods --------------------------------------------------------
/**
* Replace the current control with the new control.
*
* @param currentControl the control currently contained in the page
* @param newControl the control to replace the current control container in
* the page
*
* @throws IllegalStateException if the currentControl is not contained in
* the page
*/
private void replaceControl(Control currentControl, Control newControl) {
// Current control and new control are referencing the same object
// so we exit early
if (currentControl == newControl) {
return;
}
int controlIndex = getControls().indexOf(currentControl);
if (controlIndex == -1) {
throw new IllegalStateException("Cannot replace the given control"
+ " because it is not present in the page");
}
// Note: set parent first since setParent might veto further processing
newControl.setParent(this);
currentControl.setParent(null);
// Set control to current control index
getControls().set(controlIndex, newControl);
addModel(newControl.getName(), newControl);
}
}