From 419aaa9bd6363d18e1e95b1922b3c4ff7820f56c Mon Sep 17 00:00:00 2001
From: Krzysztof Jamrog <krzysztof.piotr.jamrog@cern.ch>
Date: Mon, 15 Oct 2018 22:08:18 +0200
Subject: [PATCH] iconfTool

Former-commit-id: d79ef673f649e706f036f602a2baf12029efdcbc
---
 Control/AthenaConfiguration/CMakeLists.txt    |   2 +-
 .../python/iconfTool/gui/__init__.py          |   0
 .../python/iconfTool/gui/modals.py            |  67 +++++
 .../python/iconfTool/gui/pad.py               | 283 ++++++++++++++++++
 .../python/iconfTool/gui/wrappers.py          | 165 ++++++++++
 .../python/iconfTool/iconfTool                |  53 ++++
 .../python/iconfTool/models/__init__.py       |   0
 .../python/iconfTool/models/element.py        | 180 +++++++++++
 .../python/iconfTool/models/loaders.py        |  95 ++++++
 .../python/iconfTool/models/structure.py      | 263 ++++++++++++++++
 .../python/iconfTool/utils/__init__.py        |   0
 .../python/iconfTool/utils/logger.py          |  34 +++
 .../python/iconfTool/utils/serialization.py   |  19 ++
 13 files changed, 1160 insertions(+), 1 deletion(-)
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/gui/__init__.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/gui/modals.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/gui/pad.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/gui/wrappers.py
 create mode 100755 Control/AthenaConfiguration/python/iconfTool/iconfTool
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/models/__init__.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/models/element.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/models/loaders.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/models/structure.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/utils/__init__.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/utils/logger.py
 create mode 100644 Control/AthenaConfiguration/python/iconfTool/utils/serialization.py

