com.liferay.faces.portal.component.inputsearch.internal.InputSearchRenderer.java Source code

Java tutorial

Introduction

Here is the source code for com.liferay.faces.portal.component.inputsearch.internal.InputSearchRenderer.java

Source

/**
 * Copyright (c) 2000-2015 Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */
package com.liferay.faces.portal.component.inputsearch.internal;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.el.MethodExpression;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.AjaxBehavior;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.html.HtmlCommandButton;
import javax.faces.component.html.HtmlInputText;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.MethodExpressionActionListener;
import javax.faces.event.PreRenderComponentEvent;
import javax.faces.render.FacesRenderer;

import com.liferay.faces.portal.component.inputsearch.InputSearch;
import com.liferay.faces.portal.render.internal.DelayedPortalTagRenderer;
import com.liferay.faces.util.event.PreRenderComponentEventListener;
import com.liferay.faces.util.logging.Logger;
import com.liferay.faces.util.logging.LoggerFactory;
import com.liferay.faces.util.render.RendererUtil;

import com.liferay.portal.kernel.xml.Attribute;
import com.liferay.portal.kernel.xml.Document;
import com.liferay.portal.kernel.xml.Element;
import com.liferay.portal.kernel.xml.Node;
import com.liferay.portal.kernel.xml.SAXReaderUtil;

import com.liferay.taglib.ui.InputSearchTag;

/**
 * This class is a renderer for the {@link com.liferay.faces.portal.component.inputsearch.InputSearch} component. The
 * component has unique requirements in the sense that the corresponding JSP-based {@link
 * com.liferay.taglib.ui.InputSearchTag} renders an <input type="text">...</input> and also a <button></button> for
 * submission. From a JSF perspective, this creates a multiple-inheritance dilemma. For example, should the component
 * component extend {@link javax.faces.component.UIInput} or {@link javax.faces.component.UICommand}? The solution is to
 * have the component extend {@link javax.faces.component.UIInput}, but to dynamically create an {@link
 * javax.faces.component.html.HtmlCommandButton} child that can participate in the processing of JSF events. This design
 * is essentially a 100% Java equivalent of a JSF composite component.
 *
 * @author  Juan Gonzalez
 */
