/* Licensed 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.riotfamily.forms;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.riotfamily.common.util.DocumentWriter;
import org.riotfamily.common.util.FormatUtils;
import org.riotfamily.common.util.Generics;
import org.riotfamily.common.util.TagWriter;
import org.riotfamily.forms.event.Button;
import org.riotfamily.forms.event.ClickEvent;
import org.riotfamily.forms.event.ClickListener;
import org.riotfamily.forms.event.EventPropagation;
import org.riotfamily.forms.event.JavaScriptEventAdapter;
import org.riotfamily.forms.request.FormRequest;
import org.riotfamily.forms.request.HttpFormRequest;
import org.riotfamily.forms.resource.FormResource;
import org.riotfamily.forms.resource.LoadingCodeGenerator;
import org.riotfamily.forms.resource.ResourceElement;
import org.riotfamily.forms.resource.Resources;
import org.riotfamily.forms.resource.ScriptResource;
import org.riotfamily.forms.ui.Dimension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.validation.Validator;
/**
* Serverside representation of a HTML form.
*/
public class Form implements BeanEditor {
private static final String DEFAULT_ID = "f0";
private static final String FORM_ATTR = "form";
private static final String ELEMENT_CONTAINER_ATTR = "elements";
private static final String BUTTON_CONTAINER_ATTR = "buttons";
private Logger log = LoggerFactory.getLogger(Form.class);
private String id = DEFAULT_ID;
/** Elements keyed by their ID */
private Map<String, Element> elementMap = new HashMap<String, Element>();
/** Counter to create unique IDs */
private int idCount;
/** Set of used parameter names */
private Set<String> paramNames = Generics.newHashSet();
/** EditorBinder to bind top-level elements to properties */
private EditorBinder editorBinder;
/** Set containing resources required by the form itself (not it's elements) */
private List<FormResource> globalResources = Generics.newArrayList();
private FormInitializer initializer;
private List<Container> containers = Generics.newArrayList();
private Container elements;
private Container buttons;
private FormContext formContext;
private FormErrors errors;
private Validator validator;
private FormListener formListener;
private boolean rendering;
private String clickedButton;
private String template = TemplateUtils.getTemplatePath(this);
private Map<String, Object> renderModel = Generics.newHashMap();
private String hint;
public Form() {
setAttribute(FORM_ATTR, this);
elements = createContainer(ELEMENT_CONTAINER_ATTR);
buttons = createContainer(BUTTON_CONTAINER_ATTR);
}
public Form(Class<?> type) {
this();
setBeanClass(type);
}
/**
* @since 6.4
*/
@SuppressWarnings("unchecked")
public Form(Object object) {
this();
Assert.notNull(object);
if (object instanceof Map) {
editorBinder = new MapEditorBinder((Map) object);
}
else {
editorBinder = new BeanEditorBinder(object);
}
}
public void setEditorBinder(EditorBinder editorBinder) {
this.editorBinder = editorBinder.replace(this.editorBinder);
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public void setTemplate(String template) {
this.template = template;
}
public Collection<Element> getRegisteredElements() {
return elementMap.values();
}
public void setAttribute(String key, Object value) {
renderModel.put(key,value);
}
@SuppressWarnings("unchecked")
public<T> T getAttribute(String key) {
return (T) renderModel.get(key);
}
@SuppressWarnings("unchecked")
public void setBeanClass(Class<?> beanClass) {
Assert.notNull(beanClass, "The beanClass must not be null.");
if (Map.class.isAssignableFrom(beanClass)) {
editorBinder = new MapEditorBinder((Class<? extends Map<Object,Object>>) beanClass);
}
else {
editorBinder = new BeanEditorBinder(beanClass);
}
}
public void setBackingObject(Object value) {
editorBinder.setBackingObject(value);
}
public boolean isNew() {
return !editorBinder.isEditingExistingBean();
}
public EditorBinder getEditorBinder() {
return editorBinder;
}
public Editor getEditor(String property) {
return editorBinder.getEditor(property);
}
public void bind(Editor editor, String property) {
editorBinder.bind(editor, property);
}
public void addElement(Element element) {
elements.addElement(element);
}
public List<Element> getElements() {
return this.elements.getComponents();
}
public List<Element> getButtons() {
return this.buttons.getComponents();
}
public String getHint() {
if (hint == null) {
hint = MessageUtils.getHint(this, editorBinder.getBeanClass());
}
return hint;
}
/**
* Convenience method to add and bind an element in a single step.
*/
public void addElement(Editor element, String property) {
bind(element, property);
addElement(element);
}
public void addButton(Button button) {
button.addClickListener(new ClickListener() {
public void clicked(ClickEvent event) {
clickedButton = event.getSource().getParamName();
}
});
buttons.addElement(button);
}
public void addButton(String name) {
Button button = new Button();
button.setSubmit(true);
button.setParamName(name);
button.setLabelKey("label.form.button." + name);
addButton(button);
}
public String getClickedButton() {
return clickedButton;
}
public Container createContainer(String name) {
Container container = new Container();
containers.add(container);
registerElement(container);
setAttribute(name, container);
return container;
}
public void addResource(FormResource resource) {
globalResources.add(resource);
}
protected List<FormResource> getResources() {
List<FormResource> resources = Generics.newArrayList(globalResources);
for (Element element : getRegisteredElements()) {
if (element instanceof ResourceElement) {
ResourceElement re = (ResourceElement) element;
FormResource res = re.getResource();
if (res != null) {
resources.add(res);
}
}
}
return resources;
}
/**
* Creates and sets an id for the given element and puts it into the
* internal elementMap.
*
* @param element the element to register
*/
public void registerElement(Element element) {
String id = createId();
element.setId(id);
element.setForm(this);
if (formContext != null) {
element.setFormContext(formContext);
}
elementMap.put(id, element);
}
public void unregisterElement(Element element) {
elementMap.remove(element.getId());
}
public String createId() {
return FormatUtils.toCssClass(this.id) + "e" + idCount++;
}
/**
* Returns the previously registered element with the given id.
*/
public Element getElementById(String id) {
return elementMap.get(id);
}
/**
* Returns a String that can be used as parameter name for input elements.
* Subsequent calls will return different values to ensure uniqueness of
* parameter names within a form.
*/
public String createUniqueParameterName() {
return createUniqueParameterName(null);
}
/**
* Like {@link #createUniqueParameterName()}this method returns a String
* that can be used as parameter name. Since most modern browsers keep track
* of previously entered values (with the same parameter name) a desired
* name can be passed to this method. Typically an element will use the name
* of the property it is bound to as name. As this name might already be
* taken by another element (especially when lists of nested forms are used)
* this method will append an integer value to the given name if necessary.
*/
public String createUniqueParameterName(String desiredName) {
if (desiredName == null) {
desiredName = "p" + paramNames.size();
}
String name = desiredName;
if (name.equalsIgnoreCase("target")) {
// Otherwise changing the target of the form would not work
name = "_target";
}
//TODO Assure uniqueness of synthetic names
if (paramNames.contains(name)) {
name = desiredName + paramNames.size();
}
paramNames.add(name);
return name;
}
public void render(PrintWriter writer) {
rendering = true;
formContext.setWriter(writer);
DocumentWriter doc = new DocumentWriter(writer);
doc.start("script").body();
writer.write("if (!(window.riot && riot.Resources)) document.write('"
+ "<script src=\"" + formContext.getContextPath()
+ formContext.getResourcePath() + "riot/resources.js"
+ "\"></scr'+'ipt>');\n");
doc.end();
doc.start("script").body();
writer.write("riot.Resources.setBasePath('" + formContext.getContextPath()
+ formContext.getResourcePath() + "');\n");
LoadingCodeGenerator.renderLoadingCode(getResources(), writer);
doc.end();
formContext.getTemplateRenderer().render(
template, renderModel, writer);
writer.print("<script>");
ArrayList<EventPropagation> propagations = new ArrayList<EventPropagation>();
for (Element element : getRegisteredElements()) {
if (element instanceof JavaScriptEventAdapter) {
JavaScriptEventAdapter adapter = (JavaScriptEventAdapter) element;
EventPropagation.addPropagations(adapter, propagations);
}
}
if (!propagations.isEmpty()) {
writer.print("riot.Resources.waitFor('propagate', function() {");
for (EventPropagation p : propagations) {
writer.print("propagate('");
writer.print(p.getTriggerId());
writer.print("', '");
writer.print(p.getType());
writer.print("', '");
writer.print(p.getSourceId());
writer.print("');\n");
}
writer.print("});");
}
writer.print("</script>");
rendering = false;
}
public boolean isRendering() {
return rendering;
}
public void elementRendered(Element element) {
log.debug("Element rendered: " + element.getId());
if (getFormListener() != null) {
getFormListener().elementRendered(element);
}
if (rendering && element instanceof DHTMLElement) {
DHTMLElement dhtml = (DHTMLElement) element;
PrintWriter writer = formContext.getWriter();
TagWriter script = new TagWriter(writer);
String initScript = dhtml.getInitScript();
if (initScript != null) {
script.start("script");
script.attribute("language", "JavaScript");
script.attribute("type", "text/javascript");
script.body().print("//").cData().println();
if (dhtml instanceof ResourceElement) {
ResourceElement resEle = (ResourceElement) dhtml;
FormResource res = resEle.getResource();
if (res != null) {
script.print("riot.Resources.execWhenLoaded(['");
script.print(res.getUrl());
script.print("'], function() {");
script.print(initScript);
script.print("})");
}
else {
script.print(initScript);
}
}
else {
script.print(initScript);
}
script.print("//").end();
}
}
}
public void setInitializer(FormInitializer initializer) {
this.initializer = initializer;
}
public void setValidator(Validator validator) {
this.validator = validator;
}
public void init() {
addResource(new ScriptResource("form/ajax.js", "propagate",
Resources.RIOT_EFFECTS));
if (initializer != null) {
initializer.initForm(this);
}
editorBinder.initEditors();
}
public void processRequest(HttpServletRequest request) {
processRequest(new HttpFormRequest(request));
}
public void processRequest(FormRequest request) {
clickedButton = null;
errors.removeAllErrors();
Iterator<Container> it = containers.iterator();
while (it.hasNext()) {
Container container = it.next();
if (container.isEnabled()) {
container.processRequest(request);
}
}
if (validator != null) {
validator.validate(populateBackingObject(), errors);
}
}
public void processExclusiveRequest(String elementId,
HttpServletRequest request) {
processExclusiveRequest(elementId, new HttpFormRequest(request));
}
public void processExclusiveRequest(String elementId, FormRequest request) {
errors.removeAllErrors();
Element element = getElementById(elementId);
if (element.isEnabled()) {
element.processRequest(request);
}
}
public void setFormListener(FormListener formListener) {
this.formListener = formListener;
}
public FormListener getFormListener() {
return formListener;
}
public void requestFocus(Element element) {
if (formListener != null) {
formListener.elementFocused(element);
}
}
public Object getBackingObject() {
return editorBinder.getBackingObject();
}
public Object populateBackingObject() {
return editorBinder.populateBackingObject();
}
public FormContext getFormContext() {
return this.formContext;
}
public void setFormContext(FormContext formContext) {
this.formContext = formContext;
errors = new FormErrors(this);
editorBinder.registerPropertyEditors(formContext.getPropertyEditorRegistrars());
renderModel.put("messageResolver", formContext.getMessageResolver());
elements.setComponentPadding(formContext.getSizing().getLabelSize());
Iterator<Element> it = getRegisteredElements().iterator();
while (it.hasNext()) {
Element element = it.next();
element.setFormContext(formContext);
}
}
public Dimension getDimension() {
return elements.getDimension();
}
public String getAction() {
return formContext.getFormUrl();
}
public FormErrors getErrors() {
return errors;
}
public boolean hasErrors() {
return errors.hasErrors();
}
}