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.web.resources.jsf;
018
019import java.io.IOException;
020import java.io.UnsupportedEncodingException;
021import java.net.URLEncoder;
022import java.util.ArrayList;
023import java.util.Map;
024
025import javax.faces.application.FacesMessage;
026import javax.faces.application.ProjectStage;
027import javax.faces.application.Resource;
028import javax.faces.component.UIComponent;
029import javax.faces.component.UIParameter;
030import javax.faces.context.FacesContext;
031
032import org.apache.commons.lang.StringUtils;
033import org.nuxeo.ecm.web.resources.api.ResourceType;
034import org.nuxeo.ecm.web.resources.api.service.WebResourceManager;
035import org.nuxeo.runtime.api.Framework;
036
037import com.sun.faces.config.WebConfiguration;
038import com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.Param;
039
040/**
041 * Base class for web resources resolution, factoring out helper methods for resources retrieval.
042 *
043 * @since 7.3
044 */
045public abstract class AbstractResourceRenderer extends ScriptStyleBaseRenderer {
046
047    /**
048     * @since 7.10
049     */
050    public static final String BUNDLE_ENDPOINT_PATH = "/wro/api/v1/resource/bundle/";
051
052    /**
053     * @deprecated since 7.10, use {@link #BUNDLE_ENDPOINT_PATH} instead.
054     */
055    @Deprecated
056    public static final String ENDPOINT_PATH = BUNDLE_ENDPOINT_PATH;
057
058    /**
059     * @since 7.10
060     */
061    public static final String PAGE_ENDPOINT_PATH = "/wro/api/v1/resource/page/";
062
063    public static final String COMPONENTS_PATH = "/bower_components/";
064
065    protected static final Param[] EMPTY_PARAMS = new Param[0];
066
067    /**
068     * Resolve url either from src, looking up resource in the war, either from JSF resources, given a name (and
069     * optional library).
070     */
071    protected String resolveUrl(FacesContext context, UIComponent component) throws IOException {
072        Map<String, Object> attributes = component.getAttributes();
073        String src = (String) attributes.get("src");
074        String url;
075        if (src != null) {
076            url = resolveResourceFromSource(context, component, src);
077        } else {
078            String name = (String) attributes.get("name");
079            String library = (String) attributes.get("library");
080            url = resolveResourceUrl(context, component, library, name);
081        }
082        return url;
083    }
084
085    protected String resolveResourceFromSource(FacesContext context, UIComponent component, String src)
086            throws UnsupportedEncodingException {
087        String value = context.getApplication().getViewHandler().getResourceURL(context, src);
088        return getUrlWithParams(context, component, value);
089    }
090
091    protected org.nuxeo.ecm.web.resources.api.Resource resolveNuxeoResource(FacesContext context, UIComponent component,
092            String resource) throws UnsupportedEncodingException {
093        WebResourceManager wrm = Framework.getService(WebResourceManager.class);
094        return wrm.getResource(resource);
095    }
096
097    protected String resolveNuxeoResourcePath(org.nuxeo.ecm.web.resources.api.Resource resource) {
098        if (resource == null) {
099            return null;
100        }
101        String name = resource.getName();
102        if (ResourceType.css.matches(resource)) {
103            String suffixed = name;
104            if (!suffixed.endsWith(ResourceType.css.getSuffix())) {
105                suffixed += ResourceType.css.getSuffix();
106            }
107            return BUNDLE_ENDPOINT_PATH + suffixed;
108        } else if (ResourceType.js.matches(resource)) {
109            String suffixed = name;
110            if (!suffixed.endsWith(ResourceType.js.getSuffix())) {
111                suffixed += ResourceType.js.getSuffix();
112            }
113            return BUNDLE_ENDPOINT_PATH + suffixed;
114        } else if (ResourceType.html.matches(resource)) {
115            // assume html resources are copied to the war "components" sub-directory for now
116            return COMPONENTS_PATH + resource.getPath();
117        }
118        // fallback on URI
119        return resource.getURI();
120    }
121
122    protected String resolveNuxeoResourceUrl(FacesContext context, UIComponent component, String uri)
123            throws UnsupportedEncodingException {
124        String value = context.getApplication().getViewHandler().getResourceURL(context, uri);
125        return getUrlWithParams(context, component, value);
126    }
127
128    protected String resolveResourceUrl(FacesContext context, UIComponent component, String library, String name) {
129        Map<Object, Object> contextMap = context.getAttributes();
130
131        String key = name + library;
132
133        if (null == name) {
134            return null;
135        }
136
137        // Ensure this import is not rendered more than once per request
138        if (contextMap.containsKey(key)) {
139            return null;
140        }
141        contextMap.put(key, Boolean.TRUE);
142
143        // Special case of scripts that have query strings
144        // These scripts actually use their query strings internally, not externally
145        // so we don't need the resource to know about them
146        int queryPos = name.indexOf("?");
147        String query = null;
148        if (queryPos > -1 && name.length() > queryPos) {
149            query = name.substring(queryPos + 1);
150            name = name.substring(0, queryPos);
151        }
152
153        Resource resource = context.getApplication().getResourceHandler().createResource(name, library);
154
155        String resourceSrc = "RES_NOT_FOUND";
156
157        WebConfiguration webConfig = WebConfiguration.getInstance();
158
159        if (library == null && name != null && name.startsWith(
160                webConfig.getOptionValue(WebConfiguration.WebContextInitParameter.WebAppContractsDirectory))) {
161
162            if (context.isProjectStage(ProjectStage.Development)) {
163
164                String msg = "Illegal path, direct contract references are not allowed: " + name;
165                context.addMessage(component.getClientId(context),
166                        new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
167            }
168            resource = null;
169        }
170
171        if (resource == null) {
172
173            if (context.isProjectStage(ProjectStage.Development)) {
174                String msg = "Unable to find resource " + (library == null ? "" : library + ", ") + name;
175                context.addMessage(component.getClientId(context),
176                        new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
177            }
178
179        } else {
180            resourceSrc = resource.getRequestPath();
181            if (query != null) {
182                resourceSrc = resourceSrc + ((resourceSrc.indexOf("?") > -1) ? "&amp;" : "?") + query;
183            }
184            resourceSrc = context.getExternalContext().encodeResourceURL(resourceSrc);
185        }
186
187        return resourceSrc;
188    }
189
190    protected String getUrlWithParams(FacesContext context, UIComponent component, String src)
191            throws UnsupportedEncodingException {
192        // Write Anchor attributes
193
194        Param paramList[] = getParamList(component);
195        StringBuffer sb = new StringBuffer();
196        sb.append(src);
197        boolean paramWritten = false;
198        for (int i = 0, len = paramList.length; i < len; i++) {
199            String pn = paramList[i].name;
200            if (pn != null && pn.length() != 0) {
201                String pv = paramList[i].value;
202                sb.append((paramWritten) ? '&' : '?');
203                sb.append(URLEncoder.encode(pn, "UTF-8"));
204                sb.append('=');
205                if (pv != null && pv.length() != 0) {
206                    sb.append(URLEncoder.encode(pv, "UTF-8"));
207                }
208                paramWritten = true;
209            }
210        }
211
212        return context.getExternalContext().encodeResourceURL(sb.toString());
213    }
214
215    protected Param[] getParamList(UIComponent command) {
216        String flavor = (String) command.getAttributes().get("flavor");
217        if (StringUtils.isNotBlank(flavor) || command.getChildCount() > 0) {
218            ArrayList<Param> parameterList = new ArrayList<Param>();
219            if (StringUtils.isNotBlank(flavor)) {
220                Param param = new Param("flavor", flavor);
221                parameterList.add(param);
222            }
223            for (UIComponent kid : command.getChildren()) {
224                if (kid instanceof UIParameter) {
225                    UIParameter uiParam = (UIParameter) kid;
226                    if (!uiParam.isDisable()) {
227                        Object value = uiParam.getValue();
228                        Param param = new Param(uiParam.getName(), (value == null ? null : value.toString()));
229                        parameterList.add(param);
230                    }
231                }
232            }
233            return parameterList.toArray(new Param[parameterList.size()]);
234        } else {
235            return EMPTY_PARAMS;
236        }
237    }
238
239}