//J-
@FacesRenderer(componentFamily = InputSearch.COMPONENT_FAMILY, rendererType = InputSearch.RENDERER_TYPE)
//J+
public class InputSearchRenderer extends DelayedPortalTagRenderer<InputSearch, InputSearchTag>
        implements PreRenderComponentEventListener {

    // Logger
    private static final Logger logger = LoggerFactory.getLogger(InputSearchRenderer.class);

    @Override
    public void decode(FacesContext facesContext, UIComponent uiComponent) {

        RendererUtil.decodeClientBehaviors(facesContext, uiComponent);

        ExternalContext externalContext = facesContext.getExternalContext();
        Map<String, String> requestParameterMap = externalContext.getRequestParameterMap();
        String clientId = uiComponent.getClientId();
        String submittedValue = requestParameterMap.get(clientId);
        InputSearch inputSearch = cast(uiComponent);
        inputSearch.setSubmittedValue(submittedValue);
    }

    @Override
    public void encodeBegin(FacesContext facesContext, UIComponent uiComponent) throws IOException {

        ResponseWriter responseWriter = facesContext.getResponseWriter();
        responseWriter.startElement("div", uiComponent);
        responseWriter.writeAttribute("class", "form-search", "class");

        // Delegate to PortalTagRenderer so that the JSP tag output will get encoded.
        super.encodeBegin(facesContext, uiComponent);
    }

    @Override
    public void encodeEnd(FacesContext facesContext, UIComponent uiComponent) throws IOException {

        // Delegate to PortalTagRenderer so that the JSP tag output will get encoded.
        super.encodeEnd(facesContext, uiComponent);

        // Encode the closing </div> element
        ResponseWriter responseWriter = facesContext.getResponseWriter();
        responseWriter.endElement("div");
    }

    @Override
    public InputSearchTag newTag() {
        return new InputSearchTag();
    }

    @Override
    public void processEvent(PreRenderComponentEvent preRenderComponentEvent) throws AbortProcessingException {
        dynamicallyAddChildComponents((InputSearch) preRenderComponentEvent.getComponent());
    }

    @Override
    protected InputSearch cast(UIComponent uiComponent) {
        return (InputSearch) uiComponent;
    }

    protected void changeClientBehaviorIds(ClientBehavior clientBehavior, String id) {

        // Determine whether or not the developer added an f:ajax child tag.
        if (clientBehavior instanceof AjaxBehavior) {

            // Add the element Id to the list of components that participate in the "execute" portion
            // of the JSF partial request lifecycle.
            AjaxBehavior ajaxBehavior = (AjaxBehavior) clientBehavior;
            Collection<String> execute = new ArrayList<String>();
            execute.addAll(ajaxBehavior.getExecute());

            if (execute.contains("@this") || !execute.contains(id)) {
                execute.add(id);
                ajaxBehavior.setExecute(execute);
            }

            // Add the element Id to the list of components that participate in the "render" portion
            // of the JSF partial request lifecycle.
            Collection<String> render = new ArrayList<String>();
            render.addAll(ajaxBehavior.getRender());

            if (render.contains("@this")) {
                render.remove("@this");
                render.add(id);
                ajaxBehavior.setRender(render);
            }
        }
    }

    @Override
    protected void copyFrameworkAttributes(FacesContext facesContext, InputSearch inputSearch,
            InputSearchTag inputSearchTag) {

        inputSearchTag.setCssClass(inputSearch.getStyleClass());
        inputSearchTag.setId(inputSearch.getClientId());
        inputSearchTag.setName(inputSearch.getClientId());
        inputSearchTag.setAutoFocus(inputSearch.isAutoFocus());

        if (inputSearch.getButtonLabel() != null) {
            inputSearchTag.setButtonLabel(inputSearch.getButtonLabel());
        }

        if (inputSearch.getPlaceholder() != null) {
            inputSearchTag.setPlaceholder(inputSearch.getPlaceholder());
        }

        inputSearchTag.setShowButton(inputSearch.isShowButton());

        if (inputSearch.getTitle() != null) {
            inputSearchTag.setTitle(inputSearch.getTitle());
        }
    }

    @Override
    protected void copyNonFrameworkAttributes(FacesContext facesContext, InputSearch u, InputSearchTag t) {
        // no-op
    }

    // The purpose of this method is to dynamically add HtmlInputText and HtmlCommandButton JSF child components. These
    // children will render child <input>..</input> elements with attributes like "id","name", "onclick", etc. that need
    // to be copied to the HTML elements that are rendered by html/taglib/ui/input_search/page.jsp
    protected void dynamicallyAddChildComponents(InputSearch inputSearch) {

        // If the HtmlInputText and HtmlCommandButton JSF child components are not already present in the component
        // tree, then
        List<UIComponent> children = inputSearch.getChildren();

        if (children.size() == 0) {

            // Dynamically create the HtmlInputText JSF child
            FacesContext facesContext = FacesContext.getCurrentInstance();
            Application application = facesContext.getApplication();

            HtmlInputText htmlInputText = (HtmlInputText) application.createComponent(HtmlInputText.COMPONENT_TYPE);
            children.add(htmlInputText);

            // Dynamically create the HtmlCommandButton JSF child and set JSF attributes accordingly.
            HtmlCommandButton htmlCommandButton = (HtmlCommandButton) application
                    .createComponent(HtmlCommandButton.COMPONENT_TYPE);
            children.add(htmlCommandButton);

            MethodExpression action = inputSearch.getAction();

            if (action != null) {
                htmlCommandButton.setActionExpression(action);
            }

            MethodExpression actionListener = inputSearch.getActionListener();

            if (actionListener != null) {
                htmlCommandButton.addActionListener(new MethodExpressionActionListener(actionListener));
            }

            boolean showButton = inputSearch.isShowButton();

            if (!showButton) {
                htmlCommandButton.setRendered(false);
            }

            // For each client behavior associated with the InputSearch component:
            Map<String, List<ClientBehavior>> clientBehaviours = inputSearch.getClientBehaviors();
            Set<Map.Entry<String, List<ClientBehavior>>> entries = clientBehaviours.entrySet();
            boolean hasButtonAjaxBehavior = false;

            for (Map.Entry<String, List<ClientBehavior>> mapEntry : entries) {

                // If the developer did not specify an event, then that means the "action" (default event name for
                // HtmlCommandButton) is implied.
                List<ClientBehavior> clientBehaviors = mapEntry.getValue();
                String eventName = mapEntry.getKey();
                String defaultEventName = inputSearch.getDefaultEventName();

                // If processing the "action" event, then
                if (eventName.equals(defaultEventName)) {

                    // For each client behavior associated with the "action" event:
                    for (ClientBehavior clientBehavior : clientBehaviors) {

                        hasButtonAjaxBehavior = true;

                        changeClientBehaviorIds(clientBehavior, inputSearch.getId());

                        htmlCommandButton.addClientBehavior(defaultEventName, clientBehavior);
                    }
                }

                // Otherwise, add the client behavior to the HtmlInputText JSF child so that attributes will be rendered
                // by the renderer.
                else {

                    for (ClientBehavior clientBehavior : clientBehaviors) {
                        changeClientBehaviorIds(clientBehavior, inputSearch.getId());

                        htmlInputText.addClientBehavior(eventName, clientBehavior);
                    }
                }
            }

            // If the developer has specified an action/actionListener or has enabled Ajax, then log an error indicating
            // that the developer must set showButton="true".
            if (!showButton && (hasButtonAjaxBehavior || (action != null) || (actionListener != null))) {
                logger.error("Set showButton=\"true\" when using action/actionListener or f:ajax.");
            }

            inputSearch.markInitialState();
        }

    }

    @Override
    public String getChildInsertionMarker() {
        return "</div>";
    }

    @Override
    protected StringBuilder getMarkup(UIComponent uiComponent, StringBuilder markup) throws Exception {

        //J-
        // NOTE: The specified markup looks like the following (HTML comments added for clarity):
        //
        // <!-- Opening <div> rendered by html/taglib/ui/input_search/page.jsp -->
        // <div class="input-append">
        //
        //    <!-- Input text field rendered by html/taglib/ui/input_search/page.jsp -->
        //    <input class="search-query span9" id="...:jid42" name="..." placeholder="" type="text" value="" />
        //
        //    <!-- Search button rendered by html/taglib/ui/input_search/page.jsp -->
        //    <button class="btn" type="submit">Search</button>
        //
        //    <!-- HtmlInputText (dynamically added JSF child component) -->
        //    <input type="text" name="...:htmlInputText" />
        //
        //    <!-- HtmlCommandButton (dynamically added JSF child component) -->
        //    <input type="submit" name="...:htmlCommandButton" value="" />
        //
        // <!-- Closing </div> rendered by html/taglib/ui/input_search/page.jsp -->
        // </div>
        //J+

        // Parse the generated markup as an XML document.
        InputSearch inputSearch = cast(uiComponent);
        Document document = SAXReaderUtil.read(markup.toString());
        Element rootElement = document.getRootElement();

        // Locate the first/main input element in the XML document. This is the one that will contain contain the value
        // that will be submitted in the postback and received by the decode(FacesContext, UIComponent) method).
        String xpathInput = "//input[contains(@id, '" + uiComponent.getClientId() + "')]";
        Element mainInputElement = (Element) rootElement.selectSingleNode(xpathInput);

        if (mainInputElement != null) {

            // Copy the value attribute of the InputSearch component to the first input element in the XML document.
            mainInputElement.attribute("value").setValue((String) inputSearch.getValue());

            // Locate the dynamically added HtmlInputText and HtmlCommandButton child components.
            String xpathInputs = "//input[@type='text']";
            List<Node> inputElements = rootElement.selectNodes(xpathInputs);

            if ((inputElements != null) && (inputElements.size() == 2)) {

                // Copy each "on" attribute name/value pairs from the HtmlInputText to the first input element in
                // the XML document. This will enable all of the AjaxBehavior events like keyup/keydown to work.
                Element htmlInputTextElement = (Element) inputElements.get(1);
                Iterator<Attribute> attributeIterator = htmlInputTextElement.attributeIterator();

                while (attributeIterator.hasNext()) {
                    Attribute attribute = attributeIterator.next();
                    String attributeName = attribute.getName();

                    if (attributeName.startsWith("on")) {
                        mainInputElement.addAttribute(attributeName, attribute.getValue());
                    }
                }

                // Remove the HtmlInputText <input> from the XML document so that only one text field is rendered.
                htmlInputTextElement.getParent().remove(htmlInputTextElement);
            }
        }

        // Locate the HtmlCommandButton in the XML document.
        List<UIComponent> children = uiComponent.getChildren();
        HtmlCommandButton htmlCommandButton = (HtmlCommandButton) children.get(1);
        String htmlCommandButtonClientId = htmlCommandButton.getClientId();

        // Note that if there is an AjaxBehavior, then the rendered HtmlCommandButton can be located in the XML document
        // via the name attribute. Otherwise it can be located in the XML document via the id attribute.
        String htmlCommandButtonXPath = "//input[contains(@name,'" + htmlCommandButtonClientId
                + "') and @type='submit']";
        Element htmlCommandButtonElement = (Element) rootElement.selectSingleNode(htmlCommandButtonXPath);

        if (htmlCommandButtonElement == null) {
            htmlCommandButtonXPath = "//input[contains(@id,'" + htmlCommandButtonClientId
                    + "') and @type='submit']";
            htmlCommandButtonElement = (Element) rootElement.selectSingleNode(htmlCommandButtonXPath);
        }

        if (htmlCommandButtonElement != null) {

            // Locate the <button> element in the XML document that was rendered by page.jsp
            Element buttonElement = (Element) rootElement.selectSingleNode("//button[@type='submit']");

            if (buttonElement != null) {

                // Copy attributes found on the HtmlCommandButton <input> element to the <button> element that was
                // rendered by page.jsp
                Attribute onClickAttr = htmlCommandButtonElement.attribute("onclick");

                if (onClickAttr != null) {
                    buttonElement.addAttribute("onclick", onClickAttr.getValue());
                }

                Attribute nameAttr = htmlCommandButtonElement.attribute("name");

                if (nameAttr != null) {
                    buttonElement.addAttribute("name", nameAttr.getValue());
                }

                Attribute idAttr = htmlCommandButtonElement.attribute("id");

                if (idAttr != null) {
                    buttonElement.addAttribute("id", idAttr.getValue());
                }

                // Remove the HtmlCommandButton <input> from the XML document so that only one button is rendered.
                htmlCommandButtonElement.getParent().remove(htmlCommandButtonElement);
            }
        }

        // Returned the modified verson of the specified markup.
        return new StringBuilder(rootElement.asXML());
    }
}