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