net.sf.click.jquery.behavior.JQBehavior.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.click.jquery.behavior.JQBehavior.java

Source

/*
 * 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 net.sf.click.jquery.behavior;

import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import org.apache.click.Context;
import org.apache.click.Control;
import org.apache.click.Page;
import org.apache.click.ActionResult;
import org.apache.click.element.Element;
import org.apache.click.element.JsImport;
import org.apache.click.element.JsScript;
import net.sf.click.jquery.JQEvent;
import net.sf.click.jquery.util.JSONWriter;
import net.sf.click.jquery.util.Options;
import org.apache.click.service.ConfigService;
import org.apache.click.service.LogService;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.HtmlStringBuffer;
import org.apache.commons.lang.StringUtils;

/**
 * TODO: Describe how to set a custom setupScript.
 * <p/>
 * This behavior has an associated JavaScript template that can be modified
 * according to your needs.
 * <p/>
 The jQuery-Click templates are provided in the JavaScript file
 * <a href="../../../../../js/template/jquery.templates.js.txt">jquery.templates.js</a>.
 * This behavior's template is <tt>Template 1</tt> and the JavaScript function is
 * called <tt>Click.jq.ajaxTemplate</tt>.
 * <p/>
 * If you rename the JavaScript function or change the function's arguments, you
 * will also have to customize the {@link #setSetupScript(org.apache.click.element.JsScript) setupScript},
 * otherwise the Behavior will still generate the old JavaScript function signature.
 *
 * * @see JQTemplateBehavior if you want to create a new custom template instead.
 */
public class JQBehavior extends AbstractJQBehavior implements Serializable {

    // Constants --------------------------------------------------------------

    private static final long serialVersionUID = 1L;

    /**
     * The path of the default jQuery templates:
     * "<tt>/click-jquery/template/jquery.templates.js</tt>".
     * <p/>
     * These templates can be customized to suite your needs. You can even
     * replace these templates with your own version.
     */
    public static String defaultTemplatesPath = "/click-jquery/template/jquery.templates.js";

    /**
     * The path to the {@value #defaultTemplatesPath} language packs, default
     * value is <tt>"/click-jquery/template/lang/"</tt>.
     */
    public static String defaultTemplatesLangFolder = "/click-jquery/template/lang/";

    // Variables --------------------------------------------------------------

    /** Supported locales. */
    static String[] SUPPORTED_LANGUAGES = { "af", "en" };

    /**
     * The path of the default jQuery templates: {@value #jqueryTemplatesPath}.
     * <p/>
     * These templates can be customized to suite your needs. You can even
     * replace these templates with your own version.
     */
    protected String templatesPath = defaultTemplatesPath;

    /**
     * The folder where the template language packs can be found, the default
     * value is {@value #defaultTemplatesLangFolder}.
     */
    protected String templatesLangFolder = defaultTemplatesLangFolder;

    protected String eventType = JQEvent.CLICK;

    /** The data model for the JavaScript {@link #template}. */
    protected Map<String, Object> model;

    /** The Ajax request parameters. */
    protected Map<String, Object> data;

    /** The type request (POST / GET), default value is GET. */
    protected String type = "GET";

    /** The Ajax request url. */
    protected String url;

    protected String setupScriptId;

    /**
     * The delay within which multiple Ajax requests are merged into a
     * single request. Useful for keyboard and mouse based events.
     */
    protected int delay = 0;

    protected boolean skipSetupScript = false;

    protected boolean skipHeadElements = false;

    protected JsScript setupScript;

    /**
     * Flag indicating whether an Ajax indicator (busy indicator) must be shown,
     * default value is true.
     */
    protected boolean showBusyIndicator = true;

    protected Options busyIndicatorOptions;

    protected String busyIndicatorMessage;

    protected String busyIndicatorTarget;

    protected int timeout = 20000;

    protected int timeoutRetryLimit = 3;

    /**
     * By default JQBehavior uses the cssSelector of each control it is
     * added to. You can override this default behavior by setting the
     * CSS Selector property to use.
     */
    protected String cssSelector;

    // ----------------------------------------------------------- Constructors

    public JQBehavior(String eventType) {
        // EventType is immutable and cannot be changed at a later stage
        this.eventType = eventType;
    }

    public JQBehavior() {
    }

    // Public Method ----------------------------------------------------------

