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}