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