    /**
     * Return the CSS Selector for the behavior, defaults to the Controls CSS selector
     * returned by {@link org.apache.click.util.ClickUtils#getCssSelector(org.apache.click.Control)}.
     *
     * @return the behavior CSS Selector
     */
    public String getCssSelector() {
        return cssSelector;
    }

    /**
     * By default JQBehavior uses the cssSelector of each control it is added to.
     * You can override this default behavior by setting the CSS Selector property
     * to use.
     *
     * @param cssSelector the behavior CSS Selector
     */
    public void setCssSelector(String cssSelector) {
        this.cssSelector = cssSelector;
    }

    public String getEventType() {
        return eventType;
    }

    /**
     * Return the template to render for this behavior.
     *
     * @return the template to render for this behavior
     */
    public String getTemplatesPath() {
        return templatesPath;
    }

    /**
     * Set the template to render for this behavior.
     *
     * @param templatesPath the template to render for this behavior
     */
    public void setTemplatesPath(String templatesPath) {
        this.templatesPath = templatesPath;
    }

    /**
     * Return the folder containing the template language packs. The default
     * value is {@value #templatesLangFolder}.
     *
     * @see #setTemplatesLangPath(java.lang.String)
     *
     * @return the template to render for this helper
     */
    public String getTemplatesLangFolder() {
        return templatesLangFolder;
    }

    /**
     * Set the path of the template language pack to render. Specific language
     * packs will be looked up from this folder based on the request's
     * {@link org.apache.click.Context#getLocale() Context locale}.
     * <p/>
     * For example if templatesLangPath is <tt>"/click-jquery/templates/"</tt>
     * and the Context locale is German (de), the behavior language pack will
     * be loaded from <tt>"/click-jquery/templates/de.js"</tt>.
     *
     * @param templatesLangPath the path of the template language pack to render
     */
    public void setTemplatesLangPath(String templatesLangPath) {
        this.templatesLangFolder = templatesLangPath;
    }

    /**
     * Return the number of milliseconds to wait before the Ajax request is
     * invoked.
     *
     * @see #setDelay(int)
     *
     * @return the number of milliseconds to wait before the Ajax request is
     * invoked
     */
    public int getDelay() {
        return delay;
    }

    /**
     * Set the number of milliseconds to wait before the Ajax request is
     * invoked, default value is 0, meaning requests are invoked immediately.
     * <p/>
     * <b>Please note:</b> all other Ajax requests invoked within the
     * delay period, is merged into a single request.
     *
     * @param delay the delay to wait before the Ajax request is invoked
     */
    public void setDelay(int delay) {
        this.delay = delay;
    }

    /**
     * Return the data model for the JavaScript {@link #template}.
     *
     * @return the data model for the JavaScript template
     */
    public Map<String, Object> getModel() {
        if (model == null) {
            model = new HashMap<String, Object>();
        }
        return model;
    }

    /**
     * Return the Ajax request parameter Map.
     *
     * @return the Ajax request parameter Map
     */
    public Map<String, Object> getData() {
        if (data == null) {
            data = new HashMap<String, Object>(2);
        }
        return data;
    }

    /**
     * Return true if the Ajax request has parameters, false otherwise.
     *
     * @return true if the Ajax request has parameters, false otherwise
     */
    public boolean hasData() {
        return data == null || data.isEmpty() ? false : true;
    }

    /**
     * Set the Ajax request parameter.
     *
     * @param data the Ajax request parameters
     */
    public void setData(Map<String, Object> data) {
        this.data = data;
    }

    /**
     * Set an Ajax request parameter with the given name and value.
     *
     * @param name the name of the parameter
     * @param value the value of the parameter
     */
    public void setData(String name, Object value) {
        if (name == null) {
            throw new IllegalArgumentException("Null name data parameter");
        }

        if (value != null) {
            getData().put(name, value);
        } else {
            getData().remove(name);
        }
    }

    /**
     * Return true if the Ajax indicator (busy indicator) should be shown,
     * false otherwise.
     *
     * @return true if the Ajax indicator (busy indicator) should be shown,
     * false otherwise
     */
    public boolean isShowBusyIndicator() {
        return showBusyIndicator;
    }

