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