001/*
002 * (C) Copyright 2006-2013 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id: URLPolicyServiceImpl.java 29556 2008-01-23 00:59:39Z jcarsique $
020 */
021
022package org.nuxeo.ecm.platform.ui.web.rest.services;
023
024import java.util.ArrayList;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.el.ELContext;
030import javax.el.ELException;
031import javax.el.ExpressionFactory;
032import javax.el.MethodExpression;
033import javax.el.ValueExpression;
034import javax.faces.context.FacesContext;
035import javax.servlet.ServletContext;
036import javax.servlet.http.HttpServletRequest;
037import javax.servlet.http.HttpServletResponse;
038
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.nuxeo.common.utils.URIUtils;
043import org.nuxeo.ecm.core.api.DocumentLocation;
044import org.nuxeo.ecm.core.io.download.DownloadService;
045import org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants;
046import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter;
047import org.nuxeo.ecm.platform.ui.web.rest.StaticNavigationHandler;
048import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService;
049import org.nuxeo.ecm.platform.ui.web.rest.descriptors.URLPatternDescriptor;
050import org.nuxeo.ecm.platform.ui.web.rest.descriptors.ValueBindingDescriptor;
051import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
052import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
053import org.nuxeo.ecm.platform.url.api.DocumentView;
054import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
055import org.nuxeo.ecm.platform.url.codec.DocumentFileCodec;
056import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
057import org.nuxeo.runtime.api.Framework;
058
059public class URLPolicyServiceImpl implements URLPolicyService {
060
061    public static final String NAME = URLPolicyServiceImpl.class.getName();
062
063    private static final Log log = LogFactory.getLog(URLPolicyServiceImpl.class);
064
065    // this used to be a codec but now delegates to DownloadService
066    public static final String DOWNLOADFILE_PATTERN = "downloadFile";
067
068    protected final Map<String, URLPatternDescriptor> descriptors;
069
070    protected StaticNavigationHandler viewIdManager;
071
072    public URLPolicyServiceImpl() {
073        // make sure the descriptors list order follows registration order, as
074        // order may have an impact on resolved pattern
075        descriptors = new LinkedHashMap<String, URLPatternDescriptor>();
076    }
077
078    protected List<URLPatternDescriptor> getURLPatternDescriptors() {
079        // TODO: add cache
080        List<URLPatternDescriptor> lst = new ArrayList<URLPatternDescriptor>();
081        for (URLPatternDescriptor desc : descriptors.values()) {
082            if (desc.getEnabled()) {
083                // add default at first
084                if (desc.getDefaultURLPolicy()) {
085                    lst.add(0, desc);
086                } else {
087                    lst.add(desc);
088                }
089            }
090        }
091        return lst;
092    }
093
094    protected URLPatternDescriptor getDefaultPatternDescriptor() {
095        for (URLPatternDescriptor desc : descriptors.values()) {
096            if (desc.getEnabled()) {
097                if (desc.getDefaultURLPolicy()) {
098                    return desc;
099                }
100            }
101        }
102        return null;
103    }
104
105    @Override
106    public String getDefaultPatternName() {
107        URLPatternDescriptor desc = getDefaultPatternDescriptor();
108        if (desc != null) {
109            return desc.getName();
110        }
111        return null;
112    }
113
114    @Override
115    public boolean hasPattern(String name) {
116        URLPatternDescriptor desc = descriptors.get(name);
117        return desc != null;
118    }
119
120    protected static DocumentViewCodecManager getDocumentViewCodecService() {
121        return Framework.getService(DocumentViewCodecManager.class);
122    }
123
124    protected URLPatternDescriptor getURLPatternDescriptor(String patternName) {
125        URLPatternDescriptor desc = descriptors.get(patternName);
126        if (desc == null) {
127            throw new IllegalArgumentException("Unknown pattern " + patternName);
128        }
129        return desc;
130    }
131
132    @Override
133    public boolean isCandidateForDecoding(HttpServletRequest httpRequest) {
134        // only rewrite GET/HEAD URLs
135        String method = httpRequest.getMethod();
136        if (!method.equals("GET") && !method.equals("HEAD")) {
137            return false;
138        }
139
140        // look for appropriate pattern and see if it needs filter
141        // preprocessing
142        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
143        if (desc != null) {
144            return desc.getNeedFilterPreprocessing();
145        }
146        // return default pattern descriptor behaviour
147        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
148        if (defaultPattern != null) {
149            return defaultPattern.getNeedFilterPreprocessing();
150        }
151        return false;
152    }
153
154    @Override
155    public boolean isCandidateForEncoding(HttpServletRequest httpRequest) {
156        Boolean forceEncoding = Boolean.FALSE;
157        Object forceEncodingValue = httpRequest.getAttribute(FORCE_URL_ENCODING_REQUEST_KEY);
158        if (forceEncodingValue instanceof Boolean) {
159            forceEncoding = (Boolean) forceEncodingValue;
160        }
161
162        // only POST access need a redirect,unless with force encoding (this
163        // happens when redirect is triggered after a seam page has been
164        // processed)
165        if (!forceEncoding.booleanValue() && !httpRequest.getMethod().equals("POST")) {
166            return false;
167        }
168
169        Object skipRedirect = httpRequest.getAttribute(NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY);
170        if (skipRedirect instanceof Boolean && ((Boolean) skipRedirect).booleanValue()) {
171            return false;
172        }
173
174        // look for appropriate pattern and see if it needs redirect
175        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
176        if (desc != null) {
177            return desc.getNeedRedirectFilter();
178        }
179        // return default pattern descriptor behaviour
180        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
181        if (defaultPattern != null) {
182            return defaultPattern.getNeedRedirectFilter();
183        }
184        return false;
185    }
186
187    @Override
188    public void setDocumentViewInRequest(HttpServletRequest request, DocumentView docView) {
189        request.setAttribute(NXAuthConstants.REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request));
190        request.setAttribute(DOCUMENT_VIEW_REQUEST_KEY, docView);
191    }
192
193    protected URLPatternDescriptor getURLPatternDescriptor(HttpServletRequest request) {
194        URLPatternDescriptor res = null;
195        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
196            DocumentView docView = getDocumentViewFromRequest(desc.getName(), request);
197            if (docView != null) {
198                res = desc;
199                break;
200            }
201        }
202        // if (res == null && log.isDebugEnabled()) {
203        // log.debug("Could not get url pattern for request "
204        // + request.getRequestURL());
205        // }
206        return res;
207    }
208
209    @Override
210    public DocumentView getDocumentViewFromRequest(HttpServletRequest request) {
211        DocumentView docView = null;
212        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
213            docView = getDocumentViewFromRequest(desc.getName(), request);
214            if (docView != null) {
215                break;
216            }
217        }
218
219        // if (docView == null && log.isDebugEnabled()) {
220        // log.debug("Could not get document view from request "
221        // + request.getRequestURL());
222        // }
223        return docView;
224    }
225
226    @Override
227    public DocumentView getDocumentViewFromRequest(String patternName, HttpServletRequest request) {
228        Object value = request.getAttribute(DOCUMENT_VIEW_REQUEST_KEY);
229        if (value instanceof DocumentView) {
230            DocumentView requestDocView = (DocumentView) value;
231            // check if document view in request was set thanks to this pattern
232            if (patternName.equals(requestDocView.getPatternName())) {
233                return requestDocView;
234            }
235        }
236
237        // try to build it from the request
238        String url;
239        String queryString = request.getQueryString();
240        if (queryString != null) {
241            url = new String(request.getRequestURL() + "?" + queryString);
242        } else {
243            url = new String(request.getRequestURL());
244        }
245        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
246        String codecName = desc.getDocumentViewCodecName();
247        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
248        DocumentView docView = docViewService.getDocumentViewFromUrl(codecName, url, desc.getNeedBaseURL(),
249                BaseURL.getLocalBaseURL(request));
250        if (docView != null) {
251            // set pattern name
252            docView.setPatternName(patternName);
253            // set other parameters as set in the url pattern if docView does
254            // not hold them already
255            Map<String, String> docViewParameters = docView.getParameters();
256            Map<String, String> requestParameters = URIUtils.getRequestParameters(queryString);
257            if (requestParameters != null) {
258                ValueBindingDescriptor[] bindings = desc.getValueBindings();
259                for (ValueBindingDescriptor binding : bindings) {
260                    String paramName = binding.getName();
261                    if (!docViewParameters.containsKey(paramName)) {
262                        Object paramValue = requestParameters.get(paramName);
263                        if (paramValue == null || paramValue instanceof String) {
264                            docView.addParameter(paramName, (String) paramValue);
265                        }
266                    }
267                }
268            }
269        }
270
271        return docView;
272    }
273
274    protected URLPatternDescriptor getURLPatternDescriptor(DocumentView docView) {
275        URLPatternDescriptor res = null;
276        if (docView != null) {
277            String patternName = docView.getPatternName();
278            try {
279                res = getURLPatternDescriptor(patternName);
280            } catch (IllegalArgumentException e) {
281            }
282        }
283        // if (res == null && log.isDebugEnabled()) {
284        // log.debug("Could not get url pattern for document view");
285        // }
286        return res;
287    }
288
289    @Override
290    public String getUrlFromDocumentView(DocumentView docView, String baseUrl) {
291        String url = null;
292        String patternName = docView.getPatternName();
293        if (patternName != null) {
294            // try with original document view pattern
295            URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
296            if (desc != null) {
297                // return corresponding url
298                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
299            }
300        }
301        if (url == null) {
302            // take first matching pattern
303            List<URLPatternDescriptor> descs = getURLPatternDescriptors();
304            for (URLPatternDescriptor desc : descs) {
305                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
306                if (url != null) {
307                    break;
308                }
309            }
310        }
311        // if (url == null && log.isDebugEnabled()) {
312        // log.debug("Could not get url from document view");
313        // }
314        return url;
315    }
316
317    /**
318     * Returns patterns sorted according to:
319     * <ul>
320     * <li>First patterns holding the given view id</li>
321     * <li>The default pattern if it does not hold this view id</li>
322     * <li>Other patterns not holding this view id</li>
323     * </ul>
324     *
325     * @since 5.4.2
326     * @param viewId
327     * @deprecated since 5.5
328     */
329    @Deprecated
330    protected List<URLPatternDescriptor> getSortedURLPatternDescriptorsFor(String viewId) {
331        List<URLPatternDescriptor> sortedDescriptors = new ArrayList<URLPatternDescriptor>();
332        List<URLPatternDescriptor> nonMatchingViewIdDescriptors = new ArrayList<URLPatternDescriptor>();
333
334        List<URLPatternDescriptor> descriptors = getURLPatternDescriptors();
335        for (URLPatternDescriptor descriptor : descriptors) {
336            List<String> handledViewIds = descriptor.getViewIds();
337            if (handledViewIds != null && handledViewIds.contains(viewId)) {
338                sortedDescriptors.add(descriptor);
339            } else {
340                nonMatchingViewIdDescriptors.add(descriptor);
341            }
342        }
343        sortedDescriptors.addAll(nonMatchingViewIdDescriptors);
344        return sortedDescriptors;
345    }
346
347    @Override
348    public String getUrlFromDocumentView(String patternName, DocumentView docView, String baseUrl) {
349        if (DOWNLOADFILE_PATTERN.equals(patternName)) {
350            // this used to be a codec but now delegates to DownloadService
351            DownloadService downloadService = Framework.getService(DownloadService.class);
352            DocumentLocation docLoc = docView.getDocumentLocation();
353            String repositoryName = docLoc.getServerName();
354            String docId = docLoc.getDocRef().toString();
355            String xpath = docView.getParameter(DocumentFileCodec.FILE_PROPERTY_PATH_KEY);
356            String filename = docView.getParameter(DocumentFileCodec.FILENAME_KEY);
357            String url = downloadService.getDownloadUrl(repositoryName, docId, xpath, filename);
358            if (!StringUtils.isBlank(baseUrl)) {
359                if (!baseUrl.endsWith("/")) {
360                    baseUrl += "/";
361                }
362                url = baseUrl + url;
363            }
364            return url;
365        }
366        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
367        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
368        String codecName = desc.getDocumentViewCodecName();
369        return docViewService.getUrlFromDocumentView(codecName, docView, desc.getNeedBaseURL(), baseUrl);
370    }
371
372    @Override
373    public void applyRequestParameters(FacesContext facesContext) {
374        // try to set document view
375        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
376        ELContext context = facesContext.getELContext();
377
378        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
379
380        URLPatternDescriptor pattern = getURLPatternDescriptor(httpRequest);
381        if (pattern == null) {
382            return;
383        }
384
385        DocumentView docView = getDocumentViewFromRequest(pattern.getName(), httpRequest);
386        // pattern applies => document view will not be null
387        if (docView != null) {
388            String documentViewBinding = pattern.getDocumentViewBinding();
389            if (documentViewBinding != null && !"".equals(documentViewBinding)) {
390                // try to set it from custom mapping
391                ValueExpression ve = ef.createValueExpression(context, pattern.getDocumentViewBinding(), Object.class);
392                ve.setValue(context, docView);
393            }
394        }
395
396        Map<String, String> docViewParameters = null;
397        if (docView != null) {
398            docViewParameters = docView.getParameters();
399        }
400        ValueBindingDescriptor[] bindings = pattern.getValueBindings();
401        if (bindings != null && httpRequest.getAttribute(URLPolicyService.DISABLE_ACTION_BINDING_KEY) == null) {
402            for (ValueBindingDescriptor binding : bindings) {
403                if (!binding.getCallSetter()) {
404                    continue;
405                }
406                String paramName = binding.getName();
407                // try doc view parameters
408                Object value = null;
409                if (docViewParameters != null && docViewParameters.containsKey(paramName)) {
410                    value = docView.getParameter(paramName);
411                } else {
412                    // try request attributes
413                    value = httpRequest.getAttribute(paramName);
414                }
415                String expr = binding.getExpression();
416                if (ComponentTagUtils.isValueReference(expr)) {
417                    ValueExpression ve = ef.createValueExpression(context, expr, Object.class);
418                    try {
419                        ve.setValue(context, value);
420                    } catch (ELException e) {
421                        log.error("Could not apply request parameter '" + value + "' to expression '" + expr + "'", e);
422                    }
423                }
424            }
425        }
426    }
427
428    @Override
429    public void appendParametersToRequest(FacesContext facesContext) {
430        appendParametersToRequest(facesContext, null);
431    }
432
433    public void appendParametersToRequest(FacesContext facesContext, String pattern) {
434        // try to get doc view from custom mapping
435        DocumentView docView = null;
436        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
437        ELContext context = facesContext.getELContext();
438        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
439
440        // get existing document view from given pattern, else create it
441        URLPatternDescriptor patternDesc = null;
442        if (pattern != null && !"".equals(pattern)) {
443            patternDesc = getURLPatternDescriptor(pattern);
444        } else {
445            // iterate over pattern descriptors, and take the first one that
446            // applies, or use the default one
447            List<URLPatternDescriptor> descs = getURLPatternDescriptors();
448            boolean applies = false;
449            for (URLPatternDescriptor desc : descs) {
450                String documentViewAppliesExpr = desc.getDocumentViewBindingApplies();
451                if (!StringUtils.isBlank(documentViewAppliesExpr)) {
452                    // TODO: maybe put view id to the request to help writing
453                    // the EL expression
454                    ValueExpression ve = ef.createValueExpression(context, documentViewAppliesExpr, Object.class);
455                    try {
456                        Object res = ve.getValue(context);
457                        if (Boolean.TRUE.equals(res)) {
458                            applies = true;
459                        }
460                    } catch (ELException e) {
461                        if (log.isDebugEnabled()) {
462                            log.debug(String.format("Error executing expression '%s' for " + "url pattern '%s': %s",
463                                    documentViewAppliesExpr, desc.getName(), e.getMessage()));
464                        }
465                    }
466                }
467                if (applies) {
468                    patternDesc = desc;
469                    break;
470                }
471            }
472            if (patternDesc == null) {
473                // default on the default pattern desc
474                patternDesc = getDefaultPatternDescriptor();
475            }
476        }
477        if (patternDesc != null) {
478            // resolved doc view values thanks to bindings
479            Object docViewValue = null;
480            String documentViewBinding = patternDesc.getDocumentViewBinding();
481            if (!StringUtils.isBlank(documentViewBinding)) {
482                ValueExpression ve = ef.createValueExpression(context, documentViewBinding, Object.class);
483                docViewValue = ve.getValue(context);
484            }
485            if (docViewValue == null) {
486                documentViewBinding = patternDesc.getNewDocumentViewBinding();
487                if (!StringUtils.isBlank(documentViewBinding)) {
488                    ValueExpression ve = ef.createValueExpression(context, documentViewBinding, Object.class);
489                    docViewValue = ve.getValue(context);
490                }
491            }
492            if (docViewValue instanceof DocumentView) {
493                docView = (DocumentView) docViewValue;
494                // set pattern name in case it was just created
495                docView.setPatternName(patternDesc.getName());
496                ValueBindingDescriptor[] bindings = patternDesc.getValueBindings();
497                if (bindings != null) {
498                    for (ValueBindingDescriptor binding : bindings) {
499                        if (!binding.getCallGetter()) {
500                            continue;
501                        }
502                        String paramName = binding.getName();
503                        String expr = binding.getExpression();
504                        try {
505                            Object value;
506                            if (ComponentTagUtils.isValueReference(expr)) {
507                                ValueExpression ve = ef.createValueExpression(context, expr, Object.class);
508                                value = ve.getValue(context);
509                            } else {
510                                value = expr;
511                            }
512                            if (docView != null) {
513                                // do not set attributes on the request as
514                                // document view will be put in the request
515                                // anyway
516                                docView.addParameter(paramName, (String) value);
517                            } else {
518                                httpRequest.setAttribute(paramName, value);
519                            }
520                        } catch (ELException e) {
521                            log.error(String.format("Could not get parameter %s from expression %s", paramName, expr),
522                                    e);
523                        }
524                    }
525                }
526            }
527        }
528
529        // save document view to the request
530        setDocumentViewInRequest(httpRequest, docView);
531    }
532
533    @Override
534    public String navigate(FacesContext facesContext) {
535        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
536
537        URLPatternDescriptor pattern = getURLPatternDescriptor(httpRequest);
538        if (pattern == null) {
539            return null;
540        }
541
542        DocumentView docView = getDocumentViewFromRequest(pattern.getName(), httpRequest);
543        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
544        ELContext context = facesContext.getELContext();
545        String actionBinding = pattern.getActionBinding();
546
547        if (actionBinding != null && !"".equals(actionBinding)
548                && httpRequest.getAttribute(URLPolicyService.DISABLE_ACTION_BINDING_KEY) == null) {
549            MethodExpression action = ef.createMethodExpression(context, actionBinding, String.class,
550                    new Class[] { DocumentView.class });
551            return (String) action.invoke(context, new Object[] { docView });
552        }
553        return null;
554    }
555
556    // registries management
557
558    @Override
559    public void addPatternDescriptor(URLPatternDescriptor pattern) {
560        String name = pattern.getName();
561        if (descriptors.containsKey(name)) {
562            // no merging right now
563            descriptors.remove(name);
564        }
565        descriptors.put(pattern.getName(), pattern);
566        log.debug("Added URLPatternDescriptor: " + name);
567    }
568
569    @Override
570    public void removePatternDescriptor(URLPatternDescriptor pattern) {
571        String name = pattern.getName();
572        descriptors.remove(name);
573        log.debug("Removed URLPatternDescriptor: " + name);
574    }
575
576    @Override
577    public void initViewIdManager(ServletContext context, HttpServletRequest request, HttpServletResponse response) {
578        if (viewIdManager == null) {
579            viewIdManager = new StaticNavigationHandler(context, request, response);
580        }
581    }
582
583    StaticNavigationHandler getViewIdManager() {
584        if (viewIdManager == null) {
585            throw new RuntimeException("View id manager is not initialized: "
586                    + "URLPolicyService#initViewIdManager should " + "have been called first");
587        }
588        return viewIdManager;
589    }
590
591    @Override
592    public String getOutcomeFromViewId(String viewId, HttpServletRequest httpRequest) {
593        return getViewIdManager().getOutcomeFromViewId(viewId);
594    }
595
596    @Override
597    public String getOutcomeFromUrl(String url, HttpServletRequest request) {
598        String baseUrl = BaseURL.getBaseURL(request);
599        // parse url to get outcome from view id
600        String viewId = url;
601        String webAppName = "/" + VirtualHostHelper.getWebAppName(request);
602        if (viewId.startsWith(baseUrl)) {
603            // url is absolute
604            viewId = '/' + viewId.substring(baseUrl.length());
605        } else if (viewId.startsWith(webAppName)) {
606            // url is relative to the web app
607            viewId = viewId.substring(webAppName.length());
608        }
609        int index = viewId.indexOf('?');
610        if (index != -1) {
611            viewId = viewId.substring(0, index);
612        }
613        return getOutcomeFromViewId(viewId, request);
614    }
615
616    @Override
617    public String getViewIdFromOutcome(String outcome, HttpServletRequest httpRequest) {
618        return getViewIdManager().getViewIdFromOutcome(outcome);
619    }
620
621    @Override
622    public void clear() {
623        descriptors.clear();
624    }
625
626    @Override
627    public void flushCache() {
628        viewIdManager = null;
629    }
630
631}