Adding EPL version of configurationengine.
#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description:
#
import sys, zipfile, os
import re, fnmatch
import logging
from optparse import OptionParser, OptionGroup
import cone_common
from cone.public import api, plugin, utils, exceptions
from cone.confml import persistentconfml
from cone.storage.filestorage import FileStorage
import report_util
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
VERSION = '1.0'
REPORT_SHORTCUTS = {
'api': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_api_report_template.html'),
'api_report.html',
"Create a report of the configuration's ConfML API."),
'value': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_value_report_template.html'),
'value_report.html',
"Create a report of the configuration's data values. Multiple "\
"configurations can also be given using --configurations"),
'value_csv': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_value_report_template.csv'),
'value_report.csv',
"Create a report of the configuration's data values (CSV format)."),
'api_csv': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_api_report_template.csv'),
'api_report.csv',
"Create a report of the configuration's ConfML API (CSV format)."),
'impl': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_impl_report_template.html'),
'impl_report.html',
'Create a report of all implementations in the configuration.'),
'content': report_util.ReportShortcut(
os.path.join(ROOT_PATH, 'info_content_report_template.html'),
'content_report.html',
'Create a report of the content files in the configuration.'),
}
def main():
shortcut_container = report_util.ReportShortcutContainer(REPORT_SHORTCUTS,
None)
parser = OptionParser(version="%%prog %s" % VERSION)
parser.add_options(cone_common.COMMON_OPTIONS)
parser.add_option("-c", "--configuration",
action="append",
dest="configs",
help="Defines a configuration to use in report generation or info printout. "\
"May be defined more than once, but multiple configuration will only "\
"do anything if the report type supports them. If multiple configurations "\
"are not supported, the first one will be used",
metavar="CONFIG",
default=[])
parser.add_option("--config-wildcard",\
action="append",
dest="config_wildcards",
help="Wildcard pattern for including configurations, e.g. product_langpack_*_root.confml",
metavar="WILDCARD",
default=[])
parser.add_option("--config-regex",\
action="append",
dest="config_regexes",
help="Regular expression for including configurations, e.g. product_langpack_\\d{2}_root.confml",
metavar="REGEX",
default=[])
parser.add_option("-p", "--project",\
dest="project",\
help="defines the location of current project. Default is the current working directory.",\
default=".",\
metavar="STORAGE")
info_group = OptionGroup(parser, 'Info options',
'The info functionality is meant for printing information about the '\
'contents of a cpf/zip file or Configuration Project (folder). Two '\
'separate use cases are currently supported: '\
'1. Printing basic information about a project or configuration to '\
' stdout. '\
'2. Creating a report of the contents of a configuration or configurations.')
info_group.add_option("--report-type",
help="The type of the report to generate. This is a convenience "\
"switch for setting the used template. "\
"Possible values: "\
+ shortcut_container.get_shortcut_help_text(),
metavar="TYPE",\
default=None)
info_group.add_option("--report",
help="The file where the configuration info report is written."\
"By default this value is determined by the used "\
"report type. Example: --report report.html.",
metavar="FILE",\
default=None)
info_group.add_option("--template",
help="Template used in a report generation. By default "\
"this value is determined by the used report type. "\
"Example: --template report_template.html.",
metavar="FILE",\
default=None)
info_group.add_option("--impl-filter",
help="The pattern used for filtering implementations for the "\
"report. See the switch --impl in action generate for "\
"more info. ",
metavar="PATTERN",
default='.*')
info_group.add_option("--view-file",
help="External ConfML file containing the view used for filtering "\
"the features listed in the setting value report. The first view "\
"defined in the file will be used.",
metavar="FILE",
default=None)
parser.add_option_group(info_group)
(options, args) = parser.parse_args()
cone_common.handle_common_options(options)
if not shortcut_container.is_valid_shortcut(options.report_type):
parser.error("Invalid report type: %s" % options.report_type)
if (options.report_type or options.template) and \
(not options.configs and not options.config_wildcards and not options.config_regexes):
parser.error("Report type or template specified but configuration(s) not given!")
if options.view_file and not os.path.isfile(options.view_file):
parser.error("No such file: %s" % options.view_file)
# Load view from the specified file if necessary
view = None
if options.view_file:
try:
view = _load_view_from_file(options.view_file)
print "Loaded view '%s' from '%s'" % (view.get_name(), options.view_file)
except ViewLoadError, e:
print e
sys.exit(1)
current = api.Project(api.Storage.open(options.project,"r"))
print "Opened project in %s" % options.project
# Get a list of configurations if necessary
config_list = None
if options.configs or options.config_wildcards or options.config_regexes:
try:
config_list = cone_common.get_config_list_from_project(
project = current,
configs = options.configs,
config_wildcards = options.config_wildcards,
config_regexes = options.config_regexes)
except cone_common.ConfigurationNotFoundError, e:
parser.error(str(e))
# Specifying configurations using --configuration should always either result
# in an error or a configuration, so if there are no configurations, it
# means that the user specified only wildcards and/or patterns, and none
# matched
if len(config_list) == 0:
parser.error("No matching configurations for wildcard(s) and/or pattern(s).")
if config_list is not None:
# One or more configurations have been specified
if options.report_type is not None or options.template is not None:
# Generating a report
# Create a list of configurations
configs = []
for config_name in config_list:
configs.append(current.get_configuration(config_name))
# Generate the report
if options.report_type: report_name = options.report_type + '_info'
else: report_name = 'info'
template, report = shortcut_container.determine_template_and_report(
options.report_type,
options.template,
options.report,
report_name)
data_providers = {'impl_data' : ImplDataProvider(configs[0], options.impl_filter),
'api_data' : ApiDataProvider(configs[0]),
'content_data': ContentDataProvider(configs[0]),
'value_data' : ValueDataProvider(configs, view)}
report_util.generate_report(template, report, {'data': ReportDataProxy(data_providers)})
else:
# Printing configuration info
config_name = config_list[0]
config = current.get_configuration(config_name)
print "Opened configuration %s" % config_name
print "Features %s" % len(config.get_default_view().list_all_features())
print "Impl files %s" % len(plugin.get_impl_set(config).list_implementation())
else:
print "Configurations in the project."
configlist = current.list_configurations()
configlist.sort()
for config in configlist:
print config
if current: current.close()
# ============================================================================
# Report data proxy and data providers
# ============================================================================
class ReportDataProxy(object):
"""
Proxy object for loading report data on demand.
It is used so that e.g. when generating an API report, the
implementations are not unnecessarily loaded. The class utilizes
ReportDataProviderBase objects to handle the actual data generation,
and logs any exceptions that happen there.
"""
def __init__(self, data_providers):
assert isinstance(data_providers, dict), "data_providers must be a dict!"
self._data_providers = data_providers
def __getattr__(self, attrname):
if attrname in self._data_providers:
try:
return self._data_providers[attrname].get_data()
except Exception, e:
utils.log_exception(logging.getLogger('cone'),
"Exception getting %s: %s" % (attrname, e))
else:
return super(ReportDataProxy, self).__getattr__(attrname)
class ReportDataProviderBase(object):
"""
Report data provider base class for lazy-loading of report data.
"""
def get_data(self):
"""
Return the report data.
The data is generated on the first call to this, and later the
cached data is returned.
"""
CACHE_ATTRNAME = "__datacache"
if hasattr(self, CACHE_ATTRNAME):
return getattr(self, CACHE_ATTRNAME)
else:
data = self.generate_data()
setattr(self, CACHE_ATTRNAME, data)
return data
def generate_data(self):
"""
Generate the actual report data. Called when get_data() is called
the first time.
"""
raise NotImplmentedError()
# ----------------------------------------------------------------------------
class ApiDataProvider(ReportDataProviderBase):
def __init__(self, config):
self._config = config
def generate_data(self):
columns = {'fqr':'Full reference',
'name':'Name',
'type':'Type',
'desc':'Description',
}
data = self._get_feature_api_data(self._config, columns)
data.sort(key=lambda item: item['fqr'])
return {'columns' : columns,
'data' : data}
@classmethod
def _get_feature_api_data(cls, config, column_dict):
# Traverse through all features in the api
# and construct the data rows
data = []
storageroot = os.path.abspath(config.get_storage().get_path())
for elem in config.get_default_view().get_features('**'):
elemfile = os.path.join(storageroot,elem._obj.find_parent(type=api.Configuration).get_full_path())
#print "elemfile %s " % elemfile
featurerow = {'file': elemfile}
for col in column_dict:
try:
featurerow[col] = getattr(elem,col) or ''
except AttributeError,e:
#logging.getLogger('cone').warning('Could not find attribute %s from %s' % (col, elem.fqr))
featurerow[col] = ''
data.append(featurerow)
return data
class ImplDataProvider(ReportDataProviderBase):
def __init__(self, config, impl_filters):
self._config = config
self._impl_filters = impl_filters
def generate_data(self):
impl_set = plugin.filtered_impl_set(self._config, [self._impl_filters or '.*'])
return impl_set.get_all_implementations()
class ContentDataProvider(ReportDataProviderBase):
def __init__(self, config):
self._config = config
def generate_data(self):
class Entry(object):
pass
data = []
layered_content = self._config.layered_content()
for ref in sorted(layered_content.list_keys()):
entry = Entry()
entry.file = ref
entry.actual_files = layered_content.get_values(ref)
data.append(entry)
return data
class ValueDataProvider(ReportDataProviderBase):
class FeatureGroup(object):
def __init__(self, name, features):
self.name = name
self.features = features
class Feature(object):
def __init__(self, **kwargs):
self.ref = kwargs['ref']
self.name = kwargs['name']
self.type = kwargs['type']
self.desc = kwargs['desc']
self.options = kwargs['options']
class Config(object):
def __init__(self, path, values):
self.path = path
self.values = values
class SequenceColumn(object):
def __init__(self, ref, name, type):
self.ref = ref
self.name = name
self.type = type
def __eq__(self, other):
if type(self) == type(other):
for varname in ('ref', 'name', 'type'):
if getattr(self, varname) != getattr(other, varname):
return False
return True
else:
return False
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "SequenceColumn(ref=%r, name=%r, type=%r)" \
% (self.ref, self.name, self.type)
class SequenceData(object):
def __init__(self, columns, rows):
self.columns = columns
self.rows = rows
self.is_sequence_data = True
def __eq__(self, other):
if type(self) == type(other):
for varname in ('columns', 'rows'):
if getattr(self, varname) != getattr(other, varname):
return False
return True
else:
return False
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "SequenceData(columns=%r, rows=%r)" \
% (self.columns, self.rows)
def __init__(self, configs, view):
assert len(configs) > 0, "configs must contain at least one configuration!"
self._configs = configs
self._view = view
def generate_data(self):
configs = self._configs
view = self._view
# Get the feature list from the first configuration
feature_groups = self._get_feature_groups(self._configs[0], view)
# Load setting values from all configurations
output_configs = [] # List of self.Config objects, not api.Configuration objects
for i, config in enumerate(self._configs):
print "Loading configuration %s (%d of %d)" % (config.get_path(), i + 1, len(self._configs))
dview = config.get_default_view()
values = {}
for group in feature_groups:
for entry in group.features:
try:
feature = dview.get_feature(entry.ref)
values[entry.ref] = self._resolve_value(feature)
except exceptions.NotFound:
pass
output_configs.append(self.Config(config.get_path(), values))
# Add a 'modified' attribute to all features
for group in feature_groups:
for feature in group.features:
modified = False
first_value_set = False
first_value = None
for output_config in output_configs:
if feature.ref not in output_config.values:
continue
if not first_value_set:
first_value = output_config.values[feature.ref]
first_value_set = True
else:
if output_config.values[feature.ref] != first_value:
modified = True
break
feature.modified = modified
return {'feature_groups' : feature_groups,
'configs' : output_configs}
def _resolve_value(self, feature):
"""
Resolve the value of the given feature (must be a data proxy).
@param feature: The feature whose value is to be resolved.
@return: The resolved value (value directly from the feature, name of a selection option,
or a SequenceData object.
"""
assert isinstance(feature, api._FeatureDataProxy)
if isinstance(feature._obj, api.FeatureSequence):
return self._get_sequence_data(feature)
return self._resolve_option_value(feature, feature.get_value())
def _resolve_option_value(self, feature, value):
"""
Resolve an option value for the given feature.
@param feature: The feature, can be a data proxy or the feature object itself.
@param value: The value to resolve.
@return: The resolved value; the name of the selected option if possible,
otherwise the value that was passed in.
"""
if isinstance(feature, api._FeatureDataProxy):
feature = feature._obj
for option in self._get_options(feature):
if option.get_value() == value:
return option.get_name()
return value
def _get_sequence_data(self, seq_feature):
"""
Return a SequenceData object based on the given sequence feature.
"""
assert isinstance(seq_feature, api._FeatureDataProxy)
assert isinstance(seq_feature._obj, api.FeatureSequence)
sub_feature_objs = []
columns = []
for obj in seq_feature._obj._objects():
if isinstance(obj, api.Feature):
col = self.SequenceColumn(obj.ref, obj.name, obj.type)
columns.append(col)
sub_feature_objs.append(obj)
rows = []
for value_row in seq_feature.get_value():
row = {}
for index, value_item in enumerate(value_row):
ref = columns[index].ref
sub_feature = sub_feature_objs[index]
value = self._resolve_option_value(sub_feature, value_item)
row[ref] = value
rows.append(row)
return self.SequenceData(columns, rows)
def _get_feature_groups(self, config, view):
"""
Return a list of FeatureGroup objects generated based on the given configuration and view.
@param configuration: The configuration to use.
@param view: The view to use. Can be None, in which case all features in the
configuration will be used.
"""
feature_groups = []
if view is None:
feature_list = self._get_feature_list(config.get_default_view().get_features('**'))
feature_groups = [self.FeatureGroup('All settings', feature_list)]
else:
# Populate the view so that it contains the settings and
# get_features() can be used
config._add(view)
view.populate()
# Recursively collect a flattened list of all groups
def visit_group(group, name_prefix):
name = None
# Ignore the name for the view, record only group-level names
# (the view cannot directly contain settings according to the
# spec, they need to be inside a group)
if not isinstance(group, api.View):
if name_prefix: name = name_prefix + ' -- ' + group.get_name()
else: name = group.get_name()
# Add features if necessary
features = self._get_feature_list(group.get_features('*'))
if len(features) > 0:
feature_groups.append(self.FeatureGroup(name, features))
# Recurse to child groups
for child in group._objects():
if isinstance(child, api.Group):
visit_group(child, name)
visit_group(view, None)
return feature_groups
def _get_feature_list(self, feature_objects):
"""
Return a list of feature data entries based on the given features.
@param feature_objects: List of api._FeatureDataProxy objects, e.g.
the output of api.Group.get_features().
@return: List of ValueDataProvider.Feature objects.
"""
refs = set()
feature_list = []
for elem in feature_objects:
# Ignore elements with no type (they are not settings)
if not hasattr(elem, 'type') or elem.type in (None, ''):
continue
# For sequences don't include sub-settings, only the
# sequence settings themselves
feature = self._get_parent_sequence_or_self(elem._obj)
ref = feature.fqr
# Don't add if it's already in the list
if ref in refs: continue
else: refs.add(ref)
feature_data = self.Feature(
ref = ref,
name = feature.name,
type = feature.type,
desc = feature.desc,
options = self._get_options(feature))
feature_list.append(feature_data)
return feature_list
def _get_options(self, feature):
"""
Return a list of api.Option objects for the given feature.
"""
if isinstance(feature, api._FeatureDataProxy):
feature = feature._obj
options = []
for obj in feature._objects():
if isinstance(obj, api.Option):
options.append(obj)
return options
def _get_parent_sequence_or_self(self, feature):
"""
Return the parent sequence of the given feature, or the feature
itself if it is not a sequence sub-setting.
"""
current = feature._parent
while current is not None:
if isinstance(current, api.FeatureSequence):
return current
current = current._parent
return feature
# ============================================================================
# Helper functions
# ============================================================================
class ViewLoadError(RuntimeError):
"""Exception raised if _load_view_from_file() fails"""
pass
def _load_view_from_file(filename):
"""
Load the first view from the given ConfML file.
@raise ViewLoadError: An error occurred when loading the file.
"""
file_abspath = os.path.abspath(filename)
file_dir = os.path.dirname(file_abspath)
file_name = os.path.basename(file_abspath)
# Open the view file inside a "project" so that XIncludes are
# handled properly
try:
view_project = api.Project(FileStorage(file_dir, 'r'))
view_config = view_project.get_configuration(file_name)
views = view_config._traverse(type=api.View)
except Exception, e:
import traceback
logging.getLogger('cone').debug(traceback.format_exc())
raise ViewLoadError("Error parsing view ConfML file: %s" % e)
if len(views) == 0:
raise ViewLoadError("No views in specified view ConfML file '%s'" % filename)
elif len(views) == 1:
return views[0]
else:
print "Found %d view(s) in file '%s', using the last one" % (len(views), filename)
return views[-1]
# ============================================================================
if __name__ == "__main__":
main()