001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thomas Roger
018 */
019
020package org.nuxeo.ecm.platform.ui.web.component.message;
021
022import java.io.IOException;
023import java.util.Iterator;
024
025import javax.faces.application.FacesMessage;
026import javax.faces.component.UIComponent;
027import javax.faces.component.UIMessages;
028import javax.faces.context.FacesContext;
029import javax.faces.context.ResponseWriter;
030import javax.faces.event.AbortProcessingException;
031import javax.faces.event.ComponentSystemEvent;
032import javax.faces.event.ComponentSystemEventListener;
033import javax.faces.event.ListenerFor;
034import javax.faces.event.PostAddToViewEvent;
035
036import org.apache.commons.text.StringEscapeUtils;
037import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
038import org.nuxeo.runtime.api.Framework;
039
040import com.sun.faces.renderkit.html_basic.MessagesRenderer;
041
042/**
043 * Handles rendering of {@link javax.faces.application.FacesMessage} through jQuery Ambiance plugin.
044 *
045 * @since 5.7.3
046 */
047@ListenerFor(systemEventClass = PostAddToViewEvent.class)
048public class NXMessagesRenderer extends MessagesRenderer implements ComponentSystemEventListener {
049
050    public static final String RENDERER_TYPE = "javax.faces.NXMessages";
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            String message = "";
139            if (showDetail) {
140                message = detail;
141            }
142            String scriptContent = new StringBuilder().append("jQuery(document).ready(function() {\n")
143                                                      .append("  jQuery.ambiance({\n")
144                                                      .append("    message: \"")
145                                                      .append(StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(message)))
146                                                      .append("\",\n")
147                                                      .append("    title: \"")
148                                                      .append(StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(summary)))
149                                                      .append("\",\n")
150                                                      .append("    type: \"")
151                                                      .append(errorType)
152                                                      .append("\",\n")
153                                                      .append("    className: \"")
154                                                      .append(severityStyleClass)
155                                                      .append("\",\n")
156                                                      .append("    timeout: \"")
157                                                      .append(timeout)
158                                                      .append("\"")
159                                                      .append("  })\n")
160                                                      .append("});\n")
161                                                      .toString();
162            writer.writeText(scriptContent, null);
163            writer.endElement("script");
164        }
165    }
166
167    /*
168     * When this method is called, we know that there is a component with a script renderer somewhere in the view. We
169     * need to make it so that when an element with a name given by the value of the optional "target" component
170     * attribute is encountered, this component can be called upon to render itself. This method will add the component
171     * (associated with this Renderer) to a facet in the view only if a "target" component attribute is set.
172     */
173    public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
174        UIComponent component = event.getComponent();
175        if (ComponentUtils.isRelocated(component)) {
176            return;
177        }
178        String target = verifyTarget((String) component.getAttributes().get("target"));
179        if (target != null) {
180            ComponentUtils.relocate(component, target, null);
181        }
182    }
183
184    protected String verifyTarget(String toVerify) {
185        return ComponentUtils.verifyTarget(toVerify, toVerify);
186    }
187
188}