001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thomas Roger
016 */
017
018package org.nuxeo.ecm.platform.ui.web.component.message;
019
020import java.io.IOException;
021import java.util.Iterator;
022
023import javax.faces.application.FacesMessage;
024import javax.faces.component.UIComponent;
025import javax.faces.component.UIMessages;
026import javax.faces.context.FacesContext;
027import javax.faces.context.ResponseWriter;
028import javax.faces.event.AbortProcessingException;
029import javax.faces.event.ComponentSystemEvent;
030import javax.faces.event.ComponentSystemEventListener;
031import javax.faces.event.ListenerFor;
032import javax.faces.event.PostAddToViewEvent;
033
034import org.apache.commons.lang.StringEscapeUtils;
035import org.apache.commons.lang.StringUtils;
036import org.nuxeo.runtime.api.Framework;
037
038import com.sun.faces.renderkit.html_basic.MessagesRenderer;
039
040/**
041 * Handles rendering of {@link javax.faces.application.FacesMessage} through jQuery Ambiance plugin.
042 *
043 * @since 5.7.3
044 */
045@ListenerFor(systemEventClass = PostAddToViewEvent.class)
046public class NXMessagesRenderer extends MessagesRenderer implements ComponentSystemEventListener {
047
048    public static final String RENDERER_TYPE = "javax.faces.NXMessages";
049
050    private static final String COMP_KEY = NXMessagesRenderer.class.getName() + "_COMPOSITE_COMPONENT";
051
052    @Override
053    public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
054        rendererParamsNotNull(context, component);
055
056        if (!shouldEncode(component)) {
057            return;
058        }
059
060        // If id is user specified, we must render
061        boolean mustRender = shouldWriteIdAttribute(component);
062
063        UIMessages messages = (UIMessages) component;
064        ResponseWriter writer = context.getResponseWriter();
065        assert(writer != null);
066
067        String clientId = ((UIMessages) component).getFor();
068        // if no clientId was included
069        if (clientId == null) {
070            // and the author explicitly only wants global messages
071            if (messages.isGlobalOnly()) {
072                // make it so only global messages get displayed.
073                clientId = "";
074            }
075        }
076
077        // "for" attribute optional for Messages
078        Iterator<?> messageIter = getMessageIter(context, clientId, component);
079
080        assert(messageIter != null);
081
082        if (!messageIter.hasNext()) {
083            if (mustRender) {
084                // no message to render, but must render anyway
085                // but if we're writing the dev stage messages,
086                // only write it if messages exist
087                if ("javax_faces_developmentstage_messages".equals(component.getId())) {
088                    return;
089                }
090                writer.startElement("div", component);
091                writeIdAttributeIfNecessary(context, writer, component);
092                writer.endElement("div");
093            } // otherwise, return without rendering
094            return;
095        }
096
097        boolean showDetail = messages.isShowDetail();
098
099        while (messageIter.hasNext()) {
100            FacesMessage curMessage = (FacesMessage) messageIter.next();
101            if (curMessage.isRendered() && !messages.isRedisplay()) {
102                continue;
103            }
104            curMessage.rendered();
105
106            // make sure we have a non-null value for summary and
107            // detail.
108            String summary = (null != (summary = curMessage.getSummary())) ? summary : "";
109            // Default to summary if we have no detail
110            String detail = (null != (detail = curMessage.getDetail())) ? detail : summary;
111
112            String severityStyleClass = null;
113            String errorType = "default";
114            long timeout = 5;
115            if (curMessage.getSeverity() == FacesMessage.SEVERITY_INFO) {
116                severityStyleClass = (String) component.getAttributes().get("infoClass");
117                errorType = "info";
118            } else if (curMessage.getSeverity() == FacesMessage.SEVERITY_WARN) {
119                severityStyleClass = (String) component.getAttributes().get("warnClass");
120                errorType = "warn";
121            } else if (curMessage.getSeverity() == FacesMessage.SEVERITY_ERROR) {
122                severityStyleClass = (String) component.getAttributes().get("errorClass");
123                errorType = "error";
124                timeout = 0;
125            } else if (curMessage.getSeverity() == FacesMessage.SEVERITY_FATAL) {
126                severityStyleClass = (String) component.getAttributes().get("fatalClass");
127                errorType = "fatal";
128                timeout = 0;
129            }
130
131            // ensure message stays visible when running tests
132            if (Framework.getProperty("org.nuxeo.ecm.tester.name") != null) {
133                timeout = 0;
134            }
135
136            writer.startElement("script", messages);
137            writer.writeAttribute("type", "text/javascript", null);
138
139            String scriptContent = "jQuery(document).ready(function() {\n" + "  jQuery.ambiance({\n" + "    "
140                    + "message: \"%s\",\n" + "    title: \"%s\",\n" + "    type: \"%s\",\n" + "    className: \"%s\",\n"
141                    + "    timeout: \"%d\"" + "  })\n" + "});\n";
142            String formattedScriptContent;
143            if (showDetail) {
144                formattedScriptContent = String.format(scriptContent, StringEscapeUtils.escapeJavaScript(detail),
145                        StringEscapeUtils.escapeJavaScript(summary), errorType, severityStyleClass, timeout);
146            } else {
147                formattedScriptContent = String.format(scriptContent, "", StringEscapeUtils.escapeJavaScript(summary),
148                        errorType, severityStyleClass, timeout);
149            }
150            writer.writeText(formattedScriptContent, null);
151            writer.endElement("script");
152
153        }
154    }
155
156    /*
157     * When this method is called, we know that there is a component with a script renderer somewhere in the view. We
158     * need to make it so that when an element with a name given by the value of the optional "target" component
159     * attribute is encountered, this component can be called upon to render itself. This method will add the component
160     * (associated with this Renderer) to a facet in the view only if a "target" component attribute is set.
161     */
162    public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
163        UIComponent component = event.getComponent();
164        FacesContext context = FacesContext.getCurrentInstance();
165
166        String target = verifyTarget((String) component.getAttributes().get("target"));
167        if (target != null) {
168            // We're checking for a composite component here as if the resource
169            // is relocated, it may still require it's composite component context
170            // in order to properly render. Store it for later use by
171            // encodeBegin() and encodeEnd().
172            UIComponent cc = UIComponent.getCurrentCompositeComponent(context);
173            if (cc != null) {
174                component.getAttributes().put(COMP_KEY, cc.getClientId(context));
175            }
176            context.getViewRoot().addComponentResource(context, component, target);
177
178        }
179    }
180
181    protected String verifyTarget(String toVerify) {
182        if (StringUtils.isBlank(toVerify)) {
183            return null;
184        }
185        FacesContext context = FacesContext.getCurrentInstance();
186        boolean ajaxRequest = context.getPartialViewContext().isAjaxRequest();
187        if (ajaxRequest) {
188            // ease up ajax re-rendering in case of js scripts parsing defer
189            return null;
190        }
191        return toVerify;
192    }
193
194}