diff --git a/Control/AthenaConfiguration/CMakeLists.txt b/Control/AthenaConfiguration/CMakeLists.txt
index 65c31477247..831c1a51d66 100644
--- a/Control/AthenaConfiguration/CMakeLists.txt
+++ b/Control/AthenaConfiguration/CMakeLists.txt
@@ -11,7 +11,7 @@ atlas_subdir( AthenaConfiguration )
 # Install files from the package:
 atlas_install_python_modules( python/*.py )
 atlas_install_runtime(python/*.pkl )
-atlas_install_scripts( share/confTool.py )
+atlas_install_scripts( share/confTool.py python/iconfTool/iconfTool )
 
 atlas_add_test( ComponentAccumulatorTest
    SCRIPT python -m AthenaConfiguration.ComponentAccumulator 
diff --git a/Control/AthenaConfiguration/python/iconfTool/gui/__init__.py b/Control/AthenaConfiguration/python/iconfTool/gui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Control/AthenaConfiguration/python/iconfTool/gui/modals.py b/Control/AthenaConfiguration/python/iconfTool/gui/modals.py
new file mode 100644
index 00000000000..7761483bc92
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/gui/modals.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import curses.textpad
+
+
+class Modal(object):
+    def __init__(self, start_x, start_y, width, height):
+        self.width = width
+        self.height = height
+        self.start_x = start_x
+        self.start_y = start_y
+        self.window = curses.newwin(height, width, start_y, start_x)
+        self.window.box()
+        self.refresh()
+
+    def refresh(self):
+        self.window.refresh()
+
+    def destroy(self):
+        del self.window
+
+
+class SearchModal(Modal):
+    def __init__(self, start_x, start_y, width, height):
+        super(SearchModal, self).__init__(start_x, start_y, width, height)
+        self.search_window = self.window.derwin(self.height - 2,
+                                                self.width - 2, 1, 1)
+        self.search = curses.textpad.Textbox(self.search_window)
+
+    def edit(self):
+        return self.search.edit().strip()
+
+
+class InputModal(Modal):
+    def __init__(self, start_x, start_y, width, height, message):
+        super(InputModal, self).__init__(start_x, start_y, width, height)
+        self.message_box = self.window.derwin((self.height / 2) - 2,
+                                              self.width - 2,
+                                              (self.height / 2) - 1, 1)
+        self.message_box.addstr(0, (self.width / 2) - len(message) / 2,
+                                message, curses.A_NORMAL)
+        self.message_box.refresh()
+        input_width = 30
+        if input_width > (self.width - 2):
+            input_width = self.width - 2
+        input_start_y, input_start_x = (self.height / 2 + 1,
+                                        self.width / 2 - input_width / 2)
+        self.input_box = self.window.derwin(1, input_width, input_start_y,
+                                            input_start_x)
+        self.input = curses.textpad.Textbox(self.input_box)
+
+    def get_input(self):
+        return self.input.edit().strip()
+
+
+class InfoModal(Modal):
+    def __init__(self, start_x, start_y, width, height, message_lines):
+        super(InfoModal, self).__init__(start_x, start_y, width, height)
+        line_y = 1
+        for line in message_lines:
+            self.window.addstr(line_y, 1, line, curses.A_NORMAL)
+            line_y += 1
+        self.refresh()
+        self.window.getch()
diff --git a/Control/AthenaConfiguration/python/iconfTool/gui/pad.py b/Control/AthenaConfiguration/python/iconfTool/gui/pad.py
new file mode 100644
index 00000000000..7c8359813d0
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/gui/pad.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import curses
+
+from utils import logger as logging
+
+logger = logging.get_logger()
+
+
+class Pad(object):
+    def __init__(self, data_structure, width, height, start_x=0):
+        self.structure = data_structure
+        self.structure_list = self.structure.get_list()
+        self.root_items_names = {element.name for element in
+                                 self.structure_list}
+        self.width = width
+        self.start_x = start_x
+        self.MAX_PAD_HEIGHT = 32000
+        self.pad = curses.newpad(self.MAX_PAD_HEIGHT, width)
+        self.actual_offset = 0
+        self.pad_height = height - 1
+
+        self.mark_character = ' '
+        self.styles = None
+        self.lines = []
+        self.search_text = ''
+
+        self.initialize_styles()
+        self.draw_all_structures()
+        self.actual_y = 0
+        self.initialize_cursor()
+        self.refresh()
+
+    def initialize_styles(self):
+        curses.init_pair(1, curses.COLOR_BLACK,
+                         curses.COLOR_WHITE)  # Sets up color pair
+        highlighted = curses.color_pair(1)  # highlighted menu option
+        normal = curses.A_NORMAL  # non-highlighted menu option
+        curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)
+        marked = curses.color_pair(3)
+        self.styles = {'normal': normal, 'highlighted': highlighted,
+                       'marked': marked}
+
+    def reload_data(self, data_structure):
+        self.structure = data_structure
+        self.structure_list = self.structure.get_list()
+        self.load_root_items_set()
+        self.filter(self.search_text)  # Filter also draws structures
+
+    def initialize_cursor(self):
+        # Highlight cursor on initial position
+        if self.lines_empty():
+            return
+        try:
+            if self.actual_y >= len(self.lines):
+                self.actual_y = 0
+            element = self.lines[self.actual_y]
+            curses.setsyx(self.actual_y, element.x_pos + 1)
+            self.pad.addch(curses.getsyx()[0], curses.getsyx()[1],
+                           element.get_mark_character(),
+                           self.styles['highlighted'])
+            curses.curs_set(0)
+        except IndexError:
+            # Handle situation when self.lines list is empty
+            pass
+
+    def draw_structure(self, structure, y):
+        if y >= self.MAX_PAD_HEIGHT:
+            return
+        style = self.styles['marked' if structure.is_marked() else 'normal']
+        self.pad.addstr(y, structure.x_pos,
+                        '[{}] {}'.format(structure.get_mark_character(),
+                                         structure.get_name()), style)
+        self.lines.insert(y, structure)
+        y += 1
+        if structure.type == 'GROUP' and structure.show_children:
+            for child in structure.children:
+                y = self.draw_structure(child, y)
+        return y
+
+    def draw_all_structures(self):
+        self.lines = []
+        y = 0
+        for i in self.structure_list:
+            if y >= self.MAX_PAD_HEIGHT:
+                break
+            y = self.draw_structure(i, y)
+        self.refresh()
+
+    def refresh(self):
+        self.pad.refresh(self.actual_offset, 0, 0, self.start_x,
+                         self.pad_height, self.width + self.start_x - 2)
+        cursor_pos = curses.getsyx()
+        curses.setsyx(cursor_pos[0], cursor_pos[1] - self.start_x)
+
+    def lines_empty(self):
+        return len(self.lines) == 0
+
+    def clear(self):
+        self.pad.clear()
+        self.refresh()
+
+    def redraw(self, reinit_cursor=False):
+        cursor_pos = curses.getsyx()
+        self.pad.clear()
+        self.draw_all_structures()
+        if self.lines_empty():
+            return
+        if reinit_cursor or cursor_pos[1] == 0:
+            self.actual_offset = 0
+            self.actual_y = 0
+            self.initialize_cursor()
+        else:
+            element = self.lines[self.actual_y]
+            self.pad.move(self.actual_y, cursor_pos[1])
+            self.pad.addch(self.actual_y, cursor_pos[1] - 1,
+                           element.get_mark_character(),
+                           self.styles['highlighted'])
+        self.refresh()
+
+    def filter(self, text, strict=False):
+        """ If is strict then it leaves only elements which fits to text
+        If not then it leaves whole structures in which text appear somewhere
+        """
+        self.search_text = text if text else ''
+        if len(self.search_text) > 0:
+            if strict:
+                self.structure_list = self.structure.get_filtered_by_text(text)
+            else:
+                self.structure_list = filter(
+                    lambda el: el.check_filter(self.search_text),
+                    self.structure.get_list())
+        else:
+            self.structure_list = self.structure.get_list()
+        self.redraw(True)
+
+    def show_diff_only(self):
+        self.structure_list = self.structure.get_filtered_by_mark()
+        self.redraw(True)
+
+    def show_all(self):
+        self.structure_list = self.structure.get_list()
+        self.redraw(True)
+
+    def get_character(self, y, x):
+        current_character = chr(self.pad.inch(y, x) & 0xFF)
+        return current_character
+
+    def move_cursor(self, number):
+        cursor_pos = self.hide_cursor()
+        self.actual_y += number
+        if self.actual_y < len(self.lines):
+            self.set_cursor(cursor_pos[0] + number,
+                            self.lines[self.actual_y].x_pos + 1)
+
+    def hide_cursor(self):
+        cursor_pos = curses.getsyx()
+        current_character = self.get_character(
+            cursor_pos[0] + self.actual_offset, cursor_pos[1] - 1)
+        self.pad.addch(cursor_pos[0] + self.actual_offset, cursor_pos[1] - 1,
+                       current_character,
+                       self.styles['normal'])
+        return cursor_pos
+
+    def show_cursor(self):
+        cursor_pos = curses.getsyx()
+        current_character = self.get_character(
+            cursor_pos[0] + self.actual_offset, cursor_pos[1] - 1)
+        self.pad.addch(cursor_pos[0] + self.actual_offset, cursor_pos[1] - 1,
+                       current_character,
+                       self.styles['highlighted'])
+        self.refresh()
+
+    def set_cursor(self, y, x):
+        curses.setsyx(y, x)
+        cursor_pos = curses.getsyx()
+        current_character = self.get_character(
+            cursor_pos[0] + self.actual_offset, cursor_pos[1])
+        self.pad.addch(cursor_pos[0] + self.actual_offset, cursor_pos[1],
+                       current_character,
+                       self.styles['highlighted'])
+        self.refresh()
+
+    def scroll(self, number):
+        self.actual_offset += number
+        self.refresh()
+
+    def scroll_to_element(self, new_y):
+        self.actual_y = new_y
+        self.hide_cursor()
+        # Handle the situation when element is above current view
+        if new_y < self.actual_offset:
+            self.actual_offset = new_y - 1
+            if self.actual_offset < 0:
+                self.actual_offset = 0
+        elif new_y > self.actual_offset + self.pad_height:
+            self.actual_offset = new_y - 1
+            if self.actual_offset + self.pad_height > len(self.lines):
+                self.actual_offset = len(self.lines) - self.pad_height
+        self.set_cursor(new_y - self.actual_offset,
+                        self.lines[new_y].x_pos + 1)
+
+    def get_index_by_name(self, name):
+        for index, element in enumerate(self.lines):
+            if element.parent is None and element.name == name:
+                return index
+        return -1
+
+    def handle_event(self, event):
+        if event == curses.KEY_DOWN:
+            if self.actual_y < len(self.lines) - 1:
+                self.move_cursor(1)
+                cursor_pos = curses.getsyx()
+                if cursor_pos[0] > self.pad_height - 4:
+                    self.scroll(1)
+
+        elif event == curses.KEY_UP:
+            if len(self.lines) == 0:
+                return
+            if self.actual_y >= 1:
+                self.move_cursor(-1)
+                cursor_pos = curses.getsyx()
+                if cursor_pos[0] < 4 and self.actual_offset > 0:
+                    self.scroll(-1)
+
+        elif event == ord('e'):
+            if self.actual_y >= len(self.lines):
+                return
+            element = self.lines[self.actual_y]
+            if not element.show_children:
+                element.show_children = True
+            else:
+                element.show_children = False
+            self.redraw()
+
+        elif event == curses.KEY_RIGHT:
+            if self.actual_y >= len(self.lines):
+                return
+            element = self.lines[self.actual_y]
+            if element.type == 'GROUP':
+                element.show_children = True
+                self.redraw()
+
+        elif event == curses.KEY_LEFT:
+            if self.actual_y >= len(self.lines):
+                return
+            element = self.lines[self.actual_y]
+            if element.type == 'GROUP':
+                element.show_children = False
+                self.redraw()
+
+        elif event == ord(' '):
+            element = self.lines[self.actual_y]
+            if element.checked:
+                element.set_as_unchecked()
+                self.structure.remove_from_checked(element)
+            else:
+                element.set_as_checked()
+                self.structure.add_to_checked(element)
+            self.redraw()
+
+        elif event == ord('g'):
+            if self.actual_y >= len(self.lines):
+                return
+            element = self.lines[self.actual_y]
+            if not element.type == 'SINGLE':
+                return
+            reference_name = element.get_reference_name()
+            if reference_name not in self.root_items_names:
+                return
+            # Cancel eventual search result
+            self.filter(None)
+            index = self.get_index_by_name(reference_name)
+            if index != -1:
+                self.scroll_to_element(index)
+            else:
+                # Scroll to first element if name was not found
+                # (should never happen as the name is checked
+                # in root_items_names first)
+                self.scroll_to_element(0)
diff --git a/Control/AthenaConfiguration/python/iconfTool/gui/wrappers.py b/Control/AthenaConfiguration/python/iconfTool/gui/wrappers.py
new file mode 100644
index 00000000000..febfba54a6d
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/gui/wrappers.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import curses
+
+from gui.modals import SearchModal
+from gui.pad import Pad
+from utils import logger as logging
+
+logger = logging.get_logger()
+
+
+class GuiLoader(object):
+    def __init__(self, data_loader):
+        self.data_loader = data_loader
+        self.data_structure = self.data_loader.get_data()
+        self.width, self.height, self.pad_height = (0, 0, 0)
+        self.pad = None
+        self.actual_offset = 0
+        self.screen = None
+
+    def __initialize_window(self):
+        self.height, self.width = self.screen.getmaxyx()
+        self.window = curses.newwin(self.height, self.width, 0, 0)
+        self.pad = Pad(self.data_structure, self.width, self.height)
+        self.pad_height = self.height - 1
+        self.pad.refresh()
+
+    def __initialize(self, stdscreen):
+        self.screen = stdscreen
+        self.screen.refresh()
+        self.__initialize_window()
+        self.__start_event_loop()
+
+    def load_gui(self):
+        curses.wrapper(self.__initialize)
+
+    def search(self, strict=False):
+        search_size = 50
+        b_starty = 0
+        b_startx = self.width - search_size - 2
+        b_width = search_size
+        b_height = 3
+        cursor_pos = curses.getsyx()
+        search = SearchModal(b_startx, b_starty, b_width, b_height)
+        text = search.edit()
+        curses.setsyx(cursor_pos[0], cursor_pos[1])
+        self.pad.filter(text, strict)
+
+    def refresh_whole_screen(self):
+        self.window.refresh()
+        self.pad.refresh()
+
+    def __start_event_loop(self):
+        while True:
+            event = self.screen.getch()
+            if event == ord('q'):
+                break
+            elif event == ord('f'):
+                self.search()
+            elif event == ord('m'):
+                self.search(True)
+            elif event == ord('r'):
+                self.data_structure = self.data_loader.get_data()
+                self.pad.reload_data(self.data_structure)
+            else:
+                self.pad.handle_event(event)
+
+
+class DoublePad(object):
+    def __init__(self, data_loader):
+        self.data_loader = data_loader
+        self.data_structure, self.diff_structure = self.data_loader.get_data()
+        self.width, self.height, self.pad_height = (0, 0, 0)
+        self.pad = None
+        self.diff_pad = None
+        self.current_pad = None
+        self.inactive_pad = None
+        self.actual_offset = 0
+        self.screen = None
+        self.show_diff_only = False
+
+    def __initialize_window(self):
+        self.height, self.width = self.screen.getmaxyx()
+        self.window = curses.newwin(self.height, self.width, 0, 0)
+        self.pad = Pad(self.data_structure, self.width / 2 - 1, self.height)
+        self.diff_pad = Pad(self.diff_structure, self.width / 2 - 1,
+                            self.height, self.width / 2 + 2)
+        self.pad_height = self.height - 1
+        self.current_pad, self.inactive_pad = self.pad, self.diff_pad
+        self.diff_pad.hide_cursor()
+        self.diff_pad.refresh()
+        self.pad.refresh()
+
+    def __initialize(self, stdscreen):
+        self.screen = stdscreen
+        self.screen.refresh()
+        self.__initialize_window()
+        self.__start_event_loop()
+
+    def load_gui(self):
+        curses.wrapper(self.__initialize)
+
+    def search(self, strict=False):
+        search_size = 50
+        b_starty = 0
+        b_startx = self.width - search_size - 2
+        b_width = search_size
+        b_height = 3
+        cursor_pos = curses.getsyx()
+        search = SearchModal(b_startx, b_starty, b_width, b_height)
+        text = search.edit()
+        curses.setsyx(cursor_pos[0], cursor_pos[1])
+        self.current_pad.filter(text, strict)
+        self.inactive_pad.filter(text, strict)
+
+    def refresh_whole_screen(self):
+        self.window.refresh()
+        self.current_pad.refresh()
+        self.inactive_pad.refresh()
+
+    def change_actual_pad(self):
+        if self.inactive_pad.lines_empty():
+            return
+        self.current_pad.hide_cursor()
+        self.current_pad, self.inactive_pad = self.inactive_pad, \
+            self.current_pad
+        self.inactive_pad.refresh()
+        self.current_pad.refresh()
+        self.current_pad.show_cursor()
+
+    def switch_to_diff(self):
+        if self.show_diff_only:
+            self.current_pad.show_all()
+            self.inactive_pad.show_all()
+            self.show_diff_only = False
+        else:
+            self.current_pad.show_diff_only()
+            self.inactive_pad.show_diff_only()
+            self.show_diff_only = True
+
+    def __start_event_loop(self):
+        while True:
+            event = self.screen.getch()
+            if event == ord('q'):
+                break
+            elif event == ord('\t'):
+                self.change_actual_pad()
+            elif event == ord('f'):
+                self.search()
+            elif event == ord('m'):
+                self.search(True)
+            elif event == ord('d'):
+                self.switch_to_diff()
+            elif event == ord('r'):
+                self.pad.clear()
+                self.diff_pad.clear()
+                self.data_structure, self.diff_structure = \
+                    self.data_loader.get_data()
+                self.pad.reload_data(self.data_structure)
+                self.diff_pad.reload_data(self.diff_structure)
+            else:
+                self.current_pad.handle_event(event)
diff --git a/Control/AthenaConfiguration/python/iconfTool/iconfTool b/Control/AthenaConfiguration/python/iconfTool/iconfTool
new file mode 100755
index 00000000000..24eff3c5987
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/iconfTool
@@ -0,0 +1,53 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import argparse
+
+from gui.wrappers import GuiLoader, DoublePad
+from models.loaders import ComponentsFileLoader, ComponentsDiffFileLoader
+from utils import logger as logger_config, serialization
+
+if __name__ == '__main__':
+
+    logger_config.setup()
+    logger = logger_config.get_logger()
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-i', '--input', type=str, help='Input file path')
+    parser.add_argument('-d', '--diff', type=str, help='Diff input file path')
+    parser.add_argument('-nr', '--no-replace', action='store_true',
+                        help='Disable replacing references in structure')
+    parser.add_argument('-c', '--config', type=str,
+                        help='Path to configuration file, where marked '
+                             'elements identificators are saved')
+    args = parser.parse_args()
+
+    if not args.input:
+        msg = 'Missing input file'
+        logger.error(msg)
+        exit(msg)
+
+    if args.config:
+        try:
+            checked_elements = serialization.load_strings_set(args.config)
+        except TypeError:
+            logger.warning(
+                'Cannot load checked elements from configuration file')
+            checked_elements = set()
+    else:
+        checked_elements = set()
+
+    if args.diff:
+        loader = ComponentsDiffFileLoader(args.input, args.diff,
+                                          args.no_replace, checked_elements)
+        gui_loader = DoublePad(loader)
+    else:
+        loader = ComponentsFileLoader(args.input, args.no_replace)
+        gui_loader = GuiLoader(loader)
+
+    gui_loader.load_gui()
+
+    if args.config:
+        serialization.save_object(args.config, checked_elements)
diff --git a/Control/AthenaConfiguration/python/iconfTool/models/__init__.py b/Control/AthenaConfiguration/python/iconfTool/models/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Control/AthenaConfiguration/python/iconfTool/models/element.py b/Control/AthenaConfiguration/python/iconfTool/models/element.py
new file mode 100644
index 00000000000..b8178b95555
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/models/element.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import copy
+
+from utils import logger as logging
+
+logger = logging.get_logger()
+
+INDENT = 3
+
+
+class Element(object):
+    def __init__(self, name, x_pos, parent=None):
+        self.name = name
+        self.x_pos = x_pos
+        self.parent = parent
+        self.show_children = False
+        self.marked = False  # For diff displaying
+        self.checked = False  # For disabling diff highlight
+        self.replaced = False  # To prevent replacing multiple times
+        self.hash = None
+        self.generate_hash()
+
+    def get_name(self):
+        return self.name
+
+    def get_name_string(self):
+        return self.name if type(self.name) == str else str(self.name)
+
+    def update_xpos(self, new_xpos):
+        self.x_pos = new_xpos
+
+    def get_as_flat_list(self):
+        return [self]
+
+    def mark(self):
+        self.marked = True
+
+    def has_parent(self, name_string):
+        parent = self.parent
+        while parent:
+            if parent.get_name_string() == name_string:
+                return True
+            parent = parent.parent  # Go level up
+        return False
+
+    def set_parent(self, parent):
+        self.parent = parent
+        self.generate_hash()
+
+    def generate_hash(self):
+        base = self.get_name_string()
+        if self.parent:
+            base = self.parent.hash + base
+        self.hash = str(hash(base))
+
+    def is_checked(self):
+        if self.checked:
+            return True
+        parent = self.parent
+        while parent and parent.marked:
+            if parent.checked:
+                return True
+            parent = parent.parent
+        return False
+
+    def is_marked(self):
+        return self.marked and not self.is_checked()
+
+    def set_as_checked(self):
+        self.checked = True
+
+    def set_as_unchecked(self):
+        self.checked = False
+
+    def get_mark_character(self):
+        return ' ' if not self.checked else 'X'
+
+    def sort_children(self):
+        pass
+
+
+class GroupingElement(Element):
+    def __init__(self, name, x_pos, parent=None):
+        super(GroupingElement, self).__init__(name, x_pos, parent)
+        self.type = 'GROUP'
+        self.children = []
+
+    def check_filter(self, filter_text):
+        if filter_text in self.get_name_string():
+            return True
+        for child in self.children:
+            if child.check_filter(filter_text):
+                return True
+        return False
+
+    def add_child(self, child):
+        self.children.append(child)
+        child.parent = self
+
+    def update_xpos(self, new_xpos):
+        self.x_pos = new_xpos
+        for child in self.children:
+            child.update_xpos(new_xpos + INDENT)
+
+    def get_as_flat_list(self):
+        return [self] + [child.get_as_flat_list() for child in self.children]
+
+    def mark(self):
+        self.marked = True
+        for child in self.children:
+            child.mark()
+
+    def sort_children(self):
+        self.children.sort(key=lambda c: c.get_name_string().lower())
+        for child in self.children:
+            if child.type == 'GROUP':
+                child.sort_children()
+
+
+class TitledGroupingElement(GroupingElement):
+    def __init__(self, name, x_pos, title, parent=None):
+        self.title = title
+        super(TitledGroupingElement, self).__init__(name, x_pos, parent)
+
+    @classmethod
+    def as_copy(cls, name, source_element):
+        element = cls(name, source_element.x_pos, source_element.name)
+        element.parent = source_element.parent
+        element.replaced = source_element.replaced
+        element.children = copy.deepcopy(source_element.children)
+        return element
+
+    def get_name(self):
+        return '{} : {}'.format(self.title, self.name)
+
+    def get_name_string(self):
+        return self.get_name()
+
+    def generate_hash(self):
+        base = self.get_name_string()
+        if self.parent:
+            base = self.parent.hash + base
+        self.hash = str(hash(base))
+
+
+class SingleElement(Element):
+    def __init__(self, name, x_pos, parent=None):
+        super(SingleElement, self).__init__(name, x_pos, parent)
+        self.type = 'SINGLE'
+        self.value_type = False
+
+    def check_filter(self, filter_text):
+        return filter_text in self.get_name_string()
+
+    def get_reference_name(self):
+        return self.name.split('/')[-1] if type(self.name) is str \
+            else str(self.name)
+
+
+class ValueElement(SingleElement):
+    def __init__(self, name, x_pos, value, parent=None):
+        """ Value must be initialized first as it is used in hash generating
+        """
+        self.value = value
+        super(ValueElement, self).__init__(name, x_pos, parent)
+        self.value_type = True
+
+    def get_name(self):
+        return '{} = {}'.format(self.name, self.value)
+
+    def get_name_string(self):
+        return self.get_name()
+
+    def get_reference_name(self):
+        return self.value.split('/')[-1] if type(self.value) == str \
+            else str(self.value)
diff --git a/Control/AthenaConfiguration/python/iconfTool/models/loaders.py b/Control/AthenaConfiguration/python/iconfTool/models/loaders.py
new file mode 100644
index 00000000000..da97a15c819
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/models/loaders.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import pickle
+
+from models.structure import ComponentsStructure
+from utils import logger as logging
+
+logger = logging.get_logger()
+
+
+class ComponentsFileLoader(object):
+    def __init__(self, file_path, no_replace, checked_elements=set()):
+        self.file_path = file_path
+        self.no_replace = no_replace
+        self.checked_elements = checked_elements
+
+    def __load_file_data(self):
+        f = open(self.file_path, 'rb')
+        st = pickle.load(f)
+        f.close()
+        return dict(st)
+
+    def load_structure(self):
+        data = self.__load_file_data()
+        structure = ComponentsStructure(data, self.no_replace,
+                                        self.checked_elements)
+        structure.generate()
+        return structure
+
+    def get_data(self):
+        return self.load_structure()
+
+
+class ComponentsDiffFileLoader(object):
+    def __init__(self, file_path, diff_file_path, no_replace,
+                 checked_elements):
+        self.main_loader = ComponentsFileLoader(file_path, no_replace,
+                                                checked_elements)
+        self.diff_loader = ComponentsFileLoader(diff_file_path, no_replace,
+                                                checked_elements)
+
+    def get_data(self):
+        structure = self.main_loader.load_structure()
+        diff_structure = self.diff_loader.load_structure()
+        self.mark_differences(structure.get_list(), diff_structure.get_list())
+        return structure, diff_structure
+
+    def equal(self, first, second):
+        return first.get_name() == second.get_name() \
+            and first.x_pos == second.x_pos and first.type == second.type
+
+    def mark_differences(self, structure, diff_structure):
+        i, j = 0, 0
+        while i < len(structure) and j < len(diff_structure):
+            if self.equal(structure[i], diff_structure[j]):
+                if structure[i].type == 'GROUP':
+                    self.mark_differences(structure[i].children,
+                                          diff_structure[j].children)
+                i += 1
+                j += 1
+                continue
+
+            # Find equal element in diff structure
+            for tmp_j in range(j, len(diff_structure)):
+                if self.equal(structure[i], diff_structure[tmp_j]):
+                    for marking_j in range(j, tmp_j):
+                        diff_structure[marking_j].mark()
+                    j = tmp_j
+                    break
+            else:
+                # Not found equal element in diff structure
+                # Find equal element in first structure
+                for tmp_i in range(i, len(structure)):
+                    if self.equal(structure[tmp_i], diff_structure[j]):
+                        for marking_i in range(i, tmp_i):
+                            structure[marking_i].mark()
+                        i = tmp_i
+                        break
+                else:
+                    structure[i].mark()
+                    diff_structure[j].mark()
+                    i += 1
+                    j += 1
+
+        # Mark remaining elements in both structures
+        while i < len(structure):
+            structure[i].mark()
+            i += 1
+
+        while j < len(diff_structure):
+            diff_structure[j].mark()
+            j += 1
diff --git a/Control/AthenaConfiguration/python/iconfTool/models/structure.py b/Control/AthenaConfiguration/python/iconfTool/models/structure.py
new file mode 100644
index 00000000000..62ea78ed1fd
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/models/structure.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import ast
+import copy
+import sys
+
+from models.element import SingleElement, GroupingElement, ValueElement
+from utils import logger as logging
+
+logger = logging.get_logger()
+
+INDENT = 3
+
+
+class Structure(object):
+    def __init__(self, flags_dict, checked_elements=set()):
+        self.flags_dict = flags_dict
+        self.structure = {}
+        self.checked_elements = checked_elements  # Set of checked elements
+        # hashed
+
+    def generate_structure_dict(self):
+        """ Split dictionary keys into nested structure
+        e.g. Test1.Test2: Value -> Test1: {Test2: Value}
+        """
+        structure_dict = {}
+        for flag, value in self.flags_dict.iteritems():
+            groups = flag.split('.')
+            structure_point = structure_dict
+            for i in range(len(groups) - 1):
+                if groups[i] not in structure_point:
+                    structure_point[groups[i]] = {}
+                elif type(structure_point[groups[i]]) is str:
+                    structure_point[groups[i]] = {
+                        structure_point[groups[i]]: {}}
+                structure_point = structure_point[groups[i]]
+            if groups:
+                last = groups[-1]
+                structure_point[last] = value
+        return structure_dict
+
+    def generate_for_element(self, grouping_element, structure):
+        child_x_pos = grouping_element.x_pos + INDENT
+        for child in structure:
+            try:
+                # Handles situation when list or dict is provided as a string
+                structure[child] = ast.literal_eval(structure[child])
+            except (ValueError, SyntaxError):
+                pass
+
+            if type(structure[child]) is dict:
+                group = GroupingElement(child, child_x_pos, grouping_element)
+                if group.hash in self.checked_elements:
+                    group.set_as_checked()
+                self.generate_for_element(group, structure[child])
+                grouping_element.add_child(group)
+            elif type(structure[child]) is list:
+                group = GroupingElement(child, child_x_pos, grouping_element)
+                if group.hash in self.checked_elements:
+                    group.set_as_checked()
+                for el in structure[child]:
+                    no_value_child = SingleElement(el, child_x_pos + INDENT)
+                    if no_value_child.hash in self.checked_elements:
+                        no_value_child.set_as_checked()
+                    group.add_child(no_value_child)
+                grouping_element.add_child(group)
+            else:
+                flag = ValueElement(child, child_x_pos, structure[child],
+                                    grouping_element)
+                if flag.hash in self.checked_elements:
+                    flag.set_as_checked()
+                grouping_element.add_child(flag)
+
+    def generate(self, x_pos=0):
+        structure_dict = self.generate_structure_dict()
+        roots = []
+        for root in structure_dict:
+            if type(structure_dict[root]) == dict:
+                root_group = GroupingElement(root, x_pos)
+                self.generate_for_element(root_group, structure_dict[root])
+                roots.append(root_group)
+            else:
+                flag = ValueElement(root, x_pos, structure_dict[root])
+                roots.append(flag)
+        return sorted(roots, key=lambda r: r.name.lower())
+
+    def add_to_checked(self, element):
+        hash_value = element.hash
+        self.checked_elements.add(hash_value)
+
+    def remove_from_checked(self, element):
+        hash_value = element.hash
+        if hash_value in self.checked_elements:
+            self.checked_elements.remove(hash_value)
+
+
+class ComponentsStructure(Structure):
+    def __init__(self, flags_dict, no_replace, checked_elements):
+        super(ComponentsStructure, self).__init__(flags_dict, checked_elements)
+        self.roots_dict = {}
+        self.structure_list = []
+        self.no_replace = no_replace
+        self.filter = None  # Filter object used in search
+
+    def replace_references(self, element, nesting=0):
+        element.replaced = True
+        if element.type != 'GROUP':
+            return
+        for i, child in enumerate(element.children):
+            if element.children[i].type == 'SINGLE':
+                ref_name = element.children[i].get_reference_name()
+                if ref_name in self.roots_dict and not element.has_parent(
+                        self.roots_dict[ref_name].get_name_string()):
+                    element.children[i] = copy.deepcopy(
+                        self.roots_dict[ref_name])
+                    element.children[i].parent = element
+                    element.update_xpos(element.x_pos)
+            if element.children[i].type == 'GROUP' \
+                    and not element.children[i].replaced:
+                # Single element can have been transformed into grouping
+                # element in previous section
+                self.replace_references(element.children[i], nesting + 1)
+
+    def generate(self, x_pos=0):
+        structure_dict = self.generate_structure_dict()
+        self.structure_list = []
+        for root_name in structure_dict:
+            if type(structure_dict[root_name]) == dict:
+                root_group = GroupingElement(root_name, x_pos)
+                if root_group.hash in self.checked_elements:
+                    root_group.set_as_checked()
+                self.generate_for_element(root_group,
+                                          structure_dict[root_name])
+                self.structure_list.append(root_group)
+                self.roots_dict[root_name] = root_group
+            else:
+                flag = ValueElement(root_name, x_pos,
+                                    structure_dict[root_name])
+                if flag.hash in self.checked_elements:
+                    flag.set_as_checked()
+                self.structure_list.append(flag)
+        if not self.no_replace:
+            counter = 1
+            print('{} structures to reconstruct'.format(
+                len(self.structure_list)))
+            for root in self.structure_list:
+                sys.stdout.write('\rReconstructing {}...'.format(counter))
+                sys.stdout.flush()
+                counter += 1
+                self.replace_references(root)
+            sys.stdout.write('\rStructures reconstructed\n')
+            sys.stdout.flush()
+        self.sort()
+        self.filter = ComponentsStructureFilter(self.structure_list)
+
+    def get_list(self):
+        self.filter.reset()
+        return self.structure_list
+
+    def get_as_flat_list(self):
+        self.filter.reset()
+        return [root.get_as_flat_list() for root in self.structure_list]
+
+    def get_filtered_by_text(self, filter_text):
+        return self.filter.generate_by_text(filter_text)
+
+    def get_filtered_by_mark(self):
+        return self.filter.generate_by_mark()
+
+    def sort(self):
+        self.structure_list.sort(key=lambda el: el.get_name_string().lower())
+        for element in self.structure_list:
+            element.sort_children()
+
+
+class ComponentsStructureFilter(object):
+    def __init__(self, structure):
+        self.structure = structure
+        self.output = []  # Found elements with their parents
+        self.found_elements = []  # Elements which are stored in the original
+        self.filter_called = False  # Used to restore found elements to
+        # previous state
+
+    def reset(self):
+        if not self.filter_called:
+            return
+        for element in self.found_elements:
+            if element.parent:
+                element.update_xpos(element.parent.x_pos + INDENT)
+        self.output = []
+        self.found_elements = []
+        self.filter_called = False
+
+    def generate_by_text(self, filter_text):
+        self.reset()
+        self.browse_and_save_by_text(self.structure, filter_text)
+        for element in self.output:
+            element.update_xpos(0)
+        self.filter_called = True
+        return self.output
+
+    def browse_and_save_by_text(self, elements, filter_text):
+        filtered = filter(lambda el: filter_text in el.get_name_string(),
+                          elements)
+        if len(filtered) > 0:
+            # All elements are on the same level and therefore have
+            # the same parents
+            first = filtered[0]
+            if first.parent and first.parent.parent:
+                root_parent = copy.copy(first.parent.parent)
+                direct_parent = copy.copy(first.parent)
+                direct_parent.children = filtered
+                root_parent.children = [direct_parent]
+                self.output.append(root_parent)
+                self.found_elements += filtered
+            elif first.parent:
+                direct_parent = copy.copy(first.parent)
+                direct_parent.children = filtered
+                self.output.append(direct_parent)
+                self.found_elements += filtered
+            else:
+                self.output += filtered
+                self.found_elements += filtered
+        for element in elements:
+            if element.type == 'GROUP':
+                self.browse_and_save_by_text(element.children, filter_text)
+
+    def generate_by_mark(self):
+        self.reset()
+        self.browse_and_save_by_mark(self.structure)
+        for element in self.output:
+            element.update_xpos(0)
+        self.filter_called = True
+        return self.output
+
+    def browse_and_save_by_mark(self, elements):
+        filtered = [el for el in elements if el.is_marked()]
+        to_check = [el for el in elements if not el.is_marked()]
+        if len(filtered) > 0:
+            # All elements are on the same level and therefore have
+            # the same parents
+            first = filtered[0]
+            if first.parent and first.parent.parent:
+                root_parent = copy.copy(first.parent.parent)
+                direct_parent = copy.copy(first.parent)
+                direct_parent.children = filtered
+                root_parent.children = [direct_parent]
+                self.output.append(root_parent)
+                self.found_elements += filtered
+            elif first.parent:
+                direct_parent = copy.copy(first.parent)
+                direct_parent.children = filtered
+                self.output.append(direct_parent)
+                self.found_elements += filtered
+            else:
+                self.output += filtered
+                self.found_elements += filtered
+        for element in to_check:
+            if element.type == 'GROUP':
+                self.browse_and_save_by_mark(element.children)
diff --git a/Control/AthenaConfiguration/python/iconfTool/utils/__init__.py b/Control/AthenaConfiguration/python/iconfTool/utils/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Control/AthenaConfiguration/python/iconfTool/utils/logger.py b/Control/AthenaConfiguration/python/iconfTool/utils/logger.py
new file mode 100644
index 00000000000..3525c10ad63
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/utils/logger.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python2.7
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+import logging
+
+logging_file = 'run.log'
+
+
+def clean_file(filename):
+    with open(filename, 'w'):
+        pass
+
+
+def setup():
+    logger = logging.getLogger('mainLogger')
+    logger.setLevel(logging.DEBUG)
+    clean_file(logging_file)
+    handler = logging.FileHandler(logging_file)
+    handler.setLevel(logging.DEBUG)
+    formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+
+
+def get_logger():
+    return logging.getLogger('mainLogger')
+
+
+def print_logs():
+    with open(logging_file, 'r') as file:
+        for line in file:
+            print(line.rstrip())
diff --git a/Control/AthenaConfiguration/python/iconfTool/utils/serialization.py b/Control/AthenaConfiguration/python/iconfTool/utils/serialization.py
new file mode 100644
index 00000000000..7728e8fd752
--- /dev/null
+++ b/Control/AthenaConfiguration/python/iconfTool/utils/serialization.py
@@ -0,0 +1,19 @@
+import pickle
+
+
+def save_object(file_path, obj):
+    f = open(file_path, 'wb')
+    pickle.dump(obj, f)
+    f.close()
+
+
+def load_strings_set(file_path):
+    f = open(file_path, 'rb')
+    s = pickle.load(f)
+    f.close()
+    if type(s) != set:
+        raise TypeError('Saved object is not a set')
+    for element in s:
+        if type(element) != str:
+            raise TypeError('Set element is not a string')
+    return s
-- 
GitLab