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