001/*
002 * (C) Copyright 2006-2013 Nuxeo SAS (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 *     Nuxeo - initial API and implementation
016 *
017 * $Id: URLPolicyServiceImpl.java 29556 2008-01-23 00:59:39Z jcarsique $
018 */
019
020package org.nuxeo.ecm.platform.ui.web.rest.services;
021
022import java.util.ArrayList;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026
027import javax.el.ELContext;
028import javax.el.ELException;
029import javax.el.ExpressionFactory;
030import javax.el.MethodExpression;
031import javax.el.ValueExpression;
032import javax.faces.context.FacesContext;
033import javax.servlet.ServletContext;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.nuxeo.common.utils.URIUtils;
041import org.nuxeo.ecm.core.api.DocumentLocation;
042import org.nuxeo.ecm.core.io.download.DownloadService;
043import org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants;
044import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter;
045import org.nuxeo.ecm.platform.ui.web.rest.StaticNavigationHandler;
046import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService;
047import org.nuxeo.ecm.platform.ui.web.rest.descriptors.URLPatternDescriptor;
048import org.nuxeo.ecm.platform.ui.web.rest.descriptors.ValueBindingDescriptor;
049import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
050import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
051import org.nuxeo.ecm.platform.url.api.DocumentView;
052import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
053import org.nuxeo.ecm.platform.url.codec.DocumentFileCodec;
054import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
055import org.nuxeo.runtime.api.Framework;
056
057public class URLPolicyServiceImpl implements URLPolicyService {
058
059    public static final String NAME = URLPolicyServiceImpl.class.getName();
060
061    private static final Log log = LogFactory.getLog(URLPolicyServiceImpl.class);
062
063    // this used to be a codec but now delegates to DownloadService
064    public static final String DOWNLOADFILE_PATTERN = "downloadFile";
065
066    protected final Map<String, URLPatternDescriptor> descriptors;
067
068    protected StaticNavigationHandler viewIdManager;
069
070    public URLPolicyServiceImpl() {
071        // make sure the descriptors list order follows registration order, as
072        // order may have an impact on resolved pattern
073        descriptors = new LinkedHashMap<String, URLPatternDescriptor>();
074    }
075
076    protected List<URLPatternDescriptor> getURLPatternDescriptors() {
077        // TODO: add cache
078        List<URLPatternDescriptor> lst = new ArrayList<URLPatternDescriptor>();
079        for (URLPatternDescriptor desc : descriptors.values()) {
080            if (desc.getEnabled()) {
081                // add default at first
082                if (desc.getDefaultURLPolicy()) {
083                    lst.add(0, desc);
084                } else {
085                    lst.add(desc);
086                }
087            }
088        }
089        return lst;
090    }
091
092    protected URLPatternDescriptor getDefaultPatternDescriptor() {
093        for (URLPatternDescriptor desc : descriptors.values()) {
094            if (desc.getEnabled()) {
095                if (desc.getDefaultURLPolicy()) {
096                    return desc;
097                }
098            }
099        }
100        return null;
101    }
102
103    @Override
104    public String getDefaultPatternName() {
105        URLPatternDescriptor desc = getDefaultPatternDescriptor();
106        if (desc != null) {
107            return desc.getName();
108        }
109        return null;
110    }
111
112    @Override
113    public boolean hasPattern(String name) {
114        URLPatternDescriptor desc = descriptors.get(name);
115        return desc != null;
116    }
117
118    protected static DocumentViewCodecManager getDocumentViewCodecService() {
119        return Framework.getService(DocumentViewCodecManager.class);
120    }
121
122    protected URLPatternDescriptor getURLPatternDescriptor(String patternName) {
123        URLPatternDescriptor desc = descriptors.get(patternName);
124        if (desc == null) {
125            throw new IllegalArgumentException("Unknown pattern " + patternName);
126        }
127        return desc;
128    }
129
130    @Override
131    public boolean isCandidateForDecoding(HttpServletRequest httpRequest) {
132        // only rewrite GET/HEAD URLs
133        String method = httpRequest.getMethod();
134        if (!method.equals("GET") && !method.equals("HEAD")) {
135            return false;
136        }
137
138        // look for appropriate pattern and see if it needs filter
139        // preprocessing
140        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
141        if (desc != null) {
142            return desc.getNeedFilterPreprocessing();
143        }
144        // return default pattern descriptor behaviour
145        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
146        if (defaultPattern != null) {
147            return defaultPattern.getNeedFilterPreprocessing();
148        }
149        return false;
150    }
151
152    @Override
153    public boolean isCandidateForEncoding(HttpServletRequest httpRequest) {
154        Boolean forceEncoding = Boolean.FALSE;
155        Object forceEncodingValue = httpRequest.getAttribute(FORCE_URL_ENCODING_REQUEST_KEY);
156        if (forceEncodingValue instanceof Boolean) {
157            forceEncoding = (Boolean) forceEncodingValue;
158        }
159
160        // only POST access need a redirect,unless with force encoding (this
161        // happens when redirect is triggered after a seam page has been
162        // processed)
163        if (!forceEncoding.booleanValue() && !httpRequest.getMethod().equals("POST")) {
164            return false;
165        }
166
167        Object skipRedirect = httpRequest.getAttribute(NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY);
168        if (skipRedirect instanceof Boolean && ((Boolean) skipRedirect).booleanValue()) {
169            return false;
170        }
171
172        // look for appropriate pattern and see if it needs redirect
173        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
174        if (desc != null) {
175            return desc.getNeedRedirectFilter();
176        }
177        // return default pattern descriptor behaviour
178        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
179        if (defaultPattern != null) {
180            return defaultPattern.getNeedRedirectFilter();
181        }
182        return false;
183    }
184
185    @Override
186    public void setDocumentViewInRequest(HttpServletRequest request, DocumentView docView) {
187        request.setAttribute(NXAuthConstants.REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request));
188        request.setAttribute(DOCUMENT_VIEW_REQUEST_KEY, docView);
189    }
190
191    protected URLPatternDescriptor getURLPatternDescriptor(HttpServletRequest request) {
192        URLPatternDescriptor res = null;
193        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
194            DocumentView docView = getDocumentViewFromRequest(desc.getName(), request);
195            if (docView != null) {
196                res = desc;
197                break;
198            }
199        }
200        // if (res == null && log.isDebugEnabled()) {
201        // log.debug("Could not get url pattern for request "
202        // + request.getRequestURL());
203        // }
204        return res;
205    }
206
207    @Override
208    public DocumentView getDocumentViewFromRequest(HttpServletRequest request) {
209        DocumentView docView = null;
210        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
211            docView = getDocumentViewFromRequest(desc.getName(), request);
212            if (docView != null) {
213                break;
214            }
215        }
216
217        // if (docView == null && log.isDebugEnabled()) {
218        // log.debug("Could not get document view from request "
219        // + request.getRequestURL());
220        // }
221        return docView;
222    }
223
224    @Override
225    public DocumentView getDocumentViewFromRequest(String patternName, HttpServletRequest request) {
226        Object value = request.getAttribute(DOCUMENT_VIEW_REQUEST_KEY);
227        if (value instanceof DocumentView) {
228            DocumentView requestDocView = (DocumentView) value;
229            // check if document view in request was set thanks to this pattern
230            if (patternName.equals(requestDocView.getPatternName())) {
231                return requestDocView;
232            }
233        }
234
235        // try to build it from the request
236        String url;
237        String queryString = request.getQueryString();
238        if (queryString != null) {
239            url = new String(request.getRequestURL() + "?" + queryString);
240        } else {
241            url = new String(request.getRequestURL());
242        }
243        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
244        String codecName = desc.getDocumentViewCodecName();
245        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
246        DocumentView docView = docViewService.getDocumentViewFromUrl(codecName, url, desc.getNeedBaseURL(),
247                BaseURL.getLocalBaseURL(request));
248        if (docView != null) {
249            // set pattern name
250            docView.setPatternName(patternName);
251            // set other parameters as set in the url pattern if docView does
252            // not hold them already
253            Map<String, String> docViewParameters = docView.getParameters();
254            Map<String, String> requestParameters = URIUtils.getRequestParameters(queryString);
255            if (requestParameters != null) {
256                ValueBindingDescriptor[] bindings = desc.getValueBindings();
257                for (ValueBindingDescriptor binding : bindings) {
258                    String paramName = binding.getName();
259                    if (!docViewParameters.containsKey(paramName)) {
260                        Object paramValue = requestParameters.get(paramName);
261                        if (paramValue == null || paramValue instanceof String) {
262                            docView.addParameter(paramName, (String) paramValue);
263                        }
264                    }
265                }
266            }
267        }
268
269        return docView;
270    }
271
272    protected URLPatternDescriptor getURLPatternDescriptor(DocumentView docView) {
273        URLPatternDescriptor res = null;
274        if (docView != null) {
275            String patternName = docView.getPatternName();
276            try {
277                res = getURLPatternDescriptor(patternName);
278            } catch (IllegalArgumentException e) {
279            }
280        }
281        // if (res == null && log.isDebugEnabled()) {
282        // log.debug("Could not get url pattern for document view");
283        // }
284        return res;
285    }
286
287    @Override
288    public String getUrlFromDocumentView(DocumentView docView, String baseUrl) {
289        String url = null;
290        String patternName = docView.getPatternName();
291        if (patternName != null) {
292            // try with original document view pattern
293            URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
294            if (desc != null) {
295                // return corresponding url
296                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
297            }
298        }
299        if (url == null) {
300            // take first matching pattern
301            List<URLPatternDescriptor> descs = getURLPatternDescriptors();
302            for (URLPatternDescriptor desc : descs) {
303                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
304                if (url != null) {
305                    break;
306                }
307            }
308        }
309        // if (url == null && log.isDebugEnabled()) {
310        // log.debug("Could not get url from document view");
311        // }
312        return url;
313    }
314
315    /**
316     * Returns patterns sorted according to:
317     * <ul>
318     * <li>First patterns holding the given view id</li>
319     * <li>The default pattern if it does not hold this view id</li>
320     * <li>Other patterns not holding this view id</li>
321     * </ul>
322     *
323     * @since 5.4.2
324     * @param viewId
325     * @deprecated since 5.5
326     */
327    @Deprecated
328    protected List<URLPatternDescriptor> getSortedURLPatternDescriptorsFor(String viewId) {
329        List<URLPatternDescriptor> sortedDescriptors = new ArrayList<URLPatternDescriptor>();
330        List<URLPatternDescriptor> nonMatchingViewIdDescriptors = new ArrayList<URLPatternDescriptor>();
331
332        List<URLPatternDescriptor> descriptors = getURLPatternDescriptors();
333        for (URLPatternDescriptor descriptor : descriptors) {
334            List<String> handledViewIds = descriptor.getViewIds();
335            if (handledViewIds != null && handledViewIds.contains(viewId)) {
336                sortedDescriptors.add(descriptor);
337            } else {
338                nonMatchingViewIdDescriptors.add(descriptor);
339            }
340        }
341        sortedDescriptors.addAll(nonMatchingViewIdDescriptors);
342        return sortedDescriptors;
343    }
344
345    @Override
346    public String getUrlFromDocumentView(String patternName, DocumentView docView, String baseUrl) {
347        if (DOWNLOADFILE_PATTERN.equals(patternName)) {
348            // this used to be a codec but now delegates to DownloadService
349            DownloadService downloadService = Framework.getService(DownloadService.class);
350            DocumentLocation docLoc = docView.getDocumentLocation();
351            String repositoryName = docLoc.getServerName();
352            String docId = docLoc.getDocRef().toString();
353            String xpath = docView.getParameter(DocumentFileCodec.FILE_PROPERTY_PATH_KEY);
354            String filename = docView.getParameter(DocumentFileCodec.FILENAME_KEY);
355            String url = downloadService.getDownloadUrl(repositoryName, docId, xpath, filename);
356            if (!StringUtils.isBlank(baseUrl)) {
357                if (!baseUrl.endsWith("/")) {
358                    baseUrl += "/";
359                }
360                url = baseUrl + url;
361            }
362            return url;
363        }
364        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
365        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
366        String codecName = desc.getDocumentViewCodecName();
367        return docViewService.getUrlFromDocumentView(codecName, docView, desc.getNeedBaseURL(), baseUrl);
368    }
369
370    @Override
371    public void applyRequestParameters(FacesContext facesContext) {
372        // try to set document view
373        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
374        ELContext context = facesContext.getELContext();
375
376        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
377
378        URLPatternDescriptor pattern = getURLPatternDescriptor(httpRequest);
379        if (pattern == null) {
380            return;
381        }
382
383        DocumentView docView = getDocumentViewFromRequest(pattern.getName(), httpRequest);
384        // pattern applies => document view will not be null
385        if (docView != null) {
386            String documentViewBinding = pattern.getDocumentViewBinding();
387            if (documentViewBinding != null && !"".equals(documentViewBinding)) {
388                // try to set it from custom mapping
389                ValueExpression ve = ef.createValueExpression(context, pattern.getDocumentViewBinding(), Object.class);
390                ve.setValue(context, docView);
391            }
392        }
393
394        Map<String, String> docViewParameters = null;
395        if (docView != null) {
396            docViewParameters = docView.getParameters();
397        }
398        ValueBindingDescriptor[] bindings = pattern.getValueBindings();
399        if (bindings != null && httpRequest.getAttribute(URLPolicyService.DISABLE_ACTION_BINDING_KEY) == null) {
400            for (ValueBindingDescriptor binding : bindings) {
401                if (!binding.getCallSetter()) {
402                    continue;
403                }
404                String paramName = binding.getName();
405                // try doc view parameters
406                Object value = null;
407                if (docViewParameters != null && docViewParameters.containsKey(paramName)) {
408                    value = docView.getParameter(paramName);
409                } else {
410                    // try request attributes
411                    value = httpRequest.getAttribute(paramName);
412                }
413                String expr = binding.getExpression();
414                if (ComponentTagUtils.isValueReference(expr)) {
415                    ValueExpression ve = ef.createValueExpression(context, expr, Object.class);
416                    try {
417                        ve.setValue(context, value);
418                    } catch (ELException e) {
419                        log.error(
420                                String.format("Could not apply request parameter %s " + "to expression %s", value, expr),
421                                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}