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