    /**
     * Set whether an Ajax indicator (busy indicator) should be shown during
     * Ajax requests.
     *
     * @param showBusyIndicator indicates whether an Ajax indicator should be shown
     * during Ajax requests
     */
    public void setShowBusyIndicator(boolean showBusyIndicator) {
        this.showBusyIndicator = showBusyIndicator;
    }

    /**
     * @return the skipHeadElements
     */
    public boolean isSkipHeadElements() {
        return skipHeadElements;
    }

    /**
     * @param skipHeadElements true if headElements should not be rendered,
     * false otherwise
     */
    public void setSkipHeadElements(boolean skipHeadElements) {
        this.skipHeadElements = skipHeadElements;
    }

    /**
     * @return the skipSetup
     */
    public boolean isSkipSetupScript() {
        return skipSetupScript;
    }

    /**
     * @param skipSetupScript the skipSetup to set
     */
    public void setSkipSetupScript(boolean skipSetupScript) {
        this.skipSetupScript = skipSetupScript;
    }

    public JsScript getSetupScript() {
        return setupScript;
    }

    /**
     * Set a custom setup script which override the default rendered script.
     * <p/>
     * The Behavior {@link #createTemplateModel(org.apache.click.Page, org.apache.click.Control, org.apache.click.Context) model}
     * values will be passed to the JsScript if the script {@link org.apache.click.element.JsScript#setTemplate(java.lang.String) template}
     * is set.
     * <p/>
     * <b>Please note:</b> specifying your own setup script implies using your
     * own JavaScript template, meaning the Behavior {@link #templatesPath}
     * should not be rendered and thus the {@link #templatesPath} will be set
     * to null by this method.
     *
     * @param setupScript the behavior setup script to render
     */
    public void setSetupScript(JsScript setupScript) {
        this.setupScript = setupScript;
        setTemplatesPath(null);
    }

    /**
     * Return the type of Ajax request eg GET or POST.
     *
     * @return the type of Ajax request
     */
    public String getType() {
        return type;
    }

    /**
     * Set the type of the Ajax request, e.g. GET or POST.
     *
     * @param type the type of the Ajax request
     */
    public void setType(String type) {
        this.type = type;
    }

    public String getSetupScriptId() {
        if (setupScriptId == null) {
            // TODO move to another method and get rid of incoming source argument
            setupScriptId = getTemplatesPath().substring(1).replace('/', '-').replace('.', '-');
        }
        return setupScriptId;
    }

    public void setSetupScriptId(String setupScriptId) {
        this.setupScriptId = setupScriptId;
    }

    /**
     * Return the URL for the Ajax request, defaults to the URL of the
     * current Page.
     *
     * @return the URL for the Ajax request
     */
    public String getUrl() {
        if (url == null) {
            Context context = getContext();
            url = ClickUtils.getRequestURI(context.getRequest());
            url = context.getResponse().encodeURL(url);
        }
        return url;
    }

    /**
     * Set the URL for the Ajax request. If no URL is set it will default to
     * the URL of the current Page.
     *
     * @param url the URL for the Ajax request
     */
    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * @return the busyIndicatorOptions
     */
    public Options getBusyIndicatorOptions() {
        return busyIndicatorOptions;
    }

    /**
     * @return the busyIndicatorOptions
     */
    public boolean hasBusyIndicatorOptions() {
        return busyIndicatorOptions == null || busyIndicatorOptions.isEmpty() ? false : true;
    }

    /**
     * @param busyIndicatorOptions the busyIndicatorOptions to set
     */
    public void setBusyIndicatorOptions(Options busyIndicatorOptions) {
        this.busyIndicatorOptions = busyIndicatorOptions;
    }

    /**
     * @return the busyIndicatorMessage
     */
    public String getBusyIndicatorMessage() {
        // TODO lookup message from bundle first
        return busyIndicatorMessage;
    }

    /**
     * @param busyIndicatorMessage the busyIndicatorMessage to set
     */
    public void setBusyIndicatorMessage(String busyIndicatorMessage) {
        this.busyIndicatorMessage = busyIndicatorMessage;
    }

    /**
     * @return the busyIndicatorTarget
     */
    public String getBusyIndicatorTarget() {
        return busyIndicatorTarget;
    }

    /**
     * @param busyIndicatorTarget the busyIndicatorTarget to set
     */
    public void setBusyIndicatorTarget(String busyIndicatorTarget) {
        this.busyIndicatorTarget = busyIndicatorTarget;
    }

