001/*
002 * (C) Copyright 2011 Nuxeo SA (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.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 *     Anahide Tchertchian
016 */
017package org.nuxeo.ecm.platform.ui.web.api;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.jboss.seam.core.Events;
030import org.nuxeo.ecm.platform.actions.Action;
031import org.nuxeo.ecm.platform.actions.ActionContext;
032import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
033
034/**
035 * Handles selected action tabs and raised events on current tab change.
036 *
037 * @see WebActions#CURRENT_TAB_CHANGED_EVENT
038 * @since 5.4.2
039 */
040public class TabActionsSelection implements Serializable {
041
042    private static final long serialVersionUID = 1L;
043
044    private static final Log log = LogFactory.getLog(TabActionsSelection.class);
045
046    /**
047     * Map of current tab actions, with category as key and corresponding action as value.
048     * <p>
049     * Use a linked has map to preserve order when using several selections as sub tabs management needs order to be
050     * preserved.
051     */
052    protected Map<String, Action> currentTabActions = new LinkedHashMap<String, Action>();
053
054    /**
055     * Returns the current action for given category.
056     */
057    public Action getCurrentTabAction(String category) {
058        if (currentTabActions.containsKey(category)) {
059            return currentTabActions.get(category);
060        }
061        return null;
062    }
063
064    /**
065     * Sets the current action for given category, with additional sub tabs.
066     * <p>
067     * If given action is null, it resets the current action for this category.
068     */
069    public void setCurrentTabAction(String category, Action tabAction) {
070        if (category == null) {
071            return;
072        }
073        if (tabAction != null) {
074            String[] actionCategories = tabAction.getCategories();
075            if (actionCategories != null) {
076                boolean categoryFound = false;
077                for (String actionCategory : actionCategories) {
078                    if (category.equals(actionCategory)) {
079                        categoryFound = true;
080                        Action oldAction = currentTabActions.get(category);
081                        currentTabActions.put(category, tabAction);
082                        raiseEventOnCurrentTabSelected(category, tabAction.getId());
083                        if (oldAction == null || !oldAction.getId().equals(tabAction.getId())) {
084                            // only raise the event if action actually changed
085                            raiseEventOnCurrentTabChange(category, tabAction.getId());
086                        }
087                        if (oldAction != null) {
088                            // additional cleanup of possible sub tabs
089                            resetCurrentTabs(getSubTabCategory(oldAction.getId()));
090                        }
091                        break;
092                    }
093                }
094                if (!categoryFound) {
095                    log.error(String.format("Cannot set current action '%s' for category"
096                            + " '%s' as this action does not " + "hold the given category.", tabAction.getId(),
097                            category));
098                }
099            }
100        } else {
101            resetCurrentTabs(category);
102        }
103    }
104
105    public String getCurrentTabId(String category) {
106        Action action = getCurrentTabAction(category);
107        if (action != null) {
108            return action.getId();
109        }
110        return null;
111    }
112
113    public void setCurrentTabId(ActionManager actionManager, ActionContext actionContext, String category,
114            String tabId, String... subTabIds) {
115        boolean set = false;
116        if (tabId != null && !WebActions.NULL_TAB_ID.equals(tabId)) {
117            if (actionManager.isEnabled(tabId, actionContext)) {
118                Action action = actionManager.getAction(tabId);
119                setCurrentTabAction(category, action);
120                if (subTabIds != null && subTabIds.length > 0) {
121                    String newTabId = subTabIds[0];
122                    String[] newSubTabsIds = new String[subTabIds.length - 1];
123                    System.arraycopy(subTabIds, 1, newSubTabsIds, 0, subTabIds.length - 1);
124                    setCurrentTabId(actionManager, actionContext, getSubTabCategory(tabId), newTabId, newSubTabsIds);
125                }
126                set = true;
127            } else {
128                if (actionManager.getAction(tabId) != null) {
129                    log.warn(String.format("Cannot set current tab with id '%s': " + "action is not enabled.", tabId));
130                } else {
131                    log.error(String.format("Cannot set current tab with id '%s': " + "action does not exist.", tabId));
132                }
133            }
134        }
135        if (!set && (tabId == null || WebActions.NULL_TAB_ID.equals(tabId))) {
136            resetCurrentTabs(category);
137        }
138    }
139
140    /**
141     * Returns current tab ids as a string, encoded as is:
142     * CATEGORY_1:ACTION_ID_1,CATEGORY_2:ACTION_ID_2:SUBTAB_ACTION_ID_2,...
143     *
144     * @since 5.4.2
145     */
146    public String getCurrentTabIds() {
147        StringBuffer builder = new StringBuffer();
148        boolean first = true;
149        // resolve sub tabs
150        Map<String, List<Action>> actionsToEncode = new LinkedHashMap<String, List<Action>>();
151        Map<String, String> subTabToCategories = new HashMap<String, String>();
152        for (Map.Entry<String, Action> currentTabAction : currentTabActions.entrySet()) {
153            String category = currentTabAction.getKey();
154            Action action = currentTabAction.getValue();
155            subTabToCategories.put(getSubTabCategory(action.getId()), category);
156            if (subTabToCategories.containsKey(category)) {
157                // this is a sub action, parent already added
158                String cat = subTabToCategories.get(category);
159                List<Action> subActions = actionsToEncode.get(cat);
160                if (subActions == null) {
161                    subActions = new ArrayList<Action>();
162                    actionsToEncode.put(cat, subActions);
163                }
164                subActions.add(action);
165            } else {
166                List<Action> actionsList = new ArrayList<Action>();
167                actionsList.add(action);
168                actionsToEncode.put(category, actionsList);
169            }
170        }
171        for (Map.Entry<String, List<Action>> item : actionsToEncode.entrySet()) {
172            String encodedActions = encodeActions(item.getKey(), item.getValue());
173            if (encodedActions != null) {
174                if (!first) {
175                    builder.append(",");
176                }
177                first = false;
178                builder.append(encodedActions);
179            }
180        }
181        return builder.toString();
182    }
183
184    /**
185     * Sets current tab ids as a String, splitting on commas ',' and parsing each action information as is:
186     * CATEGORY:[ACTION_ID[:OPTIONAL_SUB_ACTION_ID [:OPTIONAL_SUB_ACTION_ID]...]]
187     * <p>
188     * If category is omitted or empty, the category {@link #DEFAULT_TABS_CATEGORY} will be used (if there is no subtab
189     * information).
190     * <p>
191     * If no action id is given, the corresponding category is reset (for instance using 'CATEGORY:').
192     * <p>
193     * If the action information is '*:', all categories will be reset.
194     * <p>
195     * The resulting string looks like: CATEGORY_1:ACTION_ID_1,CATEGORY_2:ACTION_ID_2_SUB_ACTION_ID_2,...
196     *
197     * @since 5.4.2
198     */
199    public void setCurrentTabIds(ActionManager actionManager, ActionContext actionContext, String tabIds) {
200        if (tabIds == null) {
201            return;
202        }
203        String[] encodedActions = tabIds.split(",");
204        if (encodedActions != null && encodedActions.length != 0) {
205            for (String encodedAction : encodedActions) {
206                encodedAction = encodedAction.trim();
207                if ((":").equals(encodedAction)) {
208                    // reset default actions
209                    resetCurrentTabs(WebActions.DEFAULT_TABS_CATEGORY);
210                } else {
211                    String[] actionInfo = encodedAction.split(":");
212                    // XXX: "*:" vs ":TRUC"
213                    if (actionInfo != null && actionInfo.length == 1) {
214                        if (encodedAction.startsWith(":")) {
215                            // it's a default action
216                            setCurrentTabId(actionManager, actionContext, WebActions.DEFAULT_TABS_CATEGORY,
217                                    actionInfo[0]);
218                        } else {
219                            String category = actionInfo[0];
220                            // it's a category, and it needs to be reset
221                            if ("*".equals(category)) {
222                                resetCurrentTabs();
223                            } else {
224                                resetCurrentTabs(category);
225                            }
226                        }
227                    } else if (actionInfo != null && actionInfo.length > 1) {
228                        String category = actionInfo[0];
229                        String actionId = actionInfo[1];
230                        String[] subTabsIds = new String[actionInfo.length - 2];
231                        System.arraycopy(actionInfo, 2, subTabsIds, 0, actionInfo.length - 2);
232                        if (category == null || category.isEmpty()) {
233                            category = WebActions.DEFAULT_TABS_CATEGORY;
234                        }
235                        setCurrentTabId(actionManager, actionContext, category, actionId, subTabsIds);
236                    } else {
237                        log.error(String.format("Cannot set current tab from given encoded action: '%s'", encodedAction));
238                    }
239                }
240            }
241        }
242    }
243
244    /**
245     * Resets all current tabs information.
246     *
247     * @since 5.4.2
248     */
249    public void resetCurrentTabs() {
250        Set<String> categories = currentTabActions.keySet();
251        currentTabActions.clear();
252        for (String category : categories) {
253            raiseEventOnCurrentTabSelected(category, null);
254            raiseEventOnCurrentTabChange(category, null);
255        }
256    }
257
258    /**
259     * Resets current tabs for given category, taking subtabs into account by resetting actions in categories computed
260     * from reset actions id with suffix {@link #SUBTAB_CATEGORY_SUFFIX}.
261     */
262    public void resetCurrentTabs(String category) {
263        if (currentTabActions.containsKey(category)) {
264            Action action = currentTabActions.get(category);
265            currentTabActions.remove(category);
266            raiseEventOnCurrentTabSelected(category, null);
267            raiseEventOnCurrentTabChange(category, null);
268            if (action != null) {
269                resetCurrentTabs(getSubTabCategory(action.getId()));
270            }
271        }
272    }
273
274    protected String encodeActions(String category, List<Action> actions) {
275        if (actions == null || actions.isEmpty()) {
276            return null;
277        }
278        StringBuilder builder = new StringBuilder();
279        builder.append(WebActions.DEFAULT_TABS_CATEGORY.equals(category) ? "" : category);
280        for (int i = 0; i < actions.size(); i++) {
281            builder.append(":" + actions.get(i).getId());
282        }
283        return builder.toString();
284    }
285
286    public static String getSubTabCategory(String parentActionId) {
287        if (parentActionId == null) {
288            return null;
289        }
290        return parentActionId + WebActions.SUBTAB_CATEGORY_SUFFIX;
291    }
292
293    /**
294     * Raises a seam event when current tab changes for a given category.
295     * <p>
296     * Actually raises 2 events: one with name WebActions#CURRENT_TAB_CHANGED_EVENT and another with name
297     * WebActions#CURRENT_TAB_CHANGED_EVENT + '_' + category to optimize observers declarations.
298     * <p>
299     * The event is always sent with 2 parameters: the category and tab id (the tab id can be null when resetting
300     * current tab for given category).
301     *
302     * @see WebActions#CURRENT_TAB_CHANGED_EVENT
303     * @since 5.4.2
304     */
305    protected void raiseEventOnCurrentTabChange(String category, String tabId) {
306        if (Events.exists()) {
307            Events.instance().raiseEvent(WebActions.CURRENT_TAB_CHANGED_EVENT, category, tabId);
308            Events.instance().raiseEvent(WebActions.CURRENT_TAB_CHANGED_EVENT + "_" + category, category, tabId);
309        }
310    }
311
312    /**
313     * Raises a seam event when current tab is selected for a given category. Fired also when current tab did not
314     * change.
315     * <p>
316     * Actually raises 2 events: one with name WebActions#CURRENT_TAB_SELECTED_EVENT and another with name
317     * WebActions#CURRENT_TAB_SELECTED_EVENT + '_' + category to optimize observers declarations.
318     * <p>
319     * The event is always sent with 2 parameters: the category and tab id (the tab id can be null when resetting
320     * current tab for given category).
321     *
322     * @see WebActions#CURRENT_TAB_SELECTED_EVENT
323     * @since 5.6
324     */
325    protected void raiseEventOnCurrentTabSelected(String category, String tabId) {
326        if (Events.exists()) {
327            Events.instance().raiseEvent(WebActions.CURRENT_TAB_SELECTED_EVENT, category, tabId);
328            Events.instance().raiseEvent(WebActions.CURRENT_TAB_SELECTED_EVENT + "_" + category, category, tabId);
329        }
330    }
331
332}