001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 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 * Anahide Tchertchian 016 */ 017package org.nuxeo.ecm.webapp.resources; 018 019import java.util.ArrayList; 020import java.util.List; 021 022import javax.faces.component.UIComponent; 023import javax.faces.component.UIViewRoot; 024import javax.faces.context.FacesContext; 025import javax.faces.event.AbortProcessingException; 026import javax.faces.event.ComponentSystemEvent; 027import javax.faces.event.ComponentSystemEventListener; 028 029import org.apache.commons.lang.StringUtils; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.nuxeo.ecm.web.resources.api.ResourceType; 033import org.nuxeo.ecm.web.resources.jsf.PageResourceRenderer; 034import org.nuxeo.ecm.web.resources.jsf.ResourceBundleRenderer; 035import org.nuxeo.runtime.api.Framework; 036import org.nuxeo.runtime.services.config.ConfigurationService; 037 038/** 039 * Moves CSS files to the start of the head tag and reorders js resources. 040 * 041 * @since 7.10 042 */ 043public class NuxeoWebResourceDispatcher implements ComponentSystemEventListener { 044 045 private static final Log log = LogFactory.getLog(NuxeoWebResourceDispatcher.class); 046 047 protected static String TARGET_HEAD = "head"; 048 049 protected static String SLOT_HEAD_START = "headstart"; 050 051 private static String SLOT_BODY_START = "bodystart"; 052 053 private static String SLOT_BODY_END = "bodyend"; 054 055 private static String DEFER_JS_PROP = "nuxeo.jsf.deferJavaScriptLoading"; 056 057 public void processEvent(ComponentSystemEvent event) throws AbortProcessingException { 058 FacesContext ctx = FacesContext.getCurrentInstance(); 059 UIViewRoot root = ctx.getViewRoot(); 060 boolean ajaxRequest = ctx.getPartialViewContext().isAjaxRequest(); 061 if (ajaxRequest) { 062 // do not interfere with ajax scripts re-rendering logics 063 List<UIComponent> resources = root.getComponentResources(ctx, TARGET_HEAD); 064 String message = "Head resource %s on ajax request"; 065 for (UIComponent r : resources) { 066 logResourceInfo(r, message); 067 } 068 return; 069 } 070 List<UIComponent> cssResources = new ArrayList<UIComponent>(); 071 List<UIComponent> otherResources = new ArrayList<UIComponent>(); 072 List<UIComponent> resources = root.getComponentResources(ctx, TARGET_HEAD); 073 for (UIComponent r : resources) { 074 if (isCssResource(ctx, r)) { 075 cssResources.add(r); 076 } else { 077 otherResources.add(r); 078 } 079 } 080 081 // avoid relocating CSS on postback 082 moveResources(ctx, root, cssResources, TARGET_HEAD, SLOT_HEAD_START, 083 "Pushing head resource %s at the beggining of head tag"); 084 if (isDeferJavaScriptLoading()) { 085 moveResources(ctx, root, otherResources, TARGET_HEAD, SLOT_BODY_START, 086 "Pushing head resource %s at the beggining of body tag"); 087 } 088 } 089 090 protected void moveResources(FacesContext ctx, UIViewRoot root, List<UIComponent> resources, String removeFrom, 091 String addTo, String message) { 092 // push target resources 093 List<UIComponent> existing = new ArrayList<UIComponent>(root.getComponentResources(ctx, addTo)); 094 for (UIComponent r : resources) { 095 root.removeComponentResource(ctx, r, removeFrom); 096 root.addComponentResource(ctx, r, addTo); 097 logResourceInfo(r, message); 098 } 099 // add them back again for head resources to be still before them 100 for (UIComponent r : existing) { 101 root.addComponentResource(ctx, r, addTo); 102 } 103 } 104 105 protected void logResourceInfo(UIComponent resource, String message) { 106 if (log.isDebugEnabled()) { 107 String name = getLogName(resource); 108 if (StringUtils.isBlank(name)) { 109 log.debug(String.format(message, resource)); 110 } else { 111 log.debug(String.format(message, name)); 112 } 113 } 114 } 115 116 protected String getLogName(UIComponent resource) { 117 String name = (String) resource.getAttributes().get("name"); 118 if (StringUtils.isBlank(name)) { 119 return (String) resource.getAttributes().get("src"); 120 } 121 return name; 122 } 123 124 protected boolean isCssResource(FacesContext ctx, UIComponent r) { 125 String rtype = r.getRendererType(); 126 if ("javax.faces.resource.Stylesheet".equals(rtype)) { 127 return true; 128 } 129 if (ResourceBundleRenderer.RENDERER_TYPE.equals(rtype) || PageResourceRenderer.RENDERER_TYPE.equals(rtype)) { 130 String type = (String) r.getAttributes().get("type"); 131 if (ResourceType.css.equals(type) || ResourceType.jsfcss.equals(type)) { 132 return true; 133 } 134 return false; 135 } 136 String name = (String) r.getAttributes().get("name"); 137 if (name == null) { 138 return false; 139 } 140 name = name.toLowerCase(); 141 if (name.contains(".css") || name.contains(".ecss")) { 142 return true; 143 } 144 return false; 145 } 146 147 public boolean isDeferJavaScriptLoading() { 148 ConfigurationService cs = Framework.getService(ConfigurationService.class); 149 return cs.isBooleanPropertyTrue(DEFER_JS_PROP); 150 } 151 152 public String getHeadStartTarget() { 153 return SLOT_HEAD_START; 154 } 155 156 public String getBodyStartTarget() { 157 return SLOT_BODY_START; 158 } 159 160 public String getBodyEndTarget() { 161 return SLOT_BODY_END; 162 } 163 164 public String getHeadJavaScriptTarget() { 165 return isDeferJavaScriptLoading() ? SLOT_BODY_END : SLOT_BODY_START; 166 } 167 168 public String getBodyJavaScriptTarget() { 169 return isDeferJavaScriptLoading() ? SLOT_BODY_END : null; 170 } 171 172}