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}