    001// Copyright 2006-2013 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007//     http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.corelib.components;
    016
    017import java.util.List;
    018import java.util.Set;
    019
    020import org.apache.tapestry5.BindingConstants;
    021import org.apache.tapestry5.MarkupWriter;
    022import org.apache.tapestry5.SymbolConstants;
    023import org.apache.tapestry5.ValidationTracker;
    024import org.apache.tapestry5.annotations.Environmental;
    025import org.apache.tapestry5.annotations.Parameter;
    026import org.apache.tapestry5.commons.util.CollectionFactory;
    027import org.apache.tapestry5.ioc.annotations.Inject;
    028import org.apache.tapestry5.ioc.annotations.Symbol;
    029import org.apache.tapestry5.services.ComponentOverride;
    030import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    031
    032/**
    033 * Standard validation error presenter. Must be enclosed by a
    034 * {@link org.apache.tapestry5.corelib.components.Form} component. If errors are present, renders a
    035 * {@code <div>} element around a banner message and around an unnumbered list of
    036 * error messages. Renders nothing if the {@link org.apache.tapestry5.ValidationTracker} shows no
    037 * errors.
    038 *
    039 * @tapestrydoc
    040 * @see Form
    041 */
    042//@Import(module = "bootstrap/alert")
    043public class Errors
    044{
    045    /**
    046     * The banner message displayed above the errors. The default value is "You must correct the
    047     * following errors before
    048     * you may continue.".
    049     */
    050    @Parameter("message:core-default-error-banner")
    051    private String banner;
    052
    053    /**
    054     * If true, then only errors global to the form (unassociated with any specific field) are
    055     * presented. By default all errors (associated with fields, or not) are presented; with unassociated
    056     * errors presented first.
    057     *
    058     * @since 5.4
    059     */
    060    @Parameter
    061    private boolean globalOnly;
    062
    063    /**
    064     * The CSS class for the div element rendered by the component.
    065     */
    066    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, value = "symbol:" + SymbolConstants.ERRORS_DEFAULT_CLASS_PARAMETER_VALUE)
    067    private String className;
    068
    069    // Allow null so we can generate a better error message if missing
    070    @Environmental(false)
    071    private ValidationTracker tracker;
    072
    073    @Inject
    074    @Symbol(SymbolConstants.ERRORS_BASE_CSS_CLASS)
    075    private String baseCssClass;
    076
    077    @Inject
    078    @Symbol(SymbolConstants.ERRORS_CLOSE_BUTTON_CSS_CLASS)
    079    private String closeButtonCssClass;
    080
    081    @Inject
    082    private JavaScriptSupport javaScriptSupport;
    083
    084    boolean beginRender(MarkupWriter writer)
    085    {
    086        if (tracker == null)
    087            throw new RuntimeException("The Errors component must be enclosed by a Form component.");
    088
    089        if (!tracker.getHasErrors())
    090        {
    091            return false;
    092        }
    093
    094        List<String> errors =
    095                globalOnly ? tracker.getUnassociatedErrors() : tracker.getErrors();
    096
    097        if (errors.isEmpty())
    098        {
    099            return false;
    100        }
    101
    102        setUpJavaScript();
    103
    104        Set<String> previousErrors = CollectionFactory.newSet();
    105
    106        writer.element("div", "class", baseCssClass + " " + className, "role", "alert");
    107        writer.element("button",
    108                "type", "button",
    109                "class", closeButtonCssClass,
    110                "data-dismiss", "alert");
    111        writer.writeRaw("&times;");
    112        writer.end();
    113
    114        writer.element("h4");
    115        writer.writeRaw(banner);
    116        writer.end();
    117
    118        writer.element("ul");
    119
    120        for (String message : errors)
    121        {
    122            if (previousErrors.contains(message))
    123            {
    124                continue;
    125            }
    126
    127            writer.element("li");
    128            writer.write(message);
    129            writer.end();
    130
    131            previousErrors.add(message);
    132        }
    133
    134        writer.end(); // ul
    135
    136        writer.end(); // div
    137
    138        return false;
    139    }
    140
    141    /**
    142     * Hook to set up JavaScript, specifically for handling the close button.
    143     * Method intended to be overriden when Bootstrap isn't being used.
    144     * This implementation calls <code>javaScriptSupport.require("bootstrap/alert");</code>.
    145     * @since 5.5
    146     * @see ComponentOverride
    147     */
    148    protected void setUpJavaScript() {
    149        javaScriptSupport.require("bootstrap/alert");
    150    }
    151
    152}