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}