# 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)