    /**
     * @return the timeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * @param timeout the timeout to set
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * @return the timeoutRetryLimit
     */
    public int getTimeoutRetryLimit() {
        return timeoutRetryLimit;
    }

    /**
     * @param timeoutRetryLimit the timeoutRetryLimit to set
     */
    public void setTimeoutRetryLimit(int timeoutRetryLimit) {
        this.timeoutRetryLimit = timeoutRetryLimit;
    }

    public static void addSupportedLanguages(String... languages) {
        if (languages == null) {
            throw new IllegalArgumentException("Languages cannot be null");
        }

        String[] newLanguages = new String[SUPPORTED_LANGUAGES.length + languages.length];
        System.arraycopy(SUPPORTED_LANGUAGES, 0, newLanguages, 0, SUPPORTED_LANGUAGES.length);
        SUPPORTED_LANGUAGES = newLanguages;
    }

    // Behavior Methods -------------------------------------------------------

    /**
     * If the behavior specifies an eventType, the incoming request must
     * have a eventType parameter that matches the behavior eventType before
     * this method is invoked.
     *
     * If no eventType is specified, the onAction is always called if the
     * behavior is the Ajax target.
     */
    public ActionResult onAction(Control source, JQEvent eventType) {
        return null;
    }

    // Callback Methods -------------------------------------------------------

    @Override
    public final ActionResult onAction(Control source) {
        Context context = getContext();
        String whichTypeParam = context.getRequestParameter("which");

        JQEvent event = new JQEvent();
        event.setType(getEventType());
        event.setWhich(whichTypeParam);
        return onAction(source, event);
    }

    @Override
    public boolean isAjaxTarget(Context context) {
        String eventTypeParam = context.getRequestParameter("event");
        if (StringUtils.isBlank(eventTypeParam) && StringUtils.isBlank(getEventType())) {
            return true;
        }
        return StringUtils.equalsIgnoreCase(eventTypeParam, getEventType());
    }

    @Override
    public void preRenderHeadElements(Control source) {
        // If headElements should be skipped, exit early
        if (isSkipHeadElements()) {
            return;
        }

        super.preRenderHeadElements(source);

        // If setup script should be skipped, exit early
        if (!isSkipSetupScript()) {
            addSetupScript(source);
        }
    }

    /**
     * Two JQBehaviors are equal if their {@link #eventType} is equal. This
     * ensures that a control can only have one behavior of a certain type. It
     * also prevents memory leaks in stateful pages.
     *
     * @see java.lang.Object#equals(java.lang.Object)
     *
     * @param o the object with which to compare this instance with
     * @return true if the specified object is the same as this object
     */
    @Override
    public boolean equals(Object o) {

        //1. Use the == operator to check if the argument is a reference to this object.
        if (o == this) {
            return true;
        }

        //2. Use the instanceof operator to check if the argument is of the correct type.
        if (!(o instanceof JQBehavior)) {
            return false;
        }

        //3. Cast the argument to the correct type.
        JQBehavior that = (JQBehavior) o;

        String localEventType = getEventType();
        String thatEventType = that.getEventType();
        return localEventType == null ? thatEventType == null : localEventType.equals(thatEventType);
    }

    /**
     * A JQBehavior hash code value is based on its {@link #eventType}. This
     * ensures that a control can only have one behavior of a certain type. It
     * also prevents memory leaks in stateful pages.
     *
     * @see java.lang.Object#hashCode()
     *
     * @return a hash code value for this object
     */
    @Override
    public int hashCode() {
        int result = 17;
        result = 37 * result + (getEventType() == null ? 0 : getEventType().hashCode());
        return result;
    }

    // Protected methods ------------------------------------------------------

