| # Copyright 2015 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """A utility module for parsing and applying action suffixes in actions.xml. |
| |
| Note: There is a copy of this file used internally by the UMA processing |
| infrastructure. Any changes to this file should also be done (manually) to the |
| internal copy. Please contact tools/metrics/OWNERS for more details. |
| """ |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class UndefinedActionItemError(Error): |
| pass |
| |
| |
| class InvalidOrderingAttributeError(Error): |
| pass |
| |
| |
| class SuffixNameEmptyError(Error): |
| pass |
| |
| |
| class InvalidAffecteddActionNameError(Error): |
| pass |
| |
| |
| class Action(object): |
| """Represents Chrome user action. |
| |
| Attributes: |
| name: name of the action. |
| description: description of the action. |
| owners: list of action owners |
| not_user_triggered: if action is not user triggered |
| obsolete: explanation on why user action is not being used anymore |
| from_suffix: If True, this action was computed via a suffix. |
| """ |
| |
| def __init__(self, |
| name, |
| description, |
| owners, |
| not_user_triggered=False, |
| obsolete=None, |
| from_suffix=False): |
| self.name = name |
| self.description = description |
| self.owners = owners |
| self.not_user_triggered = not_user_triggered |
| self.obsolete = obsolete |
| self.from_suffix = from_suffix |
| |
| |
| class Suffix(object): |
| """Action suffix in actions.xml. |
| |
| Attributes: |
| name: name of the suffix. |
| description: description of the suffix. |
| separator: the separator between affected action name and suffix name. |
| ordering: 'suffix' or 'prefix'. if set to prefix, suffix name will be |
| inserted after the first dot separator of affected action name. |
| """ |
| |
| def __init__(self, name, description, separator, ordering): |
| if not name: |
| raise SuffixNameEmptyError('Suffix name cannot be empty.') |
| |
| if ordering != 'suffix' and ordering != 'prefix': |
| raise InvalidOrderingAttributeError("Ordering has to be either 'prefix' " |
| "or 'suffix'.") |
| |
| self.name = name |
| self.description = description |
| self.separator = separator |
| self.ordering = ordering |
| |
| def __repr__(self): |
| return '<%s, %s, %s, %s>' % (self.name, self.description, self.separator, |
| self.ordering) |
| |
| |
| def CreateActionsFromSuffixes(actions_dict, action_suffix_nodes): |
| """Creates new actions from suffixes and adds them to actions_dict. |
| |
| Args: |
| actions_dict: dict of existing action name to Action object. |
| action_suffix_nodes: a list of action-suffix nodes |
| |
| Returns: |
| A dictionary of action name to list of Suffix objects for that action. |
| |
| Raises: |
| UndefinedActionItemError: if an affected action name can't be found |
| """ |
| action_to_suffixes_dict = _CreateActionToSuffixesDict(action_suffix_nodes) |
| |
| # Some actions in action_to_suffixes_dict keys may yet to be created. |
| # Therefore, while new actions can be created and added to the existing |
| # actions keep calling _CreateActionsFromSuffixes. |
| while _CreateActionsFromSuffixes(actions_dict, action_to_suffixes_dict): |
| pass |
| |
| # If action_to_suffixes_dict is not empty by the end, we have missing actions. |
| if action_to_suffixes_dict: |
| raise UndefinedActionItemError('Following actions are missing: %s.' % |
| (list(action_to_suffixes_dict.keys()))) |
| |
| |
| def _CreateActionToSuffixesDict(action_suffix_nodes): |
| """Creates a dict of action name to list of Suffix objects for that action. |
| |
| Args: |
| action_suffix_nodes: a list of action-suffix nodes |
| |
| Returns: |
| A dictionary of action name to list of Suffix objects for that action. |
| """ |
| action_to_suffixes_dict = {} |
| for action_suffix_node in action_suffix_nodes: |
| separator = _GetAttribute(action_suffix_node, 'separator', '_') |
| ordering = _GetAttribute(action_suffix_node, 'ordering', 'suffix') |
| suffixes = [Suffix(suffix_node.getAttribute('name'), |
| suffix_node.getAttribute('label'), |
| separator, ordering) for suffix_node in |
| action_suffix_node.getElementsByTagName('suffix')] |
| |
| action_nodes = action_suffix_node.getElementsByTagName('affected-action') |
| for action_node in action_nodes: |
| action_name = action_node.getAttribute('name') |
| # If <affected-action> has <with-suffix> child nodes, only those suffixes |
| # should be used with that action. filter the list of suffix names if so. |
| action_suffix_names = [suffix_node.getAttribute('name') for suffix_node in |
| action_node.getElementsByTagName('with-suffix')] |
| if action_suffix_names: |
| action_suffixes = [suffix for suffix in suffixes if suffix.name in |
| action_suffix_names] |
| else: |
| action_suffixes = list(suffixes) |
| |
| if action_name in action_to_suffixes_dict: |
| action_to_suffixes_dict[action_name] += action_suffixes |
| else: |
| action_to_suffixes_dict[action_name] = action_suffixes |
| |
| return action_to_suffixes_dict |
| |
| |
| def _GetAttribute(node, attribute_name, default_value): |
| """Returns the attribute's value or default_value if attribute doesn't exist. |
| |
| Args: |
| node: an XML dom element. |
| attribute_name: name of the attribute. |
| default_value: default value to return if attribute doesn't exist. |
| |
| Returns: |
| The value of the attribute or default_value if attribute doesn't exist. |
| """ |
| if node.hasAttribute(attribute_name): |
| return node.getAttribute(attribute_name) |
| else: |
| return default_value |
| |
| |
| def _CreateActionsFromSuffixes(actions_dict, action_to_suffixes_dict): |
| """Creates new actions with action-suffix pairs and adds them to actions_dict. |
| |
| For every key (action name) in action_to_suffixes_dict, This function looks |
| to see whether it exists in actions_dict. If so it combines the Action object |
| from actions_dict with all the Suffix objects from action_to_suffixes_dict to |
| create new Action objects. New Action objects are added to actions_dict and |
| the action name is removed from action_to_suffixes_dict. |
| |
| Args: |
| actions_dict: dict of existing action name to Action object. |
| action_to_suffixes_dict: dict of action name to list of Suffix objects it |
| will combine with. |
| |
| Returns: |
| True if any new action was added, False otherwise. |
| """ |
| expanded_actions = set() |
| for action_name, suffixes in action_to_suffixes_dict.items(): |
| if action_name in actions_dict: |
| existing_action = actions_dict[action_name] |
| for suffix in suffixes: |
| _CreateActionFromSuffix(actions_dict, existing_action, suffix) |
| |
| expanded_actions.add(action_name) |
| |
| for action_name in expanded_actions: |
| del action_to_suffixes_dict[action_name] |
| |
| return bool(expanded_actions) |
| |
| |
| def _CreateActionFromSuffix(actions_dict, action, suffix): |
| """Creates a new action with action and suffix and adds it to actions_dict. |
| |
| Args: |
| actions_dict: dict of existing action name to Action object. |
| action: an Action object to combine with suffix. |
| suffix: a suffix object to combine with action. |
| |
| Returns: |
| None. |
| |
| Raises: |
| InvalidAffecteddActionNameError: if the action name does not contain a dot |
| """ |
| if suffix.ordering == 'suffix': |
| new_action_name = action.name + suffix.separator + suffix.name |
| else: |
| (before, dot, after) = action.name.partition('.') |
| if not after: |
| raise InvalidAffecteddActionNameError( |
| "Action name '%s' must contain a '.'." % action.name) |
| new_action_name = before + dot + suffix.name + suffix.separator + after |
| |
| new_action_description = action.description + ' ' + suffix.description |
| |
| actions_dict[new_action_name] = Action( |
| new_action_name, |
| new_action_description, |
| list(action.owners), |
| action.not_user_triggered, |
| action.obsolete, |
| from_suffix=True) |