configurationengine/source/scripts/conesub_info.py
changeset 0 2e8eeb919028
child 3 e7e0ae78773e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/configurationengine/source/scripts/conesub_info.py	Thu Mar 11 17:04:37 2010 +0200
@@ -0,0 +1,672 @@
+#
+# 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()