    /**
     * Create a default data model for the Ajax {@link #template}.
     * <p/>
     * The following values are automatically added to the model:
     * <ul>
     * <li>"<span color="blue">context</span>" - the request context path e.g: '/myapp'</li>
     * <li>"<span color="blue">control</span>" - the behavior's source control</li>
     * <li>"{@link #cssSelector}" - the CSS selector</li>
     * <li>"{@link #eventType event}" - the event that initiates the Ajax request</li>
     * <li>"<span color="blue">productionMode</span>" - true if Click is running
     * in a production mode (production or profile), false otherwise</li>
     * <li>"{@link #url url}" - the Ajax request URL</li>
     * <li>"{@link #type}" - the type of the Ajax request, eg POST or GET</li>
     * <li>"{@link #delay}" - the time to wait before making the Ajax request.
     * If more Ajax requests are fired within this period, they are merged into
     * a single request.</li>
     * <li>"{@link #timeout}" - how long to wait before wait before the Ajax
     * request should be canceled.</li>
     * <li>"{@link #timeoutRetryLimit}" - the number of times a {@link #timeout timed out}
     * Ajax request should be retried before giving up.</li>
     * <li>"{@link #showBusyIndicator}" - flag indicating whether a busy
     * indicator is shown or not</li>
     * <li>"{@link #busyIndicatorOptions}"</span> - the Ajax indicator options. Note
     * that {@link #busyIndicatorMessage} is rendered as part of the options</li>
     * <li>"{@link #busyIndicatorTarget}" - the target element of the Ajax indicator</li>
     * <li>"{@link #data}" - the Ajax request parameters</li>
     * </ul>
     *
     * @return the default data model for the Ajax template
     */
    protected Map<String, Object> createTemplateModel(Page page, Control source, Context context) {

        Map<String, Object> templateModel = new HashMap<String, Object>(getModel());

        String localEventType = getEventType();
        boolean bindableEvent = JQEvent.isBindableEvent(localEventType);

        if (bindableEvent) {
            addModel(templateModel, "event", localEventType, page, context);
        }

        String localCssSelector = getCssSelector();
        if (localCssSelector == null) {

            localCssSelector = ClickUtils.getCssSelector(source);
            if (localCssSelector == null) {
                throw new IllegalStateException("Control {" + source.getClass().getSimpleName() + ":"
                        + source.getName() + "} has no css selector defined. Either set a proper CSS"
                        + " selector or set JQBehavior.setSkipSetup(true).");
            }
        }

        addModel(templateModel, "context", context.getRequest().getContextPath(), page, context);
        addModel(templateModel, "cssSelector", localCssSelector, page, context);

        String localBusyIndicatorMessage = getBusyIndicatorMessage();

        // If set, add message to options
        if (localBusyIndicatorMessage != null) {
            getBusyIndicatorOptions().put("message", localBusyIndicatorMessage);
        }

        if (!isShowBusyIndicator()) {
            addModel(templateModel, "showBusyIndicator", false, page, context);
        }
        if (hasBusyIndicatorOptions()) {
            addModel(templateModel, "busyIndicatorOptions", getBusyIndicatorOptions(), page, context);
        }
        if (getBusyIndicatorTarget() != null) {
            addModel(templateModel, "busyIndicatorTarget", getBusyIndicatorTarget(), page, context);
        }

        String localUrl = getUrl();
        if (localUrl != null) {
            addModel(templateModel, "url", localUrl, page, context);
        }
        if (!"GET".equals(getType())) {
            addModel(templateModel, "type", getType(), page, context);
        }
        if (getDelay() > 0) {
            addModel(templateModel, "delay", getDelay(), page, context);
        }

        if (getTimeout() != 20000) {
            addModel(templateModel, "timeout", getTimeout(), page, context);
        }

        if (getTimeoutRetryLimit() != 3) {
            addModel(templateModel, "timeoutRetryLimit", getTimeoutRetryLimit(), page, context);
        }

        if (hasData()) {
            addModel(templateModel, "data", serialize(getData()), page, context);
        }

        return templateModel;
    }

    /**
     * Add the necessary JavaScript imports and scripts to the given
     * headElements list to enable Ajax requests.
     *
     * @param headElements the list which to add all JavaScript imports and
     * scripts to enable Ajax requests
     */
    @Override
    protected void addHeadElementsOnce(Control source) {

        List<Element> headElements = source.getHeadElements();

        int index = 0;
        JsImport jsImport = new JsImport(jqueryPath);
        if (!headElements.contains(jsImport)) {
            headElements.add(index, jsImport);
        }

        index++;
        jsImport = new JsImport(jqueryClickPath);
        if (!headElements.contains(jsImport)) {
            headElements.add(index, jsImport);
        }

        if (isShowBusyIndicator()) {
            index++;
            jsImport = new JsImport(blockUIPath);
            if (!headElements.contains(jsImport)) {
                headElements.add(index, jsImport);
            }
        }

        jsImport = new JsImport(getTemplatesPath());
        if (!headElements.contains(jsImport)) {
            headElements.add(jsImport);
        }

        String language = getLocale().getLanguage();

        // English is default language, only include language pack if other
        // than English
        if (!"en".equals(language)) {
            jsImport = new JsImport(getTemplatesLangFolder() + language + ".js");
            jsImport.setAttribute("charset", "UTF-8");
            headElements.add(jsImport);
        }

        // TODO Add production modes to context to quicken this check
        ServletContext servletContext = getContext().getServletContext();
        ConfigService configService = ClickUtils.getConfigService(servletContext);

        // If Click is running in development modes, enable JavaScript debugging
        if (!configService.isProductionMode() && !configService.isProfileMode()) {
            addJSDebugScript(headElements);
        }
    }

