001/*
002 * (C) Copyright 2007-2008 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 *     Eugen Ionica
018 *     Anahide Tchertchian
019 *     Florent Guillaume
020 */
021
022package org.nuxeo.ecm.webapp.action;
023
024import static org.jboss.seam.ScopeType.CONVERSATION;
025import static org.jboss.seam.ScopeType.EVENT;
026
027import java.io.Serializable;
028import java.util.ArrayList;
029import java.util.List;
030
031import javax.faces.context.ExternalContext;
032import javax.faces.context.FacesContext;
033import javax.servlet.http.HttpServletRequest;
034
035import org.apache.commons.lang.StringUtils;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.jboss.seam.Component;
039import org.jboss.seam.ScopeType;
040import org.jboss.seam.annotations.Factory;
041import org.jboss.seam.annotations.In;
042import org.jboss.seam.annotations.Install;
043import org.jboss.seam.annotations.Name;
044import org.jboss.seam.annotations.Observer;
045import org.jboss.seam.annotations.Scope;
046import org.jboss.seam.annotations.intercept.BypassInterceptors;
047import org.jboss.seam.contexts.Context;
048import org.jboss.seam.contexts.Contexts;
049import org.nuxeo.common.utils.UserAgentMatcher;
050import org.nuxeo.ecm.core.api.CoreSession;
051import org.nuxeo.ecm.core.api.DocumentModel;
052import org.nuxeo.ecm.core.api.NuxeoException;
053import org.nuxeo.ecm.platform.actions.Action;
054import org.nuxeo.ecm.platform.actions.ActionContext;
055import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
056import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
057import org.nuxeo.ecm.platform.ui.web.api.TabActionsSelection;
058import org.nuxeo.ecm.platform.ui.web.api.WebActions;
059import org.nuxeo.ecm.webapp.helpers.EventNames;
060import org.nuxeo.runtime.api.Framework;
061import org.nuxeo.runtime.services.config.ConfigurationService;
062
063/**
064 * Component that handles actions retrieval as well as current tab(s) selection.
065 *
066 * @author Eugen Ionica
067 * @author Anahide Tchertchian
068 * @author Florent Guillaume
069 * @author <a href="mailto:cbaican@nuxeo.com">Catalin Baican</a>
070 */
071@Name("webActions")
072@Scope(CONVERSATION)
073@Install(precedence = Install.FRAMEWORK)
074public class WebActionsBean implements WebActions, Serializable {
075
076    private static final long serialVersionUID = 1959221536502251848L;
077
078    private static final Log log = LogFactory.getLog(WebActionsBean.class);
079
080    @In(create = true, required = false)
081    protected transient ActionManager actionManager;
082
083    @In(create = true, required = false)
084    protected transient ActionContextProvider actionContextProvider;
085
086    @In(create = true, required = false)
087    protected transient NavigationContext navigationContext;
088
089    protected List<Action> tabsActionsList;
090
091    protected String subTabsCategory;
092
093    protected List<Action> subTabsActionsList;
094
095    protected TabActionsSelection currentTabActions = new TabActionsSelection();
096
097    // actions management
098
099    @Override
100    public List<Action> getDocumentActions(DocumentModel document, String category, boolean removeFiltered,
101            boolean postFilter) {
102        ActionContext context = postFilter ? null : createActionContext(document);
103        return getActions(context, category, removeFiltered, postFilter);
104    }
105
106    @Override
107    public Action getDocumentAction(DocumentModel document, String actionId, boolean removeFiltered, boolean postFilter) {
108        ActionContext context = postFilter ? null : createActionContext(document);
109        return getAction(context, actionId, removeFiltered, postFilter);
110    }
111
112    @Override
113    public List<Action> getActions(ActionContext context, String category, boolean removeFiltered, boolean postFilter) {
114        List<Action> list = new ArrayList<Action>();
115        List<String> categories = new ArrayList<String>();
116        if (category != null) {
117            String[] split = category.split(",|\\s");
118            if (split != null) {
119                for (String item : split) {
120                    if (!StringUtils.isBlank(item)) {
121                        categories.add(item.trim());
122                    }
123                }
124            }
125        }
126        for (String cat : categories) {
127            List<Action> actions;
128            if (postFilter) {
129                actions = actionManager.getAllActions(cat);
130            } else {
131                actions = actionManager.getActions(cat, context, removeFiltered);
132            }
133            if (actions != null) {
134                list.addAll(actions);
135            }
136        }
137        return list;
138    }
139
140    @Override
141    public Action getAction(ActionContext context, String actionId, boolean removeFiltered, boolean postFilter) {
142        if (postFilter) {
143            return actionManager.getAction(actionId);
144        }
145        return actionManager.getAction(actionId, context, removeFiltered);
146
147    }
148
149    @Override
150    public boolean isAvailableForDocument(DocumentModel document, Action action) {
151        return isAvailable(createActionContext(document), action);
152    }
153
154    @Override
155    public boolean isAvailable(ActionContext context, Action action) {
156        if (action == null) {
157            return false;
158        }
159        if (action.isFiltered()) {
160            return action.getAvailable();
161        }
162        return actionManager.checkFilters(action, context);
163    }
164
165    @Override
166    public List<Action> getActionsList(String category, ActionContext context, boolean removeFiltered) {
167        return getActions(context, category, removeFiltered, false);
168    }
169
170    @Override
171    public List<Action> getActionsList(String category, Boolean removeFiltered) {
172        return getActions(createActionContext(), category, removeFiltered, false);
173    }
174
175    @Override
176    public List<Action> getActionsList(String category, ActionContext context) {
177        return getActions(context, category, true, false);
178    }
179
180    @Override
181    public List<Action> getActionsListForDocument(String category, DocumentModel document, boolean removeFiltered) {
182        return getActions(createActionContext(document), category, removeFiltered, false);
183    }
184
185    @Override
186    public List<Action> getActionsList(String category) {
187        return getActions(createActionContext(), category, true, false);
188    }
189
190    @Override
191    public List<Action> getUnfiltredActionsList(String category, ActionContext context) {
192        return getActions(context, category, false, false);
193    }
194
195    @Override
196    public List<Action> getUnfiltredActionsList(String category) {
197        return getActions(createActionContext(), category, false, false);
198    }
199
200    @Override
201    public List<Action> getAllActions(String category) {
202        return actionManager.getAllActions(category);
203    }
204
205    protected ActionContext createActionContext() {
206        return actionContextProvider.createActionContext();
207    }
208
209    protected ActionContext createActionContext(DocumentModel document) {
210        return actionContextProvider.createActionContext(document);
211    }
212
213    @Override
214    public Action getAction(String actionId, boolean removeFiltered) {
215        return getAction(createActionContext(), actionId, removeFiltered, false);
216    }
217
218    @Override
219    public Action getActionForDocument(String actionId, DocumentModel document, boolean removeFiltered) {
220        return getAction(createActionContext(document), actionId, removeFiltered, false);
221    }
222
223    @Override
224    public Action getAction(String actionId, ActionContext context, boolean removeFiltered) {
225        return getAction(context, actionId, removeFiltered, false);
226    }
227
228    @Override
229    public boolean checkFilter(String filterId) {
230        return actionManager.checkFilter(filterId, createActionContext());
231    }
232
233    // tabs management
234
235    protected Action getDefaultTab(String category) {
236        if (DEFAULT_TABS_CATEGORY.equals(category)) {
237            if (getTabsList() == null) {
238                return null;
239            }
240            try {
241                return tabsActionsList.get(0);
242            } catch (IndexOutOfBoundsException e) {
243                return null;
244            }
245        } else {
246            // check if it's a subtab
247            if (subTabsCategory != null && subTabsCategory.equals(category)) {
248                if (getSubTabsList() == null) {
249                    return null;
250                }
251                try {
252                    return subTabsActionsList.get(0);
253                } catch (IndexOutOfBoundsException e) {
254                    return null;
255                }
256            }
257            // retrieve actions in given category and take the first one found
258            List<Action> actions = getActionsList(category, createActionContext());
259            if (actions != null && actions.size() > 0) {
260                // make sure selection event is sent
261                Action action = actions.get(0);
262                setCurrentTabAction(category, action);
263                return action;
264            }
265            return null;
266        }
267
268    }
269
270    @Override
271    public Action getCurrentTabAction(String category) {
272        Action action = currentTabActions.getCurrentTabAction(category);
273        if (action == null) {
274            // return default action
275            action = getDefaultTab(category);
276        }
277        return action;
278    }
279
280    @Override
281    public void setCurrentTabAction(String category, Action tabAction) {
282        currentTabActions.setCurrentTabAction(category, tabAction);
283        // additional cleanup of this cache
284        if (WebActions.DEFAULT_TABS_CATEGORY.equals(category)) {
285            resetSubTabs();
286        }
287    }
288
289    @Override
290    public Action getCurrentSubTabAction(String parentActionId) {
291        return getCurrentTabAction(TabActionsSelection.getSubTabCategory(parentActionId));
292    }
293
294    @Override
295    public String getCurrentTabId(String category) {
296        Action action = getCurrentTabAction(category);
297        if (action != null) {
298            return action.getId();
299        }
300        return null;
301    }
302
303    @Override
304    public boolean hasCurrentTabId(String category) {
305        if (currentTabActions.getCurrentTabAction(category) == null) {
306            return false;
307        }
308        return true;
309    }
310
311    @Override
312    public void setCurrentTabId(String category, String tabId, String... subTabIds) {
313        currentTabActions.setCurrentTabId(actionManager, createActionContext(), category, tabId, subTabIds);
314        // additional cleanup of this cache
315        if (WebActions.DEFAULT_TABS_CATEGORY.equals(category)) {
316            resetSubTabs();
317        }
318    }
319
320    @Override
321    public String getCurrentTabIds() {
322        return currentTabActions.getCurrentTabIds();
323    }
324
325    @Override
326    public void setCurrentTabIds(String tabIds) {
327        currentTabActions.setCurrentTabIds(actionManager, createActionContext(), tabIds);
328        // reset subtabs just in case
329        resetSubTabs();
330    }
331
332    @Override
333    public void resetCurrentTabs() {
334        currentTabActions.resetCurrentTabs();
335    }
336
337    @Override
338    public void resetCurrentTabs(String category) {
339        currentTabActions.resetCurrentTabs(category);
340    }
341
342    // tabs management specific to the DEFAULT_TABS_CATEGORY
343
344    @Override
345    public void resetCurrentTab() {
346        resetCurrentTabs(DEFAULT_TABS_CATEGORY);
347    }
348
349    protected void resetSubTabs() {
350        subTabsCategory = null;
351        subTabsActionsList = null;
352        // make sure event context is cleared so that factory is called again
353        Contexts.getEventContext().remove("subTabsActionsList");
354        Contexts.getEventContext().remove("currentSubTabAction");
355    }
356
357    @Override
358    @Observer(value = { EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED,
359            EventNames.LOCATION_SELECTION_CHANGED }, create = false)
360    @BypassInterceptors
361    public void resetTabList() {
362        tabsActionsList = null;
363        resetSubTabs();
364        resetCurrentTab();
365        // make sure event context is cleared so that factory is called again
366        Contexts.getEventContext().remove("tabsActionsList");
367        Contexts.getEventContext().remove("currentTabAction");
368    }
369
370    @Override
371    @Factory(value = "tabsActionsList", scope = EVENT)
372    public List<Action> getTabsList() {
373        if (tabsActionsList == null) {
374            tabsActionsList = getActionsList(DEFAULT_TABS_CATEGORY);
375        }
376        return tabsActionsList;
377    }
378
379    @Override
380    @Factory(value = "subTabsActionsList", scope = EVENT)
381    public List<Action> getSubTabsList() {
382        if (subTabsActionsList == null) {
383            String currentTabId = getCurrentTabId();
384            if (currentTabId != null) {
385                subTabsCategory = TabActionsSelection.getSubTabCategory(currentTabId);
386                subTabsActionsList = getActionsList(subTabsCategory);
387            }
388        }
389        return subTabsActionsList;
390    }
391
392    @Override
393    @Factory(value = "currentTabAction", scope = EVENT)
394    public Action getCurrentTabAction() {
395        return getCurrentTabAction(DEFAULT_TABS_CATEGORY);
396    }
397
398    @Override
399    public void setCurrentTabAction(Action currentTabAction) {
400        setCurrentTabAction(DEFAULT_TABS_CATEGORY, currentTabAction);
401    }
402
403    @Override
404    @Factory(value = "currentSubTabAction", scope = EVENT)
405    public Action getCurrentSubTabAction() {
406        Action action = getCurrentTabAction();
407        if (action != null) {
408            return getCurrentTabAction(TabActionsSelection.getSubTabCategory(action.getId()));
409        }
410        return null;
411    }
412
413    @Override
414    public void setCurrentSubTabAction(Action tabAction) {
415        if (tabAction != null) {
416            String[] categories = tabAction.getCategories();
417            if (categories == null || categories.length == 0) {
418                log.error("Cannot set subtab with id '" + tabAction.getId()
419                        + "' as this action does not hold any category");
420                return;
421            }
422            if (categories.length != 1) {
423                log.error("Setting subtab with id '" + tabAction.getId() + "' with category '" + categories[0]
424                        + "': use webActions#setCurrentTabAction(action, category) to specify another category");
425            }
426            setCurrentTabAction(categories[0], tabAction);
427        }
428    }
429
430    @Override
431    public String getCurrentTabId() {
432        Action currentTab = getCurrentTabAction();
433        if (currentTab != null) {
434            return currentTab.getId();
435        }
436        return null;
437    }
438
439    @Override
440    public void setCurrentTabId(String tabId) {
441        if (tabId != null) {
442            // do not reset tab when not set as this method
443            // is used for compatibility in default url pattern
444            setCurrentTabId(DEFAULT_TABS_CATEGORY, tabId);
445        }
446    }
447
448    @Override
449    public String getCurrentSubTabId() {
450        Action currentSubTab = getCurrentSubTabAction();
451        if (currentSubTab != null) {
452            return currentSubTab.getId();
453        }
454        return null;
455    }
456
457    @Override
458    public void setCurrentSubTabId(String tabId) {
459        if (tabId != null) {
460            // do not reset tab when not set as this method
461            // is used for compatibility in default url pattern
462            Action action = getCurrentTabAction();
463            if (action != null) {
464                setCurrentTabId(TabActionsSelection.getSubTabCategory(action.getId()), tabId);
465            }
466        }
467    }
468
469    // navigation API
470
471    @Override
472    public String setCurrentTabAndNavigate(String currentTabActionId) {
473        return setCurrentTabAndNavigate(navigationContext.getCurrentDocument(), currentTabActionId);
474    }
475
476    @Override
477    public String setCurrentTabAndNavigate(DocumentModel document, String currentTabActionId) {
478        // navigate first because it will reset the tabs list
479        String viewId = null;
480        try {
481            viewId = navigationContext.navigateToDocument(document);
482        } catch (NuxeoException e) {
483            log.error("Failed to navigate to " + document, e);
484        }
485        // force creation of new actions if needed
486        getTabsList();
487        // set current tab
488        setCurrentTabId(currentTabActionId);
489        return viewId;
490    }
491
492    // deprecated API
493
494    @Override
495    @Deprecated
496    public List<Action> getSubViewActionsList() {
497        return getActionsList("SUBVIEW_UPPER_LIST");
498    }
499
500    @Override
501    @Deprecated
502    public void selectTabAction() {
503        // if (tabAction != null) {
504        // setCurrentTabAction(tabAction);
505        // }
506    }
507
508    @Override
509    @Deprecated
510    public String getCurrentLifeCycleState() {
511        // only user of documentManager in this bean, look it up by hand
512        CoreSession documentManager = (CoreSession) Component.getInstance("documentManager");
513        return documentManager.getCurrentLifeCycleState(navigationContext.getCurrentDocument().getRef());
514    }
515
516    @Override
517    @Deprecated
518    public void setTabsList(List<Action> tabsList) {
519        tabsActionsList = tabsList;
520    }
521
522    @Override
523    @Deprecated
524    public void setSubTabsList(List<Action> tabsList) {
525        subTabsActionsList = tabsList;
526        subTabsCategory = null;
527        if (tabsList != null) {
528            // BBB code
529            for (Action action : tabsList) {
530                if (action != null) {
531                    String[] categories = action.getCategories();
532                    if (categories != null && categories.length > 0) {
533                        subTabsCategory = categories[0];
534                        break;
535                    }
536                }
537            }
538        }
539    }
540
541    @Override
542    @Deprecated
543    public void setCurrentTabAction(String currentTabActionId) {
544        setCurrentTabId(currentTabActionId);
545    }
546
547    @Override
548    @Factory(value = "useAjaxTabs", scope = ScopeType.SESSION)
549    public boolean useAjaxTabs() {
550        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
551        if (configurationService.isBooleanPropertyTrue(AJAX_TAB_PROPERTY)) {
552            return canUseAjaxTabs();
553        }
554        return false;
555    }
556
557    @Override
558    @Factory(value = "canUseAjaxTabs", scope = ScopeType.SESSION)
559    public boolean canUseAjaxTabs() {
560        FacesContext context = FacesContext.getCurrentInstance();
561        ExternalContext econtext = context.getExternalContext();
562        HttpServletRequest request = (HttpServletRequest) econtext.getRequest();
563        String ua = request.getHeader("User-Agent");
564        return UserAgentMatcher.isHistoryPushStateSupported(ua);
565    }
566
567    /**
568     * Returns true if configuration property to remove optimizations around actions (for compatibility) has been
569     * enabled.
570     *
571     * @since 8.2
572     */
573    @Factory(value = "removeActionOptims", scope = ScopeType.SESSION)
574    public boolean removeActionOptims() {
575        ConfigurationService cs = Framework.getService(ConfigurationService.class);
576        return cs.isBooleanPropertyTrue("nuxeo.jsf.actions.removeActionOptims");
577    }
578
579    @Observer(value = { EventNames.FLUSH_EVENT }, create = false)
580    @BypassInterceptors
581    public void onHotReloadFlush() {
582        // reset above caches
583        Context seamContext = Contexts.getSessionContext();
584        seamContext.remove("useAjaxTabs");
585        seamContext.remove("canUseAjaxTabs");
586    }
587
588}