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