    protected void addSetupScript(Control source) {
        List<Element> headElements = source.getHeadElements();

        // Check if user set a custom setup script
        JsScript script = getSetupScript();

        if (script != null) {
            // Set the user defined setup script

            if (script.getTemplate() != null) {
                // Get template model
                Map<String, Object> templateModel = createTemplateModel(page, source, getContext());

                // Copy script model over templateModel
                Map<String, Object> scriptModel = script.getModel();
                if (scriptModel != null) {
                    templateModel.putAll(scriptModel);
                }

                // Set templateModel as script model
                script.setModel(templateModel);
            }

            // First remove the script to cater for stateful pages (Seems strange
            // to remove and add the same instance, but JsScript is compared
            // against it's ID attribute
            headElements.remove(script);
            headElements.add(script);

        } else {
            // Create a default setup script
            script = createSetupScript();

            // EventType is immutable so should be ok for subsequent requests
            script.setId(getSetupScriptId(source, getEventType()));

            headElements.remove(script);

            setupScript(script, source);

            headElements.add(script);
        }
    }

    protected void setupScript(JsScript script, Control source) {
        Map templateModel = createTemplateModel(page, source, getContext());
        templateModel.remove("context");
        String json = new JSONWriter().write(templateModel);

        HtmlStringBuffer buffer = new HtmlStringBuffer();
        buffer.append("jQuery(document).ready(function(){");
        buffer.append("Click.jq.ajaxTemplate(");
        buffer.append(json);
        buffer.append(");");
        buffer.append("});");

        script.setContent(buffer.toString());
    }

    protected JsScript createSetupScript() {
        JsScript script = new JsScript();
        // Script should execute each time in case properties changed.
        script.setRenderId(false);
        return script;
    }

    protected void addModel(Map<String, Object> model, String key, Object value, Page page, Context context) {
        Object pop = model.put(key, value);
        if (pop != null && page != null && !page.isStateful()) {
            ConfigService configService = ClickUtils.getConfigService(context.getServletContext());
            LogService logger = configService.getLogService();

            String msg = page.getClass().getName() + " on " + page.getPath()
                    + " model contains an object keyed with reserved " + "name \"" + key
                    + "\". The behavior model object " + pop + " has been replaced with the " + key + "object";
            logger.warn(msg);
        }
    }

    protected String getSetupScriptId(Control source, String event) {
        StringBuilder builder = new StringBuilder();
        builder.append(getSetupScriptId());

        String postfix = source.getId();
        if (postfix == null) {
            postfix = source.getName();
        }

        if (postfix != null) {
            builder.append('_');
            builder.append(postfix);
        }

        builder.append('_');
        builder.append(event);
        return builder.toString();
    }

    /**
     * Returns the <tt>Locale</tt> that should be used in this behavior. The
     * returned locale must be present in the list of {@link #SUPPORTED_LANGUAGES}.
     * <p/>
     * If a locale is not currently supported you can set the
     * {@link #SUPPORTED_LANGUAGES} manually.
     *
     * @return the locale that should be used in this behavior
     */
    protected Locale getLocale() {
        Locale locale = null;

        locale = getContext().getLocale();
        String lang = locale.getLanguage();
        if (Arrays.binarySearch(SUPPORTED_LANGUAGES, lang) >= 0) {
            return locale;
        }

        locale = Locale.getDefault();
        lang = locale.getLanguage();
        if (Arrays.binarySearch(SUPPORTED_LANGUAGES, lang) >= 0) {
            return locale;
        }

        return Locale.ENGLISH;
    }

}