Initial code contribution
diff --git a/demo/Deploy/deployAndRunBattleship.py b/demo/Deploy/deployAndRunBattleship.py
new file mode 100644
index 0000000..958750d
--- /dev/null
+++ b/demo/Deploy/deployAndRunBattleship.py
@@ -0,0 +1,11 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import sys, os
+
+destination = sys.argv[1]
+os.system('python deployBattleship.py ' + destination)
+print 'Deploy finished, starting AppAgent.'
+os.system('cd ' + destination + ' && python AppAgent.py')
\ No newline at end of file
diff --git a/demo/Deploy/deployBattleship.py b/demo/Deploy/deployBattleship.py
new file mode 100644
index 0000000..689fd1a
--- /dev/null
+++ b/demo/Deploy/deployBattleship.py
@@ -0,0 +1,35 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import sys, json, os
+
+destination = sys.argv[1]
+microservicesDir = destination + '/Microservices'
+
+os.system('rm -rf ' + destination)
+if not os.path.exists(destination):
+    os.makedirs(destination)
+
+os.system('cp -ar ../../src/AppAgent.py ' + destination)
+os.system('cp -ar ../../src/LogConfig.json ' + destination)
+os.makedirs(microservicesDir)
+os.system('cp -ar ../../src/Common ' + destination)
+os.system('cp -ar ../../src/Microservices/Authentication ' + microservicesDir)
+os.system('cp -ar ../../src/Microservices/DataSource ' + microservicesDir)
+os.system('cp -ar ../../src/WebGUI/src/WebGUI ' + destination)
+os.system('cp -ar ../../src/WebGUI/src/DsRestAPI/Api/Js ' + destination + '/WebGUI/htdocs/Utils/DsRestAPI')
+os.system('cp -ar ../Microservices/BattleShip ' + microservicesDir)
+os.system('cp -ar ../Microservices/GuiConfigHandlerForBattleship ' + microservicesDir)
+os.system('cp -ar ../Microservices/__init__.py ' + microservicesDir)
+
+with open(destination + '/config.json', 'w') as f:
+    config = {
+        "address": "",
+        "port": 8000,
+        "htdocs": "WebGUI/htdocs",
+        "ssl": False
+    }
+    json.dump(config, f)
+
diff --git a/demo/Microservices/BattleShip/BattleShip.py b/demo/Microservices/BattleShip/BattleShip.py
new file mode 100644
index 0000000..46a2814
--- /dev/null
+++ b/demo/Microservices/BattleShip/BattleShip.py
@@ -0,0 +1,220 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import random, json, os, time
+from Common.DsRestAPI import DsRestAPI
+
+class BattleShip:
+
+    SOURCE_ID = 'BattleShip'
+
+    def __init__(self, directory):
+        self._directory = directory
+        # tp and state
+        # -1: empty -> disabled button
+        # 1: unknown -> buttan that can be pressed
+        # 2: ship on cell -> tp = 1
+        # 3: ship adjacent to cell
+        # 0: hit -> filtered invisible button
+        self._dsRestAPI = DsRestAPI(self._getDataHandler, self._setDataHandler)
+        self._userData = {}
+        self._createNewGame(None)
+
+    def _getDataHandler(self, request, userCredentials):
+        if userCredentials['username'] in self._userData:
+            game = self._userData[userCredentials['username']]
+        else:
+            game = self._createNewGame(userCredentials['username'])
+        element = request['element']
+        params = request['params']
+
+        if element == 'help':
+            with open(os.path.join(self._directory, 'help.json'), 'r') as f:
+                dataElements = json.load(f)
+                help = {'sources': [{'source': self.SOURCE_ID, 'dataElements': dataElements}]}
+            return {'node': {'val': json.dumps(help).encode('hex'), 'tp': 5}}
+        elif element == 'New':
+            return {'node': {'val': '0', 'tp': 1}}
+        elif element == 'HorizontalSize':
+            return {'node': {'val': str(game['horizontalSize']), 'tp': 1}}
+        elif element == 'VerticalSize':
+            return {'node': {'val': str(game['verticalSize']), 'tp': 1}}
+        elif element == 'Ships':
+            return {'node': {'val': ','.join(str(ship) for ship in game['shipSizes']), 'tp': 4}}
+        elif element =='Rows':
+            return {'list': [{'node': {'val': str(i), 'tp': 8}} for i in range(game['verticalSize'])]}
+        elif element == 'Columns':
+            return {'list': [{'node': {'val': str(i), 'tp': 8}} for i in range(game['horizontalSize'])]}
+        elif element == 'Guess' and len(params) == 2:
+            row = None
+            col = None
+            for param in params:
+                if param['paramName'] == 'Row':
+                    row = int(param['paramValue'])
+                if param['paramName'] == 'Column':
+                    col = int(param['paramValue'])
+            if row is None or col is None or row < 0 or row >= game['verticalSize'] or col < 0 or col >= game['horizontalSize'] or game['remaining'] < 0:
+                return {'node': {'val': '0', 'tp': -1}}
+            else:
+                return {'node': {'val': '0', 'tp': min(1, game['board'][row][col])}}
+        elif element == 'Status':
+            remaining = game['remaining']
+            if remaining > 0:
+                return {'node': {'val': '[led:blue]Missing: ' + str(remaining), 'tp': 11}}
+            elif remaining == 0:
+                return {'node': {'val': '[led:green]Victory!', 'tp': 11}}
+            elif remaining == -1:
+                 return {'node': {'val': '[led:yellow]Start a new game!', 'tp': 11}}
+            elif remaining == -2:
+                 return {'node': {'val': '[led:yellow]Creating game...', 'tp': 11}}
+            else:
+                return {'node': {'val': '[led:red]Error: Could not reset board. Increase board size or decrease number of ships.', 'tp': 11}}
+
+    def _setDataHandler(self, request, userCredentials):
+        if userCredentials['username'] is not None:
+            if userCredentials['username'] in self._userData:
+                game = self._userData[userCredentials['username']]
+            else:
+                game = self._createNewGame(userCredentials['username'])
+            element = request['element']
+            params = request['params']
+            content = request['content']
+
+            if element == 'New':
+                self._setBoard(game)
+                return {'node': {'val': '0', 'tp': 1}}
+            elif element == 'HorizontalSize':
+                try:
+                    amount = int(content)
+                    if amount > 0 and amount <= 20:
+                        game['horizontalSize'] = amount
+                        self._setBoard(game)
+                        return {'node': {'val': content, 'tp': 1}}
+                except:
+                    pass
+                return {'node': {'val': content, 'tp': -1}}
+            elif element == 'VerticalSize':
+                try:
+                    amount = int(content)
+                    if amount > 0 and amount <= 20:
+                        game['verticalSize'] = amount
+                        self._setBoard(game)
+                        return {'node': {'val': content, 'tp': 1}}
+                except:
+                    pass
+                return {'node': {'val': content, 'tp': -1}}
+            elif element == 'Ships':
+                try:
+                    game['shipSizes'] = [int(ship) for ship in content.replace(' ', '').split(',')]
+                    self._setBoard(game)
+                    return {'node': {'val': content, 'tp': 4}}
+                except:
+                    return {'node': {'val': content, 'tp': -4}}
+            elif element == 'Guess':
+                row = None
+                col = None
+                for param in params:
+                    if param['paramName'] == 'Row':
+                        row = int(param['paramValue'])
+                    if param['paramName'] == 'Column':
+                        col = int(param['paramValue'])
+                if row is None or col is None or row >= game['verticalSize'] or col >= game['horizontalSize']:
+                    return {'node': {'val': '0', 'tp': -1}}
+                else:
+                    if game['remaining'] > 0:
+                        self._guess(game, row, col)
+                    return {'node': {'val': '0', 'tp': min(1, game['board'][row][col])}}
+
+    def _createNewGame(self, username):
+        self._userData[username] = {
+            'horizontalSize': 10,
+            'verticalSize': 10,
+            'shipSizes': [6, 5, 4, 3, 3, 2, 2],
+            'board': [[-1]*10]*10,
+            'remaining': -1
+        }
+
+    def _guess(self, game, row, col):
+        if game['board'][row][col] in [1, 3]:
+            game['board'][row][col] = -1
+        elif game['board'][row][col] == 2:
+            game['board'][row][col] = 0
+            game['remaining'] -= 1
+
+    def _placeShips(self, game):
+        startTime = time.time()
+        for size in game['shipSizes']:
+            placed = False
+            while not placed:
+                row = random.randint(0, game['verticalSize'] - 1)
+                col = random.randint(0, game['horizontalSize'] - 1)
+                orientation = random.randint(0,4)
+                placed = True
+                i, j = row, col
+                for k in range(size):
+                    if orientation == 0 and j < game['horizontalSize'] and game['board'][i][j] < 2:
+                        j += 1
+                    elif orientation == 1 and i < game['verticalSize'] and game['board'][i][j] < 2:
+                        i += 1
+                    elif orientation == 2 and j >= 0 and game['board'][i][j] < 2:
+                        j -= 1
+                    elif orientation == 3 and i >= 0 and game['board'][i][j] < 2:
+                        i -= 1
+                    else:
+                        placed = False
+                        break
+                if placed:
+                    i, j = row, col
+                    for k in range(size):
+                        game['board'][i][j] = 2
+                        for k in [-1, 0, 1]:
+                            for l in [-1, 0, 1]:
+                                if i+k >= 0 and i+k < game['verticalSize'] and j+l >= 0 and j+l < game['horizontalSize'] and game['board'][i+k][j+l] != 2:
+                                    game['board'][i+k][j+l] = 3
+                        if orientation == 0:
+                            j += 1
+                        elif orientation == 1:
+                            i += 1
+                        elif orientation == 2:
+                            j -= 1
+                        elif orientation == 3:
+                            i -= 1
+                # stop after 2 seconds of trying
+                if time.time() - startTime > 2:
+                    game['remaining'] = -3
+                    self._clearBoard()
+                    return
+
+    def _setBoard(self, game):
+        board = []
+        for i in range(game['verticalSize']):
+            board.append([])
+            for j in range(game['horizontalSize']):
+                board[i].append(1)
+        game['board'] = board
+        game['remaining'] = -2
+        self._placeShips(game)
+        game['remaining'] = sum(game['shipSizes'])
+
+    def _clearBoard(self, game):
+        for i in range(game['verticalSize']):
+            game['board'].append([])
+            for j in range(game['horizontalSize']):
+                game['board'][i].append(-1)
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        response['body'] = json.dumps(self._dsRestAPI.parseRequest(json.loads(body), userCredentials))
+        response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self._getDataHandler,
+                'setDataHandler': self._setDataHandler
+            }
+        }
+
+    def close(self):
+        pass
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/AppConfig.json b/demo/Microservices/BattleShip/GUI/AppConfig.json
new file mode 100644
index 0000000..432104d
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/AppConfig.json
@@ -0,0 +1,12 @@
+{
+    "setup": "BattleShip_Main",
+    "overlayOpacity": 0.25,
+    "overlayEnabledOnSelect": false,
+    "overlayEnabledOnSetData": false,
+    "refreshInterval": 1000,
+    "filesToInclude": [
+        "Utils/DsRestAPI/DsRestAPI.js",
+        "Utils/DsRestAPI/DsRestAPIComm.js"
+    ],
+    "apiExtension": "appagent"
+}
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/AppConfigSchema.json b/demo/Microservices/BattleShip/GUI/AppConfigSchema.json
new file mode 100644
index 0000000..e38a188
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/AppConfigSchema.json
@@ -0,0 +1,53 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "CustomizableApp",
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {
+        "setup": {
+            "description": "The initial setup to load.",
+            "type": "string"
+        },
+        "overlayOpacity": {
+            "description": "The opacity of the overlay when the gui is busy.",
+            "type": "number",
+            "default": 0.0,
+            "minimum": 0.0,
+            "exclusiveMinimum": false,
+            "maximum": 1.0,
+            "exclusiveMaximum": false
+        },
+        "overlayEnabledOnSelect": {
+            "description": "Whether we show the overlay when selection changes.",
+            "type": "boolean",
+            "default": true
+        },
+        "overlayEnabledOnSetData": {
+            "description": "Whether we show the overlay when a setData occures (e.g. when pressing a button).",
+            "type": "boolean",
+            "default": true
+        },
+        "refreshInterval": {
+            "description": "The period in milliseconds in which requests are sent to the server.",
+            "type": "integer",
+            "default": 1000
+        },
+        "filesToInclude": {
+            "description": "Additional javascript files that will be included at start.",
+            "type": "array",
+            "items": {
+                "type": "string",
+                "title": "Javascript files"
+            },
+            "format": "table"
+        },
+        "apiExtension": {
+            "description": "The extension used by the api.",
+            "type": "string",
+            "default": "dsapi"
+        }
+    },
+    "required": [
+        "setup"
+    ]
+}
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Res/main_icon.png b/demo/Microservices/BattleShip/GUI/Res/main_icon.png
new file mode 100644
index 0000000..52487f4
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Res/main_icon.png
Binary files differ
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Desktop.json b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Desktop.json
new file mode 100644
index 0000000..e74ad6d
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Desktop.json
@@ -0,0 +1,73 @@
+{
+    "ViewmodelEditors": [
+        {
+            "pos": {
+                "top": 124,
+                "left": 269
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 3,
+                "left": 270
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        }
+    ],
+    "ViewEditors": [
+        {
+            "pos": {
+                "top": 125,
+                "left": 597
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 4,
+                "left": 590
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        }
+    ],
+    "Imports": [],
+    "HtmlEditor": {
+        "pos": {
+            "top": 64,
+            "left": 892
+        },
+        "visible": false
+    },
+    "RequestEditor": {
+        "openRequests": [
+            [
+                2
+            ],
+            [
+                2,
+                0
+            ]
+        ]
+    }
+}
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Imports.json b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Imports.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Imports.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Request.json b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Request.json
new file mode 100644
index 0000000..712bd22
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Request.json
@@ -0,0 +1,64 @@
+[
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "New"
+        }
+    },
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "Status"
+        }
+    },
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "VerticalSize"
+        }
+    },
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "HorizontalSize"
+        }
+    },
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "Ships"
+        }
+    },
+    {
+        "getData": {
+            "source": "BattleShip",
+            "element": "Rows",
+            "children": [
+                {
+                    "getData": {
+                        "source": "BattleShip",
+                        "element": "Columns",
+                        "children": [
+                            {
+                                "getData": {
+                                    "source": "BattleShip",
+                                    "element": "Guess",
+                                    "params": [
+                                        {
+                                            "paramName": "Row",
+                                            "paramValue": "%Parent0%"
+                                        },
+                                        {
+                                            "paramName": "Column",
+                                            "paramValue": "%Parent1%"
+                                        }
+                                    ]
+                                }
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.css b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.css
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.html b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.html
new file mode 100644
index 0000000..8c8e884
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/Setup.html
@@ -0,0 +1,2 @@
+<div id="Status"></div>
+<div id="BattleShip__Main"></div>
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewInstances.json b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewInstances.json
new file mode 100644
index 0000000..a132929
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewInstances.json
@@ -0,0 +1,112 @@
+[
+    {
+        "class": "CView_ElementTable",
+        "customData": {
+            "class": "TableGray",
+            "displayName": false,
+            "displayHeader": false,
+            "columns": [
+                {
+                    "subView": "CView_BasicButton",
+                    "text": ""
+                }
+            ],
+            "css": {
+                "heght": "auto",
+                "width": "auto"
+            },
+            "processCss": {
+                "table": {
+                    "height": "auto",
+                    "width": "auto",
+                    "border-spacing": "0"
+                },
+                "td": {
+                    "height": "40px",
+                    "width": "40px",
+                    "padding": "0",
+                    "background": "radial-gradient(aqua 10%, red 5%, aqua 85%)",
+                    "cursor": "pointer"
+                },
+                "button": {
+                    "height": "40px",
+                    "width": "40px",
+                    "background-color": "aqua",
+                    "background-image": "none",
+                    "padding": "0",
+                    "border": "0"
+                },
+                "button:disabled": {
+                    "background": "radial-gradient(aqua 10%, DeepSkyBlue 5%, aqua 85%)"
+                },
+                "button:hover:disabled": {
+                    "background": "radial-gradient(aqua 10%, DeepSkyBlue 5%, aqua 85%)"
+                },
+                "button:hover": {
+                    "background": "radial-gradient(red 15%, aqua 5%, aqua 80%)"
+                }
+            },
+            "showToolTip": false
+        },
+        "viewModelIndexes": [
+            0
+        ],
+        "idsCreating": [],
+        "parentID": "BattleShip__Main"
+    },
+    {
+        "class": "CView_ElementAligner",
+        "customData": {
+            "subviews": [
+                {
+                    "subView": "CView_BasicButton",
+                    "elementText": "Start new game",
+                    "isElementLabelPresent": true,
+                    "css": {
+                        "padding": "4px"
+                    }
+                },
+                {
+                    "subView": "CView_Label",
+                    "elementText": "Number of rows",
+                    "isElementLabelPresent": true,
+                    "tooltip": "1 <= x <= 20, will start a new game",
+                    "css": {
+                        "padding": "4px"
+                    }
+                },
+                {
+                    "subView": "CView_Label",
+                    "elementText": "Number of columns",
+                    "isElementLabelPresent": true,
+                    "tooltip": "1 <= x <= 20, will start a new game",
+                    "css": {
+                        "padding": "4px"
+                    }
+                },
+                {
+                    "subView": "CView_Label",
+                    "elementText": "Ships",
+                    "isElementLabelPresent": true,
+                    "tooltip": "comma separated list of integers, will start a new game",
+                    "css": {
+                        "padding": "4px"
+                    }
+                },
+                {
+                    "subView": "CView_StatusLED",
+                    "elementText": "Game status",
+                    "isElementLabelPresent": true,
+                    "css": {
+                        "padding": "4px"
+                    }
+                }
+            ]
+        },
+        "viewModelIndexes": [
+            1
+        ],
+        "idsCreating": [],
+        "parentID": "Status"
+    }
+]
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewModelInstances.json b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewModelInstances.json
new file mode 100644
index 0000000..3cce3c6
--- /dev/null
+++ b/demo/Microservices/BattleShip/GUI/Setups/BattleShip_Main/ViewModelInstances.json
@@ -0,0 +1,54 @@
+[
+    {
+        "class": "CViewModel_DynamicTable",
+        "customData": {
+            "iteratorColumnPosition": -1
+        },
+        "dataPathStrList": [
+            "Rows",
+            "Rows.Columns.Guess"
+        ],
+        "dataPathList": [
+            [
+                5
+            ],
+            [
+                5,
+                0,
+                0
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    },
+    {
+        "class": "CViewModel_ElementRelay",
+        "customData": {},
+        "dataPathStrList": [
+            "New",
+            "VerticalSize",
+            "HorizontalSize",
+            "Ships",
+            "Status"
+        ],
+        "dataPathList": [
+            [
+                0
+            ],
+            [
+                2
+            ],
+            [
+                3
+            ],
+            [
+                4
+            ],
+            [
+                1
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    }
+]
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/__init__.py b/demo/Microservices/BattleShip/__init__.py
new file mode 100644
index 0000000..175b58b
--- /dev/null
+++ b/demo/Microservices/BattleShip/__init__.py
@@ -0,0 +1,15 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+A small BattleShip game for testing purposes and a little fun.
+'''
+
+from BattleShip import BattleShip
+
+EXTENSION = 'api.battleship'
+
+def createHandler(directory, *args):
+    return BattleShip(directory)
\ No newline at end of file
diff --git a/demo/Microservices/BattleShip/help.json b/demo/Microservices/BattleShip/help.json
new file mode 100644
index 0000000..3bdff43
--- /dev/null
+++ b/demo/Microservices/BattleShip/help.json
@@ -0,0 +1,95 @@
+[
+    {
+        "dataElement": {
+            "description": "Help on the available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "List of rows.",
+            "name": "Rows",
+            "valueType": "integerlistType",
+            "typeDescriptor": {
+                "isListOf": true,
+                "typeName": "Row"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "List of columns.",
+            "name": "Columns",
+            "valueType": "integerlistType",
+            "typeDescriptor": {
+                "isListOf": true,
+                "typeName": "Column"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The status of the game.",
+            "name": "Status",
+            "valueType": "statusLEDType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Create a new game.",
+            "name": "New",
+            "valueType": "intType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The horizontal size of the board. Min: 1, Max: 20.",
+            "name": "HorizontalSize",
+            "valueType": "intType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The vertical size of the board. Min: 1, Max: 20.",
+            "name": "VerticalSize",
+            "valueType": "intType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "A comma separated list of ship sizes.",
+            "name": "Ships",
+            "valueType": "charstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Make a guess.",
+            "name": "Guess",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The row.",
+                    "exampleValue": "2",
+                    "name": "Row",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Row"
+                        }
+                    }
+                },
+                {
+                    "description": "The column.",
+                    "exampleValue": "3",
+                    "name": "Column",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Column"
+                        }
+                    }
+                }
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/demo/Microservices/GuiConfigHandlerForBattleship/GuiConfigHandler.py b/demo/Microservices/GuiConfigHandlerForBattleship/GuiConfigHandler.py
new file mode 100644
index 0000000..0bef350
--- /dev/null
+++ b/demo/Microservices/GuiConfigHandlerForBattleship/GuiConfigHandler.py
@@ -0,0 +1,33 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import os, json, traceback, sys
+from Common.BaseGuiConfigHandler import BaseGuiConfigHandler
+
+class GuiConfigHandler(BaseGuiConfigHandler):
+
+    def createConfig(self, userGroups):
+        apps = []
+        default = None
+
+        apps.append({
+            'directory': 'WebApplications/CustomizableApp',
+            'name': 'BattleShip',
+            'icon': 'CustomizableContent/BattleShip/Res/main_icon.png',
+            'params': {
+                'customization': 'CustomizableContent/BattleShip'
+            }
+        })
+
+        apps.append({
+            'directory': 'WebApplications/Authentication',
+            'name': 'Logout',
+            'icon': 'WebApplications/Authentication/logout.png',
+            'params': {
+                'logout': True
+            }
+        })
+
+        return {'availableApps': apps, 'defaultApp': 'BattleShip'}
\ No newline at end of file
diff --git a/demo/Microservices/GuiConfigHandlerForBattleship/__init__.py b/demo/Microservices/GuiConfigHandlerForBattleship/__init__.py
new file mode 100644
index 0000000..4a898f0
--- /dev/null
+++ b/demo/Microservices/GuiConfigHandlerForBattleship/__init__.py
@@ -0,0 +1,15 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+This application handles the available WebApplications for the usergroups.
+'''
+
+from GuiConfigHandler import GuiConfigHandler
+
+EXTENSION = 'MainConfig.json'
+
+def createHandler(directory, *args):
+    return GuiConfigHandler(directory)
\ No newline at end of file
diff --git a/demo/Microservices/GuiConfigHandlerForBattleship/groupRights.json b/demo/Microservices/GuiConfigHandlerForBattleship/groupRights.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/demo/Microservices/GuiConfigHandlerForBattleship/groupRights.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/demo/Microservices/GuiConfigHandlerForBattleship/groupRightsSchema.json b/demo/Microservices/GuiConfigHandlerForBattleship/groupRightsSchema.json
new file mode 100644
index 0000000..80af78b
--- /dev/null
+++ b/demo/Microservices/GuiConfigHandlerForBattleship/groupRightsSchema.json
@@ -0,0 +1,14 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "The list of WebApplications the group has access to.",
+    "type": "array",
+    "items": {
+        "type": "string",
+        "title": "WebApplications",
+        "enum": [
+
+        ]
+    },
+    "format": "table",
+    "uniqueItems": true
+}
diff --git a/demo/Microservices/GuiConfigHandlerForBattleship/help.json b/demo/Microservices/GuiConfigHandlerForBattleship/help.json
new file mode 100644
index 0000000..ca085e6
--- /dev/null
+++ b/demo/Microservices/GuiConfigHandlerForBattleship/help.json
@@ -0,0 +1,9 @@
+[
+    {
+        "dataElement": {
+            "description": "Help on the available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    }
+]
\ No newline at end of file
diff --git a/demo/Microservices/__init__.py b/demo/Microservices/__init__.py
new file mode 100644
index 0000000..c20f3a5
--- /dev/null
+++ b/demo/Microservices/__init__.py
@@ -0,0 +1,5 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/license.txt b/license.txt
new file mode 100644
index 0000000..c29ecb0
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,13 @@
+Copyright (c) 2000-2017 Ericsson Telecom AB

+

+All rights reserved. 

+

+This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at http:www.eclipse.org/legal/epl-v10.html

+

+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

+

+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

+Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

+

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/src/AppAgent.py b/src/AppAgent.py
new file mode 100644
index 0000000..580a6cb
--- /dev/null
+++ b/src/AppAgent.py
@@ -0,0 +1,339 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+#!/usr/bin/python
+
+'''
+The main module of the framework.
+AppAgent is an http server, which redirects http messages based on the message's uri extension to 
+microservices loaded dynamically from the src/Microservices directory. The microservices are handling the 
+application specific http requests by implementing the message handler interface.
+An application based on this framework should contain the framework files, along with application specific 
+microservices located in the src/Microservices directory.
+The DataSource handler is always present, and can be used by all microservices that handle DsRestAPI requests.
+For more details on the microservice- and message handler interface, see the comments for the microservices 
+(src/Microservices/__init__.py).
+'''
+
+import threading, errno, os, sys, shutil, json, traceback, urllib2, logging, logging.config, ssl, importlib
+from BaseHTTPServer import HTTPServer
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+from SocketServer import ThreadingMixIn
+
+class FileHandler(SimpleHTTPRequestHandler):
+    '''A file handler that supports the methods needed by the framework'''
+
+    def _handleProxyRequests(self, method, userCredentials):
+        if userCredentials['username'] is not None:
+            parts = self._cleanPath(self.path).split('/', 2)
+            url = parts[1].decode('hex')
+            if len(parts) > 2:
+                url = url + '/' + parts[2]
+            body = None
+            if method == 'POST' or method == 'PUT':
+                body = self.rfile.read(int(self.headers.getheader('content-length')))
+            try:
+                request = urllib2.Request(url, body, self.headers)
+                request.get_method = lambda: method
+                response = urllib2.urlopen(request)
+                self.send_response(200)
+                headers = response.info()
+                for header in headers:
+                    self.send_header(header, headers[header])
+                self.end_headers()
+                self.wfile.write(response.read())
+            except urllib2.HTTPError as e:
+                self.send_response(e.code)
+                self.end_headers()
+            except Exception as e:
+                self.send_response(404)
+                self.end_headers()
+        else:
+            self.send_response(401)
+            self.end_headers()
+
+    def _lsdir(self, path):
+        if path == '':
+            path = '.'
+        result = []
+        try:
+            files = os.listdir(path)
+            for file in files:
+                fileName = path + '/' + file
+                contentType = ''
+                if os.path.islink(fileName):
+                    contentType += 'l'
+                else:
+                    contentType += '-'
+                if os.path.isdir(fileName):
+                    contentType += 'd'
+                else:
+                    contentType += 'f'
+                stat = os.stat(fileName)
+                result.append({
+                    'fileName': fileName,
+                    'contentType': contentType,
+                    'size': stat.st_size,
+                    'timestamp': stat.st_mtime
+                })
+        except:
+            pass
+        return {'fileList': result}
+
+    def _mkdir(self, path):
+        try:
+            os.makedirs(path)
+            return True
+        except OSError as e:
+            if e.errno == errno.EEXIST and os.path.isdir(path):
+                pass
+            else:
+                return False
+
+    def _rmdir(self, path):
+        try:
+            if not os.path.exists(path):
+                return
+            if os.path.isfile(path) or os.path.islink(path):
+                os.unlink(path)
+            else:
+                shutil.rmtree(path)
+            return True
+        except:
+            return False
+
+    def _cleanPath(self, path):
+        return path.strip('/')
+
+    def do_LSDIR(self):
+        '''Listing the contents of a directory'''
+        path = self._cleanPath(self.path)
+        if path.startswith('proxy'):
+            self._handleProxyRequests('LSDIR', self.server.getUserCredentials(self.headers))
+        else:
+            self.send_response(200)
+            self.send_header('Content-Type', 'application/json')
+            self.end_headers()
+            self.wfile.write(json.dumps(self._lsdir(path), indent = 4))
+
+    def do_RMDIR(self):
+        '''Deleting directory'''
+        userCredentials = self.server.getUserCredentials(self.headers)
+        if userCredentials['username'] is not None:
+            path = self._cleanPath(self.path)
+            if path.startswith('proxy'):
+                self._handleProxyRequests('RMDIR', userCredentials)
+            else:
+                if self._rmdir(path):
+                    self.send_response(200)
+                else:
+                    self.send_response(401)
+                self.send_header('Content-Type', 'text/plain')
+                self.end_headers()
+        else:
+            self.send_response(401)
+            self.end_headers()
+
+    def do_MKDIR(self):
+        '''Creating a directory'''
+        userCredentials = self.server.getUserCredentials(self.headers)
+        if userCredentials['username'] is not None:
+            path = self._cleanPath(self.path)
+            if path.startswith('proxy'):
+                self._handleProxyRequests('MKDIR', userCredentials)
+            else:
+                if self._mkdir(path):
+                    self.send_response(200)
+                else:
+                    self.send_response(401)
+                self.send_header('Content-Type', 'text/plain')
+                self.end_headers()
+        else:
+            self.send_response(401)
+            self.end_headers()
+
+    def do_PUT(self):
+        '''Saving a file'''
+        userCredentials = self.server.getUserCredentials(self.headers)
+        if userCredentials['username'] is not None:
+            path = self._cleanPath(self.path)
+            if path.startswith('proxy'):
+                self._handleProxyRequests('PUT', userCredentials)
+            else:
+                try:
+                    with open(path, 'w') as f:
+                        f.write(self.rfile.read(int(self.headers.getheader('content-length'))))
+                    self.send_response(200)
+                except:
+                    self.send_response(401)
+                self.send_header('Content-Type', 'text/plain')
+                self.end_headers()
+        else:
+            self.send_response(401)
+            self.end_headers()
+
+class MainHandler(FileHandler):
+    '''The main handler of the HTTP server'''
+
+    def _useHandlers(self, method, path, headers, body, userCredentials):
+        '''Try to find a handler based on the extension and handle the request'''
+        try:
+            pathForExtension = path.split('?')[0]
+            if '/' in pathForExtension:
+                extension = pathForExtension[pathForExtension.rindex('/') + 1:]
+            else:
+                extension = pathForExtension
+            handler = self.server.requestHandlers[extension]
+            response = {
+                'returnCode': 200,
+                'mimeType': 'text/plain',
+                'body': '',
+                'headers': {}
+            }
+            handler.handleMessage(method, path, headers, body, userCredentials, response)
+            self.send_response(response['returnCode'])
+            if 'Content-Type' not in response['headers']:
+                self.send_header('Content-Type', 'text/plain')
+            for header in response['headers']:
+                self.send_header(header, response['headers'][header])
+            self.end_headers()
+            self.wfile.write(response['body'])
+            return True
+        except:
+            return False
+
+    def do_GET(self):
+        '''Handler GET requests'''
+        path = self._cleanPath(self.path)
+        if path.startswith('proxy'):
+            self._handleProxyRequests('GET', self.server.getUserCredentials(self.headers))
+        else:
+            if not self._useHandlers('GET', path, self.headers, '', self.server.getUserCredentials(self.headers)):
+                SimpleHTTPRequestHandler.do_GET(self)
+
+    def do_POST(self):
+        '''Handler POST requests'''
+        userCredentials = self.server.getUserCredentials(self.headers)
+        path = self._cleanPath(self.path)
+        if path.startswith('proxy'):
+            self._handleProxyRequests('POST', userCredentials)
+        else:
+            if not self._useHandlers('POST', path, self.headers, self.rfile.read(int(self.headers.getheader('content-length'))), userCredentials):
+                self.send_response(404)
+                self.send_header('Content-Type', 'text/plain')
+                self.end_headers()
+                self.wfile.write(self.path)
+
+    def log_message(self, format, *args):
+        '''Disables logging'''
+        return
+
+class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
+    '''HTTP server that handles requests in a separate thread.'''
+
+def runAppAgent(server, directory, htdocs, ssl, appList = None):
+
+    logger = logging.getLogger('AppAgent')
+
+    sys.path.insert(0, directory)
+
+    server.requestHandlers = {}
+    dsRestApiHandlers = {}
+
+    guiDirectory = os.path.normpath(os.path.join(directory, htdocs))
+    webAppDirectory = os.path.join(guiDirectory, 'WebApplications')
+    customizationDirectory = os.path.join(guiDirectory, 'CustomizableContent')
+    createdLinks = set()
+
+    originalDirectory = os.getcwd()
+    os.chdir(guiDirectory)
+
+    microservicesDirectory = os.path.join(directory, 'Microservices')
+    if appList is None:
+        appList = os.listdir(microservicesDirectory)
+    for app in appList:
+        appPath = os.path.join(microservicesDirectory, app)
+        if os.path.isdir(appPath):
+            appName = 'Microservices.' + app
+            try:
+                module = importlib.import_module(appName)
+                # create the handler
+                handler = module.createHandler(appPath)
+                server.requestHandlers[module.EXTENSION] = handler
+                appWebAppDir = os.path.join(appPath, 'WebApplication')
+                appGuiDir = os.path.join(appPath, 'GUI')
+                # link webapp
+                if os.path.isdir(appWebAppDir):
+                    linkFile = os.path.join(webAppDirectory, app)
+                    createdLinks.add(linkFile)
+                    if not os.path.exists(linkFile):
+                        os.symlink(os.path.relpath(appWebAppDir, webAppDirectory), linkFile)
+                        logger.info('Linked WebApp: ' + os.path.relpath(appWebAppDir, webAppDirectory) + ' -> ' + linkFile)
+                    else:
+                        logger.warning('Linked WebApp: ' + app + ' already exists')
+                # link customization
+                if os.path.isdir(appGuiDir):
+                    linkFile = os.path.join(customizationDirectory, app)
+                    createdLinks.add(linkFile)
+                    if not os.path.exists(linkFile):
+                        os.symlink(os.path.relpath(appGuiDir, customizationDirectory), linkFile)
+                        logger.info('Linked GUI: ' + os.path.relpath(appGuiDir, customizationDirectory) + ' -> ' + linkFile)
+                    else:
+                        logger.warning('Linked GUI: ' + app + ' already exists')
+                # add DataSource handlers if they exist
+                if hasattr(handler, 'getDataSourceHandlers'):
+                    dsRestApiHandlers.update(handler.getDataSourceHandlers())
+                logger.info('Loaded ' + appName + ' serving ' + module.EXTENSION)
+            except Exception as e:
+                logger.exception('Loading ' + appName + ' failed')
+
+    # special microservice logic
+    if 'api.authenticate' in server.requestHandlers:
+        server.getUserCredentials = server.requestHandlers['api.authenticate'].getUserCredentials
+    else:
+        server.getUserCredentials = lambda headers: {'username': None, 'password': None, 'groups': set()}
+    if 'api.appagent' in server.requestHandlers:
+        server.requestHandlers['api.appagent'].setHandlers(dsRestApiHandlers)
+
+    try:
+        if ssl:
+            server.socket = ssl.wrap_socket(server.socket, keyfile=os.path.join(directory, 'localhost.key'), certfile=os.path.join(directory, 'localhost.crt'), server_side=True)
+        logger.info('Server started on ' + server.server_name + ':' + str(server.server_port) + ' serving directory: ' + os.getcwd())
+        server.serve_forever()
+    except:
+        pass
+    finally:
+        logger.info('Stopping server')
+        for handler in server.requestHandlers:
+            server.requestHandlers[handler].close()
+            logger.info("Handler closed: " + handler)
+        os.chdir(directory)
+        for link in createdLinks:
+            if os.path.exists(link):
+                os.unlink(link)
+                logger.info('GUI unlinked: ' + link)
+        os.chdir(originalDirectory)
+        logger.info("Server stopped")
+
+if __name__ == '__main__':
+
+    # Initialize logging facility
+    logConfigPath = 'LogConfig.json'
+    if os.path.exists(logConfigPath):
+        with open(logConfigPath, 'r') as f:
+            logConfig = json.load(f)
+        logging.config.dictConfig(logConfig)
+    else:
+        logging.basicConfig(level="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+    # Logging facility initialization done
+
+    directory = os.path.abspath(os.path.dirname(sys.argv[0]))
+
+    config = {}
+    with open(os.path.join(directory, 'config.json'), 'r') as f:
+        config = json.load(f)
+
+    server = ThreadedHTTPServer((config['address'], config['port']), MainHandler)
+    runAppAgent(server, directory, config['htdocs'], 'ssl' in config and config['ssl'])
diff --git a/src/Common/BaseGuiConfigHandler.py b/src/Common/BaseGuiConfigHandler.py
new file mode 100644
index 0000000..97008f7
--- /dev/null
+++ b/src/Common/BaseGuiConfigHandler.py
@@ -0,0 +1,91 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import json
+from Common.EditableGroupRights import EditableGroupRights
+from Common.EditableGroupRights import HELP as editableGroupsHelp
+
+DSHELP = [
+    {
+        "dataElement": {
+            "description": "Help on the available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    }
+]
+
+LOGIN_CONFIG = '''{
+    "availableApps": [
+        {
+            "directory": "WebApplications/Authentication",
+            "name": "Login"
+        }
+    ],
+    "defaultApp": "Login"
+}'''
+
+class BaseGuiConfigHandler:
+
+    SOURCE_ID = 'WebApplications'
+
+    def __init__(self, directory):
+        self._groupRightsHandler = EditableGroupRights(directory, [])
+
+    def createConfig(self, userGroups):
+        apps = []
+
+        apps.append({
+            'directory': 'WebApplications/Authentication',
+            'name': 'Logout',
+            'icon': 'WebApplications/Authentication/logout.png',
+            'params': {
+                'logout': True
+            }
+        })
+
+        return {'availableApps': apps, 'defaultApp': 'Logout'}
+
+    def hasAccessRight(self, webApp, userGroups):
+        for group in userGroups:
+            if webApp in self._groupRightsHandler.getGroupRights(group):
+                return True
+        return False
+
+    def _getDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+
+        if element == 'help':
+            dataElements = DSHELP + editableGroupsHelp
+            help = {'sources': [{'source': self.SOURCE_ID, 'dataElements': dataElements}]}
+            return {'node': {'val': json.dumps(help).encode('hex'), 'tp': 5}}
+
+        return self._groupRightsHandler.handleGetData(element, params, userCredentials)
+
+    def _setDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+        content = request['content']
+
+        return self._groupRightsHandler.handleSetData(element, params, content, userCredentials)
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        if userCredentials['username'] is None:
+            response['body'] = LOGIN_CONFIG
+        else:
+            response['body'] = json.dumps(self.createConfig(userCredentials['groups']))
+        response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self._getDataHandler,
+                'setDataHandler': self._setDataHandler
+            }
+        }
+
+    def close(self):
+        pass
\ No newline at end of file
diff --git a/src/Common/DsRestAPI.py b/src/Common/DsRestAPI.py
new file mode 100644
index 0000000..b251f8d
--- /dev/null
+++ b/src/Common/DsRestAPI.py
@@ -0,0 +1,189 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''The module containing the implementation of DsRestAPI'''
+
+import json, re, logging
+
+class DsRestAPI:
+    '''The implementation of DsRestAPI'''
+
+    def __init__(self, getDataHandler, setDataHandler):
+        self._logger = logging.getLogger(__name__)
+        self._getDataHandler = getDataHandler
+        self._setDataHandler = setDataHandler
+
+    def _fillFilterFilterParam(self, filter, value, index):
+        return json.loads(json.dumps(filter).replace('%FilterParent' + str(index) + '%', value))
+
+    def _fillFilterParams(self, filter, parents):
+        parameterizedFilter = json.dumps(filter)
+        for i in range(len(parents)):
+            parameterizedFilter = parameterizedFilter.replace('%Parent' + str(i) + '%', parents[i])
+        return json.loads(parameterizedFilter)
+
+    def _filterWalk(self, filter, depth, userCredentials):
+        if 'dataValue' in filter:
+            return filter['dataValue']
+        else:
+            if 'params' in filter['request']:
+                for param in filter['request']['params']:
+                    param['paramValue'] = self._filterWalk(param['paramValue'], depth + 1, userCredentials)
+            else:
+                filter['request']['params'] = []
+            response = self._getDataHandler(filter['request'], userCredentials)
+            if response is None:
+                return 'false'
+            elif 'node' in response:
+                returnValue = response['node']['val']
+                if 'remapTo' in filter['request']:
+                    returnValue = self._filterWalk(self._fillFilterFilterParam(filter['request']['remapTo'], returnValue, depth), depth + 1, userCredentials)
+                return returnValue
+            else:
+                returnValue = [element['node']['val'] for element in response['list']]
+                if 'remapTo' in filter['request']:
+                    for i in range(len(returnValue)):
+                        returnValue[i] = self._filterWalk(self._fillFilterFilterParam(filter['request']['remapTo'], returnValue[i], depth), depth + 1, userCredentials)
+                return json.dumps(returnValue)
+
+    def evaluateFilter(self, filter, parents, userCredentials):
+        try:
+            return 'true' == self._filterWalk(self._fillFilterParams(filter, parents), 0, userCredentials)
+        except:
+            self._logger.exception('Error during filtering')
+            return False
+
+    def _isFilterAllowed(self, filter, parents):
+        for match in re.finditer(r'%Parent(\d+)%', json.dumps(filter)):
+            if match is not None:
+                if int(match.group(1)) >= len(parents):
+                    return False
+        return True
+
+    def _fillGetDataParams(self, parameterizedGetData, originalGetData, parents):
+        parameterizedGetData['source'] = originalGetData['source']
+        parameterizedGetData['element'] = originalGetData['element']
+        if 'ptcname' in originalGetData:
+            parameterizedGetData['ptcname'] = originalGetData['ptcname']
+        if 'params' in originalGetData:
+            parameterizedGetData['params'] = [
+                {
+                    'paramName': param['paramName'],
+                    'paramValue': param['paramValue']
+                } for param in originalGetData['params']
+            ]
+        else:
+            parameterizedGetData['params'] = []
+        if 'rangeFilter' in originalGetData:
+            # this is not modified, so we do not copy it
+            parameterizedGetData['rangeFilter'] = originalGetData['rangeFilter']
+        if 'filter' in originalGetData:
+            # Filter will copy it when filling it the parameters, so we don't have to
+            parameterizedGetData['filter'] = originalGetData['filter']
+        for i in range(len(parents)):
+            pattern = '%Parent' + str(i) + '%'
+            parameterizedGetData['source'] = parameterizedGetData['source'].replace(pattern, parents[i])
+            parameterizedGetData['element'] = parameterizedGetData['element'].replace(pattern, parents[i])
+            if 'ptcname' in parameterizedGetData:
+                parameterizedGetData['ptcname'] = parameterizedGetData['ptcname'].replace(pattern, parents[i])
+            for param in parameterizedGetData['params']:
+                param['paramName'] = param['paramName'].replace(pattern, parents[i])
+                param['paramValue'] = param['paramValue'].replace(pattern, parents[i])
+
+    def _fillSetDataParams(self, parameterizedSetData, originalSetData, parents):
+        self._fillGetDataParams(parameterizedSetData, originalSetData, parents)
+        if 'content' in originalSetData:
+            parameterizedSetData['content'] = originalSetData['content']
+        else:
+            parameterizedSetData['content'] = ''
+        if 'tp' in originalSetData:
+            parameterizedSetData['tp'] = originalSetData['tp']
+        else:
+            parameterizedSetData['tp'] = 0
+        if 'indxsInList' in originalSetData:
+            parameterizedSetData['indxsInList'] = originalSetData['indxsInList']
+        else:
+            parameterizedSetData['indxsInList'] = []
+        for i in range(len(parents)):
+            pattern = '%Parent' + str(i) + '%'
+            parameterizedSetData['content'] = parameterizedSetData['content'].replace(pattern, parents[i])
+
+    def _handleChildRequests(self, response, getData, parents, userCredentials):
+        if 'node' in response:
+            parents.append(response['node']['val'])
+            response['node']['childVals'] = []
+            self._parseRequestList(response['node']['childVals'], getData['children'], parents, userCredentials)
+            parents.pop()
+
+    def _handleGetData(self, responseList, getData, parents, userCredentials):
+        parameterizedGetData = {}
+        self._fillGetDataParams(parameterizedGetData, getData, parents)
+        prefilter = 'filter' in getData and self._isFilterAllowed(getData['filter'], parents)
+        if prefilter and not self.evaluateFilter(getData['filter'], parents, userCredentials):
+            responseList.append({'node': {'val': '', 'tp': 0}})
+            return
+        response = self._getDataHandler(parameterizedGetData, userCredentials)
+        if response is not None:
+            if 'filter' in getData and not prefilter:
+                if 'node' in response:
+                    parents.append(response['node']['val'])
+                    if self._isFilterAllowed(getData['filter'], parents) and not self.evaluateFilter(getData['filter'], parents, userCredentials):
+                        responseList.append({'node': {'val': '', 'tp': 0}})
+                        parents.pop()
+                        return
+                    else:
+                        parents.pop()
+                if 'list' in response:
+                    i = 0
+                    while i < len(response['list']):
+                        parents.append(response['list'][i]['node']['val'])
+                        if self._isFilterAllowed(getData['filter'], parents) and not self.evaluateFilter(getData['filter'], parents, userCredentials):
+                            response['list'].pop(i)
+                        else:
+                            i += 1
+                        parents.pop()
+            if 'rangeFilter' in getData and 'list' in response:
+                response['list'] = response['list'][max(0, getData['rangeFilter']['offset']) : min(len(response['list']), getData['rangeFilter']['offset'] + getData['rangeFilter']['count'])]
+            responseList.append(response)
+
+            if 'children' in getData and len(getData['children']) > 0:
+                if 'list' in response:
+                    if 'selection' in getData:
+                        for index in getData['selection']:
+                            if index < len(response['list']):
+                                self._handleChildRequests(response['list'][index], getData, parents, userCredentials)
+                    else:
+                        for element in response['list']:
+                            self._handleChildRequests(element, getData, parents, userCredentials)
+                else:
+                    self._handleChildRequests(response, getData, parents, userCredentials)
+        else:
+            responseList.append({'node': {'val': 'Failed to handle request: ' + parameterizedGetData['source'] + ' ' + parameterizedGetData['element'] + ' ' + str(parameterizedGetData['params']), 'tp': -4}})
+
+    def _handleSetData(self, responseList, setData, parents, userCredentials):
+        parameterizedSetData = {}
+        self._fillSetDataParams(parameterizedSetData, setData, parents)
+        response = self._setDataHandler(parameterizedSetData, userCredentials)
+        if response is not None:
+            responseList.append(response)
+        else:
+            responseList.append({'node': {'val': 'Failed to handle request: ' + parameterizedSetData['source'] + ' ' + parameterizedSetData['element'] + ' ' + str(parameterizedSetData['params']), 'tp': -4}})
+
+    def _parseRequestList(self, responseList, requestList, parents, userCredentials):
+        for request in requestList:
+            if 'getData' in request:
+                self._handleGetData(responseList, request['getData'], parents, userCredentials)
+            elif 'setData' in request:
+                self._handleSetData(responseList, request['setData'], parents, userCredentials)
+
+    def parseRequest(self, request, userCredentials):
+        parents = []
+        response = {'contentList': []}
+        try:
+            self._parseRequestList(response['contentList'], request['requests'], parents, userCredentials)
+        except Exception as e:
+            self._logger.exception('Error handling DsRestAPI request')
+            response = {'contentList': [{'node': {'val': 'Failed to handle request: ' + str(e), 'tp': 4}}]}
+        return response
\ No newline at end of file
diff --git a/src/Common/EditableGroupRights.py b/src/Common/EditableGroupRights.py
new file mode 100644
index 0000000..8f63203
--- /dev/null
+++ b/src/Common/EditableGroupRights.py
@@ -0,0 +1,91 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import os, json
+try:
+    from jsonschema import validate
+except:
+    def validate(*args):
+        return True
+
+HELP = [
+    {
+        "dataElement": {
+            "description": "The list of groups that have rights.",
+            "name": "HandledGroups",
+            "valueType": "charstringlistType",
+            "typeDescriptor": {
+                "isListOf": True,
+                "typeName": "Group"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The json schema of the group rights as a string encoded json.",
+            "name": "GroupRightsSchema",
+            "valueType": "charstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The rights of the group as a string encoded json.",
+            "name": "GroupRights",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The group name.",
+                    "exampleValue": "some_group",
+                    "name": "Group",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Group"
+                        }
+                    }
+                }
+            ]
+        }
+    }
+]
+
+class EditableGroupRights:
+
+    def __init__(self, directory, default):
+        self._directory = directory
+        self._default = default
+        with open(os.path.join(directory, 'groupRights.json'), 'r') as f:
+            self._groupRights = json.load(f)
+        with open(os.path.join(self._directory, 'groupRightsSchema.json'), 'r') as f:
+            self._schema = json.load(f)
+
+    def _saveGroupRights(self):
+        with open(os.path.join(self._directory, 'groupRights.json'), 'w') as f:
+            json.dump(self._groupRights, f, indent = 4)
+
+    def handleGetData(self, element, params, userCredentials):
+        if element == 'GroupRightsSchema':
+            return {'node': {'val': json.dumps(self._schema), 'tp': 4}}
+        elif element == 'GroupRights' and len(params) == 1 and params[0]['paramName'] == 'Group':
+            return {'node': {'val': json.dumps(self._groupRights.get(params[0]['paramValue'], self._default)), 'tp': 4}}
+        elif element == 'HandledGroups':
+            return {'list': [{'node': {'val': group, 'tp': 10}} for group in self._groupRights]}
+
+    def handleSetData(self, element, params, content, userCredentials):
+        if 'admin' in userCredentials['groups']:
+            if element == 'GroupRights' and len(params) == 1 and params[0]['paramName'] == 'Group':
+                rights = json.loads(content)
+                if rights is None:
+                    self._groupRights.pop(params[0]['paramValue'], self._default)
+                    self._saveGroupRights()
+                    return {'node': {'val': '0', 'tp': 1}}
+                elif validate(rights, self._schema):
+                    self._groupRights[params[0]['paramValue']] = rights
+                    self._saveGroupRights()
+                    return {'node': {'val': '0', 'tp': 1}}
+                else:
+                    return {'node': {'val': 'Rigths does not match the schema.', 'tp': 4}}
+
+    def getGroupRights(self, group):
+        return self._groupRights.get(group, self._default)
diff --git a/src/Common/__init__.py b/src/Common/__init__.py
new file mode 100644
index 0000000..689fa71
--- /dev/null
+++ b/src/Common/__init__.py
@@ -0,0 +1,6 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''This package contains common functionality for AppAgent and the handlers.'''
\ No newline at end of file
diff --git a/src/LogConfig.json b/src/LogConfig.json
new file mode 100644
index 0000000..90f5062
--- /dev/null
+++ b/src/LogConfig.json
@@ -0,0 +1,36 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "simple": {
+            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+        }
+    },
+
+    "handlers": {
+    
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": "ERROR",
+            "formatter": "simple",
+            "stream": "ext://sys.stdout"
+        },
+        
+        "file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "DEBUG",
+            "formatter": "simple",
+            "filename": "log.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        }
+        
+  
+    },
+
+    "root": {
+        "level": "DEBUG",
+        "handlers": ["console", "file_handler"]
+    }
+}
diff --git a/src/LogConfig_XL.json b/src/LogConfig_XL.json
new file mode 100644
index 0000000..facc333
--- /dev/null
+++ b/src/LogConfig_XL.json
@@ -0,0 +1,74 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "simple": {
+            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+        }
+    },
+
+    "handlers": {
+    
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": "DEBUG",
+            "formatter": "simple",
+            "stream": "ext://sys.stdout"
+        },
+        
+        "debug_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "DEBUG",
+            "formatter": "simple",
+            "filename": "debug.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        },
+
+        "info_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "INFO",
+            "formatter": "simple",
+            "filename": "info.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        },
+        
+        "warning_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "WARNING",
+            "formatter": "simple",
+            "filename": "warning.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        },
+
+        "error_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "ERROR",
+            "formatter": "simple",
+            "filename": "error.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        }
+        
+  
+    },
+
+    "loggers": {
+        "my_module": {
+            "level": "ERROR",
+            "handlers": ["console"],
+            "propagate": "no"
+        }
+    },
+
+    "root": {
+        "level": "INFO",
+        "handlers": ["console", "debug_file_handler", "info_file_handler", "warning_file_handler", "error_file_handler"]
+    }
+}
diff --git a/src/Microservices/Authentication/AuthenticationHandler.py b/src/Microservices/Authentication/AuthenticationHandler.py
new file mode 100644
index 0000000..eaa9f96
--- /dev/null
+++ b/src/Microservices/Authentication/AuthenticationHandler.py
@@ -0,0 +1,128 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import json, uuid, logging
+from Common.DsRestAPI import DsRestAPI
+from LdapAuthenticator import authenticate
+from FileUserHandler import FileUserHandler as UserHandler
+from Help import DSHELP
+
+class AuthenticationHandler:
+    '''
+    This class handles the authentication and user groups
+    Requests coming to login/api.authenticate are used for logging in.
+    Requests coming to logout/api.authenticate are used for logging out.
+    DsRestAPI requests can be used to edit the user groups.
+    AppAgent can use the getUserCredentials public function to get the user credentials of the current user.
+    '''
+
+    SOURCE_ID = 'Authentication'
+
+    def __init__(self, directory):
+        self._sessionDb = {}
+        self._userHandler = UserHandler(directory)
+        self._logger = logging.getLogger(__name__)
+        self._dsRestAPI = DsRestAPI(self._getDataHandler, self._setDataHandler)
+
+    def _login(self, body):
+        authenticate(body['username'], body['password'])
+        sessionId = str(uuid.uuid4())
+        groups = self._userHandler.getUserGroups(body['username'])
+        self._sessionDb[sessionId] = {'username': body['username'], 'password': body['password'], 'groups': groups}
+        return sessionId
+
+    def _getSessionIdFromHeaders(self, headers):
+        cookieHeader = headers.get('cookie')
+        sessionId = None
+        if cookieHeader != None:
+            for cookie in cookieHeader.split(';'):
+                if cookie.strip().startswith('PYSESSID='):
+                    sessionId = cookie.strip().replace('PYSESSID=', '')
+                    break
+        return sessionId
+
+    def getUserCredentials(self, headers):
+        sessionId = self._getSessionIdFromHeaders(headers)
+        if sessionId in self._sessionDb:
+            return self._sessionDb[sessionId]
+        else:
+            return {'username': None, 'password': None, 'groups': set([])}
+
+    def _getDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+
+        if element == 'help':
+            help = {'sources': [{'source': self.SOURCE_ID, 'dataElements': DSHELP}]}
+            return {'node': {'val': json.dumps(help).encode('hex'), 'tp': 5}}
+        elif element == 'ListGroups':
+            return {'list': [{'node': {'val': group, 'tp': 10}} for group in self._userHandler.listGroups()]}
+        elif element == 'ListUsersInGroup' and len(params) == 1 and params[0]['paramName'] == 'Group':
+            return {'list': [{'node': {'val': user, 'tp': 10}} for user in self._userHandler.listUsersInGroup(params[0]['paramValue'])]}
+        elif element == 'ListGroupsOfUser' and len(params) == 1 and params[0]['paramName'] == 'User':
+            return {'list': [{'node': {'val': group, 'tp': 10}} for group in self._userHandler.getUserGroups(params[0]['paramValue'])]}
+        elif element in ['RemoveGroup', 'RemoveUserFromGroup']:
+            return {'node': {'val': '0', 'tp': 1}}
+        elif element in ['AddGroup', 'AddUserToGroup']:
+            return {'node': {'val': '0', 'tp': 4}}
+
+    def _setDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+        content = request['content']
+        tp = request['tp']
+
+        if 'admin' in userCredentials['groups']:
+            if element == 'AddGroup':
+                self._userHandler.addGroup(content)
+                return {'node': {'val': content, 'tp': tp}}
+            elif element == 'RemoveGroup' and len(params) == 1 and params[0]['paramName'] == 'Group':
+                self._userHandler.removeGroup(params[0]['paramValue'])
+                return {'node': {'val': '0', 'tp': 1}}
+            elif element == 'AddUserToGroup' and len(params) == 1 and params[0]['paramName'] == 'Group':
+                self._userHandler.addUserToGroup(content, params[0]['paramValue'])
+                return {'node': {'val': content, 'tp': tp}}
+            elif element == 'RemoveUserFromGroup' and len(params) == 2 and params[0]['paramName'] == 'User' and params[1]['paramName'] == 'Group':
+                self._userHandler.removeUserFromGroup(params[0]['paramValue'], params[1]['paramValue'])
+                return {'node': {'val': '0', 'tp': 1}}
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        if path.startswith('login'):
+            try:
+                loginInfo = json.loads(body)
+                sessionId = self._login(loginInfo)
+                response['returnCode'] = 200
+                # increase the year when needed
+                response['headers']['Set-Cookie'] = 'PYSESSID=' + sessionId + '; Path=/; expires=Thu, 01 Jan 2070 00:00:00 UTC;'
+                self._logger.info('Login ' + sessionId + ' ' + loginInfo['username'])
+            except Exception as e:
+                self._logger.exception('Failed to login')
+                response['returnCode'] = 401
+                response['body'] = str(e)
+        elif path.startswith('logout'):
+            try:
+                sessionId = self._getSessionIdFromHeaders(headers)
+                self._sessionDb.pop(sessionId, None)
+                response['returnCode'] = 200
+                response['headers']['Set-Cookie'] = 'PYSESSID=' + '; Path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;'
+                self._logger.info('Logout ' + sessionId + ' ' + str(userCredentials['username']))
+            except Exception as e:
+                self._logger.exception('Failed to logout ' + str(userCredentials['username']))
+                response['returnCode'] = 400
+                response['body'] = str(e)
+        else:
+            response['body'] = json.dumps(self._dsRestAPI.parseRequest(json.loads(body), userCredentials))
+            response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self._getDataHandler,
+                'setDataHandler': self._setDataHandler
+            }
+        }
+
+    def close(self):
+        pass
\ No newline at end of file
diff --git a/src/Microservices/Authentication/FileUserHandler.py b/src/Microservices/Authentication/FileUserHandler.py
new file mode 100644
index 0000000..038272e
--- /dev/null
+++ b/src/Microservices/Authentication/FileUserHandler.py
@@ -0,0 +1,64 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import json, os
+
+class FileUserHandler:
+    '''
+    This class stores usergroup information in a json file and provides an interface for editing the usergroups.
+    '''
+
+    def __init__(self, directory):
+        self._directory = directory
+        with open(os.path.join(self._directory, 'userGroups.json'), 'r') as f:
+            self._userGroups = json.load(f)
+
+    def _saveUserGroups(self):
+        with open(os.path.join(self._directory, 'userGroups.json'), 'w') as f:
+            json.dump(self._userGroups, f, indent = 4)
+
+    def getUserGroups(self, username):
+        return set(self._userGroups['users'].get(username, []))
+
+    def addGroup(self, group, save = True):
+        if group not in self._userGroups['groups']:
+            self._userGroups['groups'].append(group)
+            if save:
+                self._saveUserGroups()
+
+    def removeGroup(self, group, save = True):
+        if group in self._userGroups['groups']:
+            self._userGroups['groups'].remove(group)
+            for user in self._userGroups['users'].keys():
+                self.removeUserFromGroup(user, group, False)
+            if save:
+                self._saveUserGroups()
+
+    def addUserToGroup(self, username, group, save = True):
+        if group in self._userGroups['groups']:
+            if username not in self._userGroups['users']:
+                self._userGroups['users'][username] = []
+            if group not in self._userGroups['users'][username]:
+                self._userGroups['users'][username].append(group)
+                if save:
+                    self._saveUserGroups()
+
+    def removeUserFromGroup(self, username, group, save = True):
+        if username in self._userGroups['users'] and group in self._userGroups['users'][username]:
+            self._userGroups['users'][username].remove(group)
+            if len(self._userGroups['users'][username]) == 0:
+                self._userGroups['users'].pop(username, [])
+            if save:
+                self._saveUserGroups()
+
+    def listGroups(self):
+        return self._userGroups['groups']
+
+    def listUsersInGroup(self, group):
+        users = set()
+        for user in self._userGroups['users']:
+            if group in self._userGroups['users'][user]:
+                users.add(user)
+        return users
diff --git a/src/Microservices/Authentication/GUI/AppConfig.json b/src/Microservices/Authentication/GUI/AppConfig.json
new file mode 100644
index 0000000..f1937f4
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/AppConfig.json
@@ -0,0 +1,12 @@
+{
+    "setup": "UserGroupEditor",
+    "overlayOpacity": 0.25,
+    "overlayEnabledOnSelect": true,
+    "overlayEnabledOnSetData": true,
+    "refreshInterval": -1,
+    "filesToInclude": [
+        "Utils/DsRestAPI/DsRestAPI.js",
+        "Utils/DsRestAPI/DsRestAPIComm.js"
+    ],
+    "apiExtension": "appagent"
+}
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/AppConfigSchema.json b/src/Microservices/Authentication/GUI/AppConfigSchema.json
new file mode 100644
index 0000000..e38a188
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/AppConfigSchema.json
@@ -0,0 +1,53 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "CustomizableApp",
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {
+        "setup": {
+            "description": "The initial setup to load.",
+            "type": "string"
+        },
+        "overlayOpacity": {
+            "description": "The opacity of the overlay when the gui is busy.",
+            "type": "number",
+            "default": 0.0,
+            "minimum": 0.0,
+            "exclusiveMinimum": false,
+            "maximum": 1.0,
+            "exclusiveMaximum": false
+        },
+        "overlayEnabledOnSelect": {
+            "description": "Whether we show the overlay when selection changes.",
+            "type": "boolean",
+            "default": true
+        },
+        "overlayEnabledOnSetData": {
+            "description": "Whether we show the overlay when a setData occures (e.g. when pressing a button).",
+            "type": "boolean",
+            "default": true
+        },
+        "refreshInterval": {
+            "description": "The period in milliseconds in which requests are sent to the server.",
+            "type": "integer",
+            "default": 1000
+        },
+        "filesToInclude": {
+            "description": "Additional javascript files that will be included at start.",
+            "type": "array",
+            "items": {
+                "type": "string",
+                "title": "Javascript files"
+            },
+            "format": "table"
+        },
+        "apiExtension": {
+            "description": "The extension used by the api.",
+            "type": "string",
+            "default": "dsapi"
+        }
+    },
+    "required": [
+        "setup"
+    ]
+}
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Res/main_icon.png b/src/Microservices/Authentication/GUI/Res/main_icon.png
new file mode 100644
index 0000000..4598dee
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Res/main_icon.png
Binary files differ
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Desktop.json b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Desktop.json
new file mode 100644
index 0000000..9b09366
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Desktop.json
@@ -0,0 +1,230 @@
+{
+    "ViewmodelEditors": [
+        {
+            "pos": {
+                "top": 28,
+                "left": 205
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ],
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 172,
+                "left": 227
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ],
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 196,
+                "left": 239
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 2,
+                "left": 206
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 124,
+                "left": 207
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 149,
+                "left": 242
+            },
+            "visible": false
+        },
+        {
+            "pos": {
+                "top": 97,
+                "left": 204
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    1
+                ]
+            ]
+        }
+    ],
+    "ViewEditors": [
+        {
+            "pos": {
+                "top": 28,
+                "left": 424
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ],
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 172,
+                "left": 520
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ],
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 194,
+                "left": 439
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 124,
+                "left": 427
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 2,
+                "left": 426
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 50,
+                "left": 596
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 148,
+                "left": 687
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ],
+                [
+                    2
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 98,
+                "left": 429
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    0
+                ]
+            ]
+        },
+        {
+            "pos": {
+                "top": 74,
+                "left": 596
+            },
+            "visible": false,
+            "openNodes": [
+                [
+                    2
+                ]
+            ]
+        }
+    ],
+    "Imports": [],
+    "HtmlEditor": {
+        "pos": {
+            "top": 150,
+            "left": 835
+        },
+        "visible": false
+    },
+    "RequestEditor": {
+        "openRequests": [
+            [
+                1
+            ],
+            [
+                2
+            ],
+            [
+                2,
+                1
+            ]
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Imports.json b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Imports.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Imports.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Request.json b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Request.json
new file mode 100644
index 0000000..d03b90b
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Request.json
@@ -0,0 +1,176 @@
+[
+    {
+        "getData": {
+            "source": "Authentication",
+            "element": "AddGroup"
+        }
+    },
+    {
+        "getData": {
+            "source": "Authentication",
+            "element": "ListGroups",
+            "children": [
+                {
+                    "getData": {
+                        "source": "Authentication",
+                        "element": "RemoveGroup",
+                        "params": [
+                            {
+                                "paramName": "Group",
+                                "paramValue": "%Parent0%"
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "getData": {
+            "source": "Authentication",
+            "element": "ListGroups",
+            "children": [
+                {
+                    "getData": {
+                        "source": "Authentication",
+                        "element": "AddUserToGroup",
+                        "params": [
+                            {
+                                "paramName": "Group",
+                                "paramValue": "%Parent0%"
+                            }
+                        ]
+                    }
+                },
+                {
+                    "getData": {
+                        "source": "Authentication",
+                        "element": "ListUsersInGroup",
+                        "params": [
+                            {
+                                "paramName": "Group",
+                                "paramValue": "%Parent0%"
+                            }
+                        ],
+                        "children": [
+                            {
+                                "getData": {
+                                    "source": "Authentication",
+                                    "element": "RemoveUserFromGroup",
+                                    "params": [
+                                        {
+                                            "paramName": "User",
+                                            "paramValue": "%Parent1%"
+                                        },
+                                        {
+                                            "paramName": "Group",
+                                            "paramValue": "%Parent0%"
+                                        }
+                                    ]
+                                }
+                            }
+                        ]
+                    }
+                },
+                {
+                    "getData": {
+                        "source": "DataSource",
+                        "element": "Sources",
+                        "filter": {
+                            "request": {
+                                "source": "DataSource",
+                                "element": "and",
+                                "params": [
+                                    {
+                                        "paramName": "Par1",
+                                        "paramValue": {
+                                            "request": {
+                                                "source": "DataSource",
+                                                "element": "dataElementPresent",
+                                                "params": [
+                                                    {
+                                                        "paramName": "Source",
+                                                        "paramValue": {
+                                                            "dataValue": "%Parent1%"
+                                                        }
+                                                    },
+                                                    {
+                                                        "paramName": "Element",
+                                                        "paramValue": {
+                                                            "dataValue": "GroupRights"
+                                                        }
+                                                    },
+                                                    {
+                                                        "paramName": "ParamName",
+                                                        "paramValue": {
+                                                            "dataValue": "Group"
+                                                        }
+                                                    },
+                                                    {
+                                                        "paramName": "ParamValue",
+                                                        "paramValue": {
+                                                            "dataValue": "%Parent0%"
+                                                        }
+                                                    }
+                                                ]
+                                            }
+                                        }
+                                    },
+                                    {
+                                        "paramName": "Par2",
+                                        "paramValue": {
+                                            "request": {
+                                                "source": "DataSource",
+                                                "element": "dataElementPresent",
+                                                "params": [
+                                                    {
+                                                        "paramName": "Source",
+                                                        "paramValue": {
+                                                            "dataValue": "%Parent1%"
+                                                        }
+                                                    },
+                                                    {
+                                                        "paramName": "Element",
+                                                        "paramValue": {
+                                                            "dataValue": "GroupRightsSchema"
+                                                        }
+                                                    }
+                                                ]
+                                            }
+                                        }
+                                    }
+                                ]
+                            }
+                        },
+                        "children": [
+                            {
+                                "getData": {
+                                    "source": "%Parent1%",
+                                    "element": "GroupRights",
+                                    "params": [
+                                        {
+                                            "paramName": "Group",
+                                            "paramValue": "%Parent0%"
+                                        }
+                                    ]
+                                }
+                            },
+                            {
+                                "getData": {
+                                    "source": "%Parent1%",
+                                    "element": "GroupRightsSchema"
+                                }
+                            }
+                        ],
+                        "selection": [
+                            0
+                        ]
+                    }
+                }
+            ],
+            "selection": [
+                0
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.css b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.css
new file mode 100644
index 0000000..7e3cc67
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.css
@@ -0,0 +1,3 @@
+#UserGroupEditor {
+    height: 100%;
+}
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.html b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.html
new file mode 100644
index 0000000..520bd46
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/Setup.html
@@ -0,0 +1 @@
+<div id="UserGroupEditor"></div>
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewInstances.json b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewInstances.json
new file mode 100644
index 0000000..5046f5e
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewInstances.json
@@ -0,0 +1,161 @@
+[
+    {
+        "class": "CView_ElementTable",
+        "customData": {
+            "class": "TableGray",
+            "columns": [
+                {},
+                {
+                    "subView": "CView_BasicButton",
+                    "text": "Remove Group",
+                    "css": {
+                        "float": "right"
+                    }
+                }
+            ],
+            "displayHeader": false,
+            "name": "Groups",
+            "css": {
+                "margin": "2px"
+            }
+        },
+        "viewModelIndexes": [
+            0
+        ],
+        "idsCreating": [],
+        "parentID": "UserGroupEditor_CView_Aligner_0_CView_Aligner_1"
+    },
+    {
+        "class": "CView_TabsWithData",
+        "customData": {
+            "flex": 3,
+            "css": {
+                "margin": "2px"
+            }
+        },
+        "viewModelIndexes": [
+            1
+        ],
+        "idsCreating": [
+            "UserGroupEditor_CView_Aligner_2_CView_TabsWithData_0"
+        ],
+        "parentID": "UserGroupEditor_CView_Aligner_2"
+    },
+    {
+        "class": "CView_JSONEditor",
+        "customData": {
+            "manualSaveRequired": true,
+            "initOnRefresh": true,
+            "headerText": "Application rights"
+        },
+        "viewModelIndexes": [
+            2
+        ],
+        "idsCreating": [],
+        "parentID": "UserGroupEditor_CView_Aligner_2_CView_TabsWithData_0"
+    },
+    {
+        "class": "CView_ElementTable",
+        "customData": {
+            "class": "TableGray",
+            "displayHeader": false,
+            "columns": [
+                {},
+                {
+                    "subView": "CView_BasicButton",
+                    "text": "Remove User",
+                    "css": {
+                        "float": "right"
+                    }
+                }
+            ],
+            "name": "Users",
+            "css": {
+                "margin": "2px"
+            }
+        },
+        "viewModelIndexes": [
+            4
+        ],
+        "idsCreating": [],
+        "parentID": "UserGroupEditor_CView_Aligner_1_CView_Aligner_1"
+    },
+    {
+        "class": "CView_InputButton",
+        "customData": {
+            "prompt": "Please enter the name of the new group.",
+            "text": "Create new group",
+            "css": {
+                "float": "right"
+            }
+        },
+        "viewModelIndexes": [
+            3
+        ],
+        "idsCreating": [],
+        "parentID": "UserGroupEditor_CView_Aligner_0_CView_Aligner_0"
+    },
+    {
+        "class": "CView_Aligner",
+        "customData": {
+            "orientation": "vertical",
+            "flex": 1,
+            "css": {
+                "overflow": "auto"
+            }
+        },
+        "viewModelIndexes": [],
+        "idsCreating": [
+            "UserGroupEditor_CView_Aligner_0_CView_Aligner_0",
+            "UserGroupEditor_CView_Aligner_0_CView_Aligner_1"
+        ],
+        "parentID": "UserGroupEditor_CView_Aligner_0"
+    },
+    {
+        "class": "CView_Aligner",
+        "customData": {
+            "flexCalculation": true,
+            "resizable": true
+        },
+        "viewModelIndexes": [
+            5
+        ],
+        "idsCreating": [
+            "UserGroupEditor_CView_Aligner_0",
+            "UserGroupEditor_CView_Aligner_1",
+            "UserGroupEditor_CView_Aligner_2"
+        ],
+        "parentID": "UserGroupEditor"
+    },
+    {
+        "class": "CView_InputButton",
+        "customData": {
+            "prompt": "Please enter the name of the new user who will be dded to the group.",
+            "text": "Add user to group",
+            "css": {
+                "float": "right"
+            }
+        },
+        "viewModelIndexes": [
+            6
+        ],
+        "idsCreating": [],
+        "parentID": "UserGroupEditor_CView_Aligner_1_CView_Aligner_0"
+    },
+    {
+        "class": "CView_Aligner",
+        "customData": {
+            "orientation": "vertical",
+            "flex": 1,
+            "css": {
+                "overflow": "auto"
+            }
+        },
+        "viewModelIndexes": [],
+        "idsCreating": [
+            "UserGroupEditor_CView_Aligner_1_CView_Aligner_0",
+            "UserGroupEditor_CView_Aligner_1_CView_Aligner_1"
+        ],
+        "parentID": "UserGroupEditor_CView_Aligner_1"
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewModelInstances.json b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewModelInstances.json
new file mode 100644
index 0000000..037aae8
--- /dev/null
+++ b/src/Microservices/Authentication/GUI/Setups/UserGroupEditor/ViewModelInstances.json
@@ -0,0 +1,118 @@
+[
+    {
+        "class": "CViewModel_DynamicTable",
+        "customData": {},
+        "dataPathStrList": [
+            "ListGroups"
+        ],
+        "dataPathList": [
+            [
+                1
+            ]
+        ],
+        "selectionToControlPathStrList": [
+            "ListGroups"
+        ],
+        "selectionToControlPathList": [
+            [
+                2
+            ]
+        ]
+    },
+    {
+        "class": "CViewModel_ElementRelay",
+        "customData": {},
+        "dataPathStrList": [
+            "ListGroups.Sources"
+        ],
+        "dataPathList": [
+            [
+                2,
+                2
+            ]
+        ],
+        "selectionToControlPathStrList": [
+            "ListGroups.Sources"
+        ],
+        "selectionToControlPathList": [
+            [
+                2,
+                2
+            ]
+        ]
+    },
+    {
+        "class": "CViewModel_JSONEditor",
+        "customData": {},
+        "dataPathStrList": [
+            "ListGroups.Sources.GroupRights",
+            "ListGroups.Sources.GroupRightsSchema"
+        ],
+        "dataPathList": [
+            [
+                2,
+                2,
+                0
+            ],
+            [
+                2,
+                2,
+                1
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    },
+    {
+        "class": "CViewModel_ElementRelay",
+        "customData": {},
+        "dataPathStrList": [
+            "AddGroup"
+        ],
+        "dataPathList": [
+            [
+                0
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    },
+    {
+        "class": "CViewModel_DynamicTable",
+        "customData": {},
+        "dataPathStrList": [
+            "ListGroups.ListUsersInGroup"
+        ],
+        "dataPathList": [
+            [
+                2,
+                1
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    },
+    {
+        "class": "CViewModel_FlexAligner",
+        "customData": {},
+        "dataPathStrList": [],
+        "dataPathList": [],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    },
+    {
+        "class": "CViewModel_ElementRelay",
+        "customData": {},
+        "dataPathStrList": [
+            "ListGroups.AddUserToGroup"
+        ],
+        "dataPathList": [
+            [
+                2,
+                0
+            ]
+        ],
+        "selectionToControlPathStrList": [],
+        "selectionToControlPathList": []
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Authentication/Help.py b/src/Microservices/Authentication/Help.py
new file mode 100644
index 0000000..ae7c6ec
--- /dev/null
+++ b/src/Microservices/Authentication/Help.py
@@ -0,0 +1,145 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+DSHELP = [
+    {
+        "dataElement": {
+            "description": "Help on the available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The available groups.",
+            "name": "ListGroups",
+            "valueType": "charstringlistType",
+            "typeDescriptor": {
+                "isListOf": True,
+                "typeName": "Group"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Add a user group.",
+            "name": "AddGroup",
+            "valueType": "charstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Remove a user group.",
+            "name": "RemoveGroup",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The group name.",
+                    "exampleValue": "some_group",
+                    "name": "Group",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Group"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Add user to the group.",
+            "name": "AddUserToGroup",
+            "valueType": "charstringType",
+            "parameters": [
+                {
+                    "description": "The group name.",
+                    "exampleValue": "some_group",
+                    "name": "Group",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Group"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "List the users in the group.",
+            "name": "ListUsersInGroup",
+            "valueType": "charstringlistType",
+            "parameters": [
+                {
+                    "description": "The group name.",
+                    "exampleValue": "some_group",
+                    "name": "Group",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Group"
+                        }
+                    }
+                }
+            ],
+            "typeDescriptor": {
+                "isListOf": True,
+                "typeName": "User"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "List the groups of a user.",
+            "name": "ListGroupsOfUser",
+            "valueType": "charstringlistType",
+            "parameters": [
+                {
+                    "description": "The user signum.",
+                    "exampleValue": "eabcxyz",
+                    "name": "User",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "User"
+                        }
+                    }
+                }
+            ],
+            "typeDescriptor": {
+                "isListOf": True,
+                "typeName": "Group"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Remove the user from the group.",
+            "name": "RemoveUserFromGroup",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The user signum.",
+                    "exampleValue": "eabcxyz",
+                    "name": "User",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "User"
+                        }
+                    }
+                },
+                {
+                    "description": "The group name.",
+                    "exampleValue": "some_group",
+                    "name": "Group",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Group"
+                        }
+                    }
+                }
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Authentication/LdapAuthenticator.py b/src/Microservices/Authentication/LdapAuthenticator.py
new file mode 100644
index 0000000..dc40f18
--- /dev/null
+++ b/src/Microservices/Authentication/LdapAuthenticator.py
@@ -0,0 +1,51 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+This module provides an authenticate(username, password) function that uses LDAP to authenticate a user.
+'''
+
+import ldap, logging
+
+#LDAP_ADDRESS = 'ldaps://159.107.49.152:3269'
+#LDAP_ADDRESS = 'ldaps://ldap-egad.internal.ericsson.com:3269'
+#LDAP_ADDRESS = 'ldaps://ericsson.se:636')
+LDAP_ADDRESS = 'ldaps://sesbiwegad0001.ericsson.se:636'
+USER_PREFIX = 'ERICSSON\\'
+
+def _setLdapOptions(ld):
+    ld.set_option(ldap.OPT_NETWORK_TIMEOUT, 5) # timeout on ldap connection
+    ld.set_option(ldap.OPT_TIMEOUT, 5) # timeout on ldap actions
+    ld.set_option(ldap.OPT_REFERRALS, 0)
+    ld.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
+    ld.set_option(ldap.OPT_DEBUG_LEVEL, 255)
+
+def authenticate(username, password):
+    # admin allowed by default for now
+    if username == 'admin' and password == 'admin':
+        return True
+    logging.getLogger(__name__).info('LDAP authentication for ' + username)
+    if password.strip() == '':
+        raise Exception('Password must not be empty')
+    if username.strip() == '':
+        raise Exception('Username must not be empty')
+    username = USER_PREFIX + username
+
+    # Must be set before initialize. The rest of the option can be set afterwards
+    ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+
+    try:
+        ld = ldap.initialize(LDAP_ADDRESS)
+        _setLdapOptions(ld)
+        ld.simple_bind_s(username, password)
+    except Exception as e:
+        # Extract textual error message from the exception
+        errorMessage = e.args[0]['desc']
+        raise Exception(errorMessage)
+    finally:
+        # Must be invoked otherwise upcoming requests may be denied by LDAP server
+        ld.unbind_s()
+
+    return True
\ No newline at end of file
diff --git a/src/Microservices/Authentication/WebApplication/Main.css b/src/Microservices/Authentication/WebApplication/Main.css
new file mode 100644
index 0000000..9c6dbe6
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/Main.css
@@ -0,0 +1,20 @@
+#Login {
+    text-align: center;
+}
+
+#Login_Windmill {
+    position: relative;
+    width: 180px;
+    height: 165px;
+    animation: spin 10s linear infinite;
+}
+
+@keyframes spin { 100% { transform:rotate(360deg); } }
+
+#Login form, #Login span, #Login label, #Login input {
+    margin: 4px;
+}
+
+#Login_Error {
+    color: red;
+}
\ No newline at end of file
diff --git a/src/Microservices/Authentication/WebApplication/Main.html b/src/Microservices/Authentication/WebApplication/Main.html
new file mode 100644
index 0000000..a6cd57d
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/Main.html
@@ -0,0 +1,13 @@
+<div id="Login">
+    <style id="Login_WebAppStyle" type="text/css"></style>
+    <img id="Login_Windmill" src="WebApplications/Authentication/animation.png">
+    <form method="post">
+        <span><label id="Login_Error"></label></span>
+        <br>
+        <span><label>Username:</label><input name="username"></span>
+        <br>
+        <span><label>Password:</label><input name="password" type="password"></span>
+        <br>
+        <span><input type="submit" value="Login"></span>
+    </form>
+</div>
\ No newline at end of file
diff --git a/src/Microservices/Authentication/WebApplication/Main.js b/src/Microservices/Authentication/WebApplication/Main.js
new file mode 100644
index 0000000..89e7a46
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/Main.js
@@ -0,0 +1,109 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+var WebApplications = WebApplications || [];
+
+WebApplications.push({'application': new Authentication_Application()});
+
+function Authentication_Application() {
+    "use strict";
+
+    var v_appBase = new WebAppBase();
+    var v_fileHandler;
+
+    this.info = function() {
+        return {
+            defaultIcon: "WebApplications/Authentication/login.png",
+            defaultName: "Login"
+        };
+    };
+
+    this.load = function(p_webAppModel, p_params, p_framework)  {
+        v_fileHandler = p_webAppModel.getFileHandler();
+        if (p_params.logout) {
+            v_appBase.load([], [], startLogoutApp, v_fileHandler);
+        } else {
+            v_appBase.load([], [], startLoginApp, v_fileHandler);
+        }
+    };
+
+    this.unload = function(webappUnloaded) {
+        v_appBase.unload(destroy);
+        webappUnloaded(true);
+    };
+    
+    function onWindowResize(event) {
+        if (event.target == window) {
+            $("#Login").height(ViewUtils.getSuggestedHeight("Login"));
+        }
+    }
+
+    function destroy() {
+        $(window).off("resize", onWindowResize);
+        $("#Login").remove();
+    }
+    
+    function loadHtml(callback) {
+        v_fileHandler.loadFile("WebApplications/Authentication/Main.html", function(ok, html) {
+            $("#TSGuiFrameworkMain").append(html);
+            $("#Login_WebAppStyle").load("WebApplications/Authentication/Main.css", function() {
+                $(window).on("resize", onWindowResize);
+                $("#Login").height(ViewUtils.getSuggestedHeight("Login"));
+                $('form').submit(function(event) {
+                    login($('input[name="username"]').val(), $('input[name="password"]').val());
+                    event.preventDefault();
+                });
+                $('input[name="username"]').focus()
+                callback(true);
+            });
+        });
+    }
+
+    function login(username, password) {
+        var credentials = {
+            'username': username,
+            'password': password
+        };
+        
+        $.ajax({
+            url: 'login/api.authenticate',
+            type: 'POST',
+            data: JSON.stringify(credentials),
+            cache: false,
+            success: function() {
+                localStorage.setItem('username', credentials.username);
+                localStorage.setItem('password', credentials.password);
+                location.reload(true);
+            },
+            error: function(jqXHR, textStatus, errorThrown) {
+                $('#Login_Error').text(jqXHR.responseText);
+            }
+        });
+    }
+    
+    function startLoginApp(callback) {
+        loadHtml(callback);
+    }
+    
+    function startLogoutApp(callback) {
+        $.ajax({
+            url: 'logout/api.authenticate',
+            type: 'POST',
+            data: '',
+            cache: false,
+            success: function() {
+                localStorage.removeItem("username");
+                localStorage.removeItem("password");
+                location.reload(true);
+            },
+            error: function(jqXHR, textStatus, errorThrown) {
+                location.reload(true);
+            }
+        });
+        callback(true);
+    }
+}
+
+//# sourceURL=Authentication\Main.js
\ No newline at end of file
diff --git a/src/Microservices/Authentication/WebApplication/animation.png b/src/Microservices/Authentication/WebApplication/animation.png
new file mode 100644
index 0000000..c03e617
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/animation.png
Binary files differ
diff --git a/src/Microservices/Authentication/WebApplication/login.png b/src/Microservices/Authentication/WebApplication/login.png
new file mode 100644
index 0000000..0997a7c
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/login.png
Binary files differ
diff --git a/src/Microservices/Authentication/WebApplication/logout.png b/src/Microservices/Authentication/WebApplication/logout.png
new file mode 100644
index 0000000..34c658b
--- /dev/null
+++ b/src/Microservices/Authentication/WebApplication/logout.png
Binary files differ
diff --git a/src/Microservices/Authentication/__init__.py b/src/Microservices/Authentication/__init__.py
new file mode 100644
index 0000000..cb4cf07
--- /dev/null
+++ b/src/Microservices/Authentication/__init__.py
@@ -0,0 +1,13 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''This package contains the handlers for authentication and user groups'''
+
+from AuthenticationHandler import AuthenticationHandler
+
+EXTENSION = 'api.authenticate'
+
+def createHandler(directory, *args):
+    return AuthenticationHandler(directory)
\ No newline at end of file
diff --git a/src/Microservices/Authentication/userGroups.json b/src/Microservices/Authentication/userGroups.json
new file mode 100644
index 0000000..f12670e
--- /dev/null
+++ b/src/Microservices/Authentication/userGroups.json
@@ -0,0 +1,23 @@
+{
+    "users": {
+        "admin": [
+            "admin"
+        ], 
+        "ekistam": [
+            "users"
+        ], 
+        "ethjgi": [
+            "users"
+        ], 
+        "eistnav": [
+            "users"
+        ], 
+        "ednigbo": [
+            "users"
+        ]
+    }, 
+    "groups": [
+        "admin", 
+        "users"
+    ]
+}
\ No newline at end of file
diff --git a/src/Microservices/DataSource/DataSource.py b/src/Microservices/DataSource/DataSource.py
new file mode 100644
index 0000000..f92cf85
--- /dev/null
+++ b/src/Microservices/DataSource/DataSource.py
@@ -0,0 +1,141 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import json, logging
+from fnmatch import fnmatch
+from Common.DsRestAPI import DsRestAPI
+from Help import DSHELP
+
+class DataSource:
+    '''The DataSource handler'''
+
+    SOURCE_ID = 'DataSource'
+
+    def __init__(self, directory):
+        self._logger = logging.getLogger(__name__)
+        self._handlers = {}
+        self._dsRestAPI = DsRestAPI(self._getDataHandler, self._setDataHandler)
+
+    def setHandlers(self, handlers):
+        self._handlers = handlers
+
+    def _getDataHandler(self, request, userCredentials):
+        if request['source'] == self.SOURCE_ID:
+            return self._handleBuildInGetData(request, userCredentials)
+        elif request['source'] in self._handlers:
+            return self._handlers[request['source']]['getDataHandler'](request, userCredentials)
+
+    def _setDataHandler(self, request, userCredentials):
+        if request['source'] == self.SOURCE_ID:
+            return None
+        elif request['source'] in self._handlers:
+            return self._handlers[request['source']]['setDataHandler'](request, userCredentials)
+
+    def _handleBuildInGetData(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+
+        if element == 'help':
+            help = {'sources': [{'source': self.SOURCE_ID, 'dataElements': DSHELP}]}
+            for handlerName in self._handlers:
+                if handlerName != self.SOURCE_ID:
+                    try:
+                        helpResponse = self._handlers[handlerName]['getDataHandler']({'source': handlerName, 'element': 'help', 'params': []}, userCredentials)
+                        if helpResponse is not None:
+                            handlerHelp = json.loads(helpResponse['node']['val'].decode('hex'))
+                            for sourceHelp in handlerHelp['sources']:
+                                help['sources'].append(sourceHelp)
+                    except:
+                        self._logger.exception('Failed to get help for ' + handlerName)
+            return {'node': {'val': json.dumps(help).encode('hex'), 'tp': 5}}
+        elif element == 'Sources':
+            return {'list': [{'node': {'val': handlerName, 'tp': 10}} for handlerName in self._handlers]}
+        elif element == 'not' and len(params) == 1:
+            return {'node': {'val': self._bool2string(params[0]['paramValue'] != 'true'), 'tp': 3}}
+        elif element == '==' and len(params) == 2:
+            return {'node': {'val': self._bool2string(params[0]['paramValue'] == params[1]['paramValue']), 'tp': 3}}
+        elif element == '!=' and len(params) == 2:
+            return {'node': {'val': self._bool2string(params[0]['paramValue'] != params[1]['paramValue']), 'tp': 3}}
+        elif element == '>' and len(params) == 2:
+            return {'node': {'val': self._bool2string(float(params[0]['paramValue']) > float(params[1]['paramValue'])), 'tp': 3}}
+        elif element == '>=' and len(params) == 2:
+            return {'node': {'val': self._bool2string(float(params[0]['paramValue']) >= float(params[1]['paramValue'])), 'tp': 3}}
+        elif element == '<' and len(params) == 2:
+            return {'node': {'val': self._bool2string(float(params[0]['paramValue']) < float(params[1]['paramValue'])), 'tp': 3}}
+        elif element == '<=' and len(params) == 2:
+            return {'node': {'val': self._bool2string(float(params[0]['paramValue']) <= float(params[1]['paramValue'])), 'tp': 3}}
+        elif element == 'and':
+            return {'node': {'val': self._bool2string(reduce(lambda x, y: x and y, [self._string2bool(param['paramValue']) for param in params], True)), 'tp': 3}}
+        elif element == 'or':
+            return {'node': {'val': self._bool2string(reduce(lambda x, y: x or y, [self._string2bool(param['paramValue']) for param in params], False)), 'tp': 3}}
+        elif element == 'match' and len(params) == 2:
+            return {'node': {'val': self._bool2string(fnmatch(params[0]['paramValue'], params[1]['paramValue'])), 'tp': 3}}
+        elif element == 'not match' and len(params) == 2:
+            return {'node': {'val': self._bool2string(not fnmatch(params[0]['paramValue'], params[1]['paramValue'])), 'tp': 3}}
+        elif element == 'sum' and len(params) == 1:
+            return {'node': {'val': str(sum([float(string) for string in json.loads(params[0]['paramValue'])])), 'tp': 2}}
+        elif element == 'exists' and len(params) == 1:
+            return {'node': {'val': self._bool2string(reduce(lambda x, y: x or y, [self._string2bool(string) for string in json.loads(params[0]['paramValue'])], False)), 'tp': 3}}
+        elif element == 'forAll' and len(params) == 1:
+            return {'node': {'val': self._bool2string(reduce(lambda x, y: x and y, [self._string2bool(string) for string in json.loads(params[0]['paramValue'])], True)), 'tp': 3}}
+        elif element == 'dataElementPresent':
+            origSource, origElement, origParams = self._getRequestPartsFromParams(params)
+            response = self._getDataHandler({'source': origSource, 'element': origElement, 'params': origParams}, userCredentials)
+            if response is not None:
+                return {'node': {'val': 'true', 'tp': 3}}
+            else:
+                return {'node': {'val': 'false', 'tp': 3}}
+        elif element == 'sizeOf':
+            origSource, origElement, origParams = self._getRequestPartsFromParams(params)
+            response = self._getDataHandler({'source': origSource, 'element': origElement, 'params': origParams}, userCredentials)
+            size = 1
+            if 'list' in response:
+                size = len(response['list'])
+            return {'node': {'val': str(size), 'tp': 1}}
+
+    def _getRequestPartsFromParams(self, params):
+        origSource = ''
+        origElement = ''
+        origParams = []
+        i = 0
+        while i < len(params):
+            param = params[i]
+            if param['paramName'] == 'Source':
+                origSource = param['paramValue']
+            elif param['paramName'] == 'Element':
+                origElement = param['paramValue']
+            elif param['paramName'] == 'ParamName':
+                i += 1
+                if i < len(params) and params[i]['paramName'] == 'ParamValue':
+                    origParams.append({'paramName': param['paramValue'], 'paramValue': params[i]['paramValue']})
+                else:
+                    return {'node': {'val': 'A ParamValue must always follow a ParamName parameter.', 'tp': 4}}
+            i += 1
+        return origSource, origElement, origParams
+
+
+    def _bool2string(self, bool):
+        if bool:
+            return 'true'
+        else:
+            return 'false'
+
+    def _string2bool(self, string):
+        return string == 'true'
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        response['body'] = json.dumps(self._dsRestAPI.parseRequest(json.loads(body), userCredentials))
+        response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self._getDataHandler,
+                'setDataHandler': self._setDataHandler
+            }
+        }
+
+    def close(self):
+        pass
\ No newline at end of file
diff --git a/src/Microservices/DataSource/Help.py b/src/Microservices/DataSource/Help.py
new file mode 100644
index 0000000..a3cae7a
--- /dev/null
+++ b/src/Microservices/DataSource/Help.py
@@ -0,0 +1,421 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+DSHELP = [
+    {
+        "dataElement": {
+            "description": "Help on all available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The available sources.",
+            "name": "Sources",
+            "valueType": "charstringlistType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Whether a dataelement returned by the request specified in the parameters is handled by a handler.",
+            "name": "dataElementPresent",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The source of the request.",
+                    "exampleValue": "DataSource",
+                    "name": "Source",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The element of the request.",
+                    "exampleValue": "Sources",
+                    "name": "Element",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "A parameter name hat must be followed by a ParamValue.",
+                    "exampleValue": "Param1",
+                    "name": "ParamName",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The parameter value.",
+                    "exampleValue": "Value_of_Param1",
+                    "name": "ParamValue",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The size of a list or 1 for single elements returned by the request specified in the parameters.",
+            "name": "sizeOf",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The source of the request.",
+                    "exampleValue": "DataSource",
+                    "name": "Source",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The element of the request.",
+                    "exampleValue": "Sources",
+                    "name": "Element",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "A parameter name hat must be followed by a ParamValue.",
+                    "exampleValue": "Param1",
+                    "name": "ParamName",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The parameter value.",
+                    "exampleValue": "Value_of_Param1",
+                    "name": "ParamValue",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether at least one of the elements of the parameter is true.",
+            "name": "exists",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "[\"true\", \"false\"]",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringlistType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether all the elements of the parameter are true.",
+            "name": "forAll",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "[\"true\", \"false\"]",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringlistType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Return the sum of the elements of the parameter.",
+            "name": "sum",
+            "valueType": "floatType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "[\"2.5\", \"1.3\"]",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringlistType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the two parameters are equal.",
+            "name": "==",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "some string",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "some other string",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the two parameters are not equal.",
+            "name": "!=",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "some string",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "some other string",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the first parameter is greater than the second.",
+            "name": ">",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "123",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "234.5",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the first parameter is equal to or greater than the second.",
+            "name": ">=",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "123",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "234.5",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the first parameter is lesser than the second.",
+            "name": "<",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "123",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "234.5",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Check whether the first parameter is equal to or lesser than the second.",
+            "name": "<=",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "123",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "234.5",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "floatType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Returns the logical and of the parameters (any number of parameters are supported).",
+            "name": "and",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "true",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "boolType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "false",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "boolType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Returns the logical or of the parameters (any number of parameters are supported).",
+            "name": "or",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "true",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "boolType"
+                    }
+                },
+                {
+                    "description": "The second parameter.",
+                    "exampleValue": "false",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "boolType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Returns the logical inverse of the parameter.",
+            "name": "not",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The first parameter.",
+                    "exampleValue": "true",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "boolType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Checks whther the pattern in the second parameter matches the string in the first parameter.",
+            "name": "match",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The string to search.",
+                    "exampleValue": "some string",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The pattern to match.",
+                    "exampleValue": "??m*str*",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Checks whther the pattern in the second parameter matches the string in the first parameter.",
+            "name": "not match",
+            "valueType": "boolType",
+            "parameters": [
+                {
+                    "description": "The string to search.",
+                    "exampleValue": "some string",
+                    "name": "Par1",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                },
+                {
+                    "description": "The pattern to match.",
+                    "exampleValue": "??m*str*",
+                    "name": "Par2",
+                    "typeDescriptor": {
+                        "valueType": "charstringType"
+                    }
+                }
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/DataSource/__init__.py b/src/Microservices/DataSource/__init__.py
new file mode 100644
index 0000000..29a561d
--- /dev/null
+++ b/src/Microservices/DataSource/__init__.py
@@ -0,0 +1,16 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+DataSource is a handler that can be used as a hub for other handlers that provide a DsRestAPI interface.
+It makes it possible to handle DsRestAPI requests that need no be handled be multiple microservices.
+'''
+
+from DataSource import DataSource
+
+EXTENSION = 'api.appagent'
+
+def createHandler(directory, *args):
+    return DataSource(directory)
\ No newline at end of file
diff --git a/src/Microservices/Playlist/Help.py b/src/Microservices/Playlist/Help.py
new file mode 100644
index 0000000..7f07b0b
--- /dev/null
+++ b/src/Microservices/Playlist/Help.py
@@ -0,0 +1,162 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+DSHELP = [
+    {
+        "dataElement": {
+            "description": "Help on the available elements.",
+            "name": "help",
+            "valueType": "octetstringType"
+        }
+    },
+    {
+        "dataElement": {
+            "description": "List of available playlists.",
+            "name": "Playlists",
+            "valueType": "charstringlistType",
+            "typeDescriptor": {
+                "isListOf": True,
+                "typeName": "Playlist"
+            }
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The status of the playlist.",
+            "name": "Status",
+            "valueType": "statusLEDType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Start the playlist.",
+            "name": "Start",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Stop a running playlist.",
+            "name": "Stop",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Pause a running playlist.",
+            "name": "Pause",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "Delete the playlist.",
+            "name": "Delete",
+            "valueType": "intType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The identifier playlist file.",
+            "name": "Edit",
+            "valueType": "charstringType",
+            "typeDescriptor": {
+                "isListOf": False,
+                "typeName": "Playlist"
+            },
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    {
+        "dataElement": {
+            "description": "The json descriptor of the playlist.",
+            "name": "Descriptor",
+            "valueType": "charstringType",
+            "parameters": [
+                {
+                    "description": "The name of the playlist.",
+                    "exampleValue": "group28/playlist",
+                    "name": "Playlist",
+                    "typeDescriptor": {
+                        "reference": {
+                            "typeName": "Playlist"
+                        }
+                    }
+                }
+            ]
+        }
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Playlist/Playlist.py b/src/Microservices/Playlist/Playlist.py
new file mode 100644
index 0000000..fb4560e
--- /dev/null
+++ b/src/Microservices/Playlist/Playlist.py
@@ -0,0 +1,145 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import os, json, logging
+from Common.DsRestAPI import DsRestAPI
+from ScheduledPlaylist import ScheduledPlaylist
+from Help import DSHELP
+
+class Playlist:
+
+    SOURCE_ID = 'Playlist'
+
+    def __init__(self, directory):
+        self._logger = logging.getLogger(__name__)
+        self._playlistDir = os.path.join(directory, 'Playlists')
+        self._dsRestAPI = DsRestAPI(self._getDataHandler, self._setDataHandler)
+        self._running = {}
+
+    def _getDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+
+        if element == 'help':
+            help = {'sources': [{'source': self.SOURCE_ID, 'dataElements': DSHELP}]}
+            return {'node': {'val': json.dumps(help).encode('hex'), 'tp': 5}}
+        elif element == 'Playlists':
+            list = []
+            for dir in os.listdir(self._playlistDir):
+                if dir in userCredentials['groups']:
+                    for file in os.listdir(os.path.join(self._playlistDir, dir)):
+                        if file.endswith('.json'):
+                            list.append({'node': {'val': os.path.join(dir, file)[:-5], 'tp': 10}})
+            list.sort(key = lambda element: element['node']['val'])
+            return {'list': list}
+        elif len(params) == 1 and params[0]['paramName'] == 'Playlist':
+            playlist = params[0]['paramValue']
+            if playlist.split('/')[0] in userCredentials['groups']:
+                if element == 'Status':
+                    if playlist in self._running:
+                        return self._running[params[0]['paramValue']].getStatus()
+                    else:
+                        return {'node': {'val': '[led:blue]Not running', 'tp': 11}}
+                elif element == 'Start':
+                    tp = 1
+                    if playlist in self._running and self._running[playlist].isRunning():
+                        tp = -1
+                    return {'node': {'val': '0', 'tp': tp}}
+                elif element == 'Stop':
+                    tp = -1
+                    if playlist in self._running and self._running[playlist].isRunning():
+                        tp = 1
+                    return {'node': {'val': '0', 'tp': tp}}
+                elif element == 'Pause':
+                    tp = -1
+                    if playlist in self._running and self._running[playlist].isRunning():
+                        tp = 1
+                    return {'node': {'val': '0', 'tp': tp}}
+                elif element == 'Edit':
+                    return {'node': {'val': playlist, 'tp': 4}}
+                elif element == 'Delete':
+                    return {'node': {'val': '0', 'tp': 1}}
+                elif element == 'Descriptor':
+                    with open(os.path.join(self._playlistDir, playlist) + '.json', 'r') as f:
+                        content = f.read()
+                    return {'node': {'val': content, 'tp': 4}}
+            else:
+                return {'node': {'val': '', 'tp': 0}}
+
+    def _setDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+        content = request['content']
+
+        if len(params) == 1 and params[0]['paramName'] == 'Playlist':
+            playlist = params[0]['paramValue']
+            group = playlist.split('/')[0]
+            if group in userCredentials['groups']:
+                if element == 'Start':
+                    return self._start(playlist, userCredentials)
+                elif element == 'Stop':
+                    return self._stop(playlist)
+                elif element == 'Pause':
+                    return self._pause(playlist)
+                elif element == 'Descriptor':
+                    groupDir = os.path.join(self._playlistDir, group)
+                    if not os.path.exists(groupDir):
+                        os.makedirs(groupDir)
+                    with open(os.path.join(self._playlistDir, playlist) + '.json', 'w') as f:
+                        f.write(content)
+                    return {'node': {'val': 'saved', 'tp': 4}}
+                elif element == 'Delete':
+                    fileName = os.path.join(self._playlistDir, playlist) + '.json'
+                    os.unlink(fileName)
+                    return {'node': {'val': '0', 'tp': 1}}
+            else:
+                return {'node': {'val': '', 'tp': 0}}
+
+    def _createPlaylistExecutor(self, descriptor):
+        return ScheduledPlaylist(descriptor)
+
+    def _start(self, playlist, userCredentials):
+        if playlist in self._running and self._running[playlist].isRunning():
+            return {'node': {'val': '0', 'tp': -1}}
+        try:
+            descriptor = []
+            with open(os.path.join(self._playlistDir, playlist) + '.json', 'r') as f:
+                descriptor = json.load(f)
+            scheduledPlaylist = self._createPlaylistExecutor(descriptor)
+            scheduledPlaylist.start(userCredentials)
+            self._running[playlist] = scheduledPlaylist
+        except:
+            self._logger.exception('Failed to start playlist')
+        return {'node': {'val': '0', 'tp': 1}}
+
+    def _stop(self, playlist):
+        if playlist in self._running and self._running[playlist].isRunning():
+            self._running[playlist].stop()
+            return {'node': {'val': '0', 'tp': 1}}
+        else:
+            return {'node': {'val': '0', 'tp': -1}}
+
+    def _pause(self, playlist):
+        if playlist in self._running and self._running[playlist].isRunning():
+            self._running[playlist].pause()
+            return {'node': {'val': '0', 'tp': 1}}
+        else:
+            return {'node': {'val': '0', 'tp': -1}}
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        response['body'] = json.dumps(self._dsRestAPI.parseRequest(json.loads(body), userCredentials))
+        response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self._getDataHandler,
+                'setDataHandler': self._setDataHandler
+            }
+        }
+
+    def close(self):
+        for playlist in self._running:
+            self._stop(playlist)
\ No newline at end of file
diff --git a/src/Microservices/Playlist/Playlists/admin/Start.json b/src/Microservices/Playlist/Playlists/admin/Start.json
new file mode 100644
index 0000000..c42e2d0
--- /dev/null
+++ b/src/Microservices/Playlist/Playlists/admin/Start.json
@@ -0,0 +1,303 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "relativeTo": [],
+                "id": "0",
+                "startTime": 1,
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "TSController",
+                            "element": "StartProcess",
+                            "content": "0",
+                            "tp": 1,
+                            "params": [
+                                {
+                                    "paramName": "TSDaemon",
+                                    "paramValue": "0"
+                                },
+                                {
+                                    "paramName": "TSConfigProvider",
+                                    "paramValue": "FileList"
+                                },
+                                {
+                                    "paramName": "TSConfig",
+                                    "paramValue": "admin/release_evolution"
+                                }
+                            ]
+                        }
+                    }
+                ],
+                "desktopData": {
+                    "top": 37,
+                    "left": 256,
+                    "visible": true,
+                    "openNodes": [
+                        [
+                            3
+                        ]
+                    ]
+                },
+                "condition": {}
+            },
+            {
+                "id": "3",
+                "relativeTo": [
+                    "0"
+                ],
+                "startTime": 5,
+                "requests": [
+                    {
+                        "getData": {
+                            "source": "TSController",
+                            "element": "TSProcesses",
+                            "filter": {
+                                "dataValue": "true"
+                            },
+                            "children": [
+                                {
+                                    "setData": {
+                                        "source": "TSController",
+                                        "element": "Kill",
+                                        "tp": 1,
+                                        "content": "0",
+                                        "params": [
+                                            {
+                                                "paramName": "TSProcess",
+                                                "paramValue": "%Parent0%"
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                ],
+                "condition": {
+                    "evaluatingPeriod": 1,
+                    "numberOfExecutions": 200,
+                    "cancelingTimeout": 1000,
+                    "expression": {
+                        "request": {
+                            "source": "DataSource",
+                            "element": "and",
+                            "params": [
+                                {
+                                    "paramName": "Par1",
+                                    "paramValue": {
+                                        "request": {
+                                            "source": "DataSource",
+                                            "element": "forAll",
+                                            "params": [
+                                                {
+                                                    "paramName": "Par1",
+                                                    "paramValue": {
+                                                        "request": {
+                                                            "source": "TSController",
+                                                            "element": "TSProcesses",
+                                                            "remapTo": {
+                                                                "request": {
+                                                                    "source": "DataSource",
+                                                                    "element": "match",
+                                                                    "params": [
+                                                                        {
+                                                                            "paramName": "Par1",
+                                                                            "paramValue": {
+                                                                                "request": {
+                                                                                    "source": "TSController",
+                                                                                    "element": "ReadyToRun",
+                                                                                    "params": [
+                                                                                        {
+                                                                                            "paramName": "TSProcess",
+                                                                                            "paramValue": {
+                                                                                                "dataValue": "%FilterParent2%"
+                                                                                            }
+                                                                                        }
+                                                                                    ]
+                                                                                }
+                                                                            }
+                                                                        },
+                                                                        {
+                                                                            "paramName": "Par1",
+                                                                            "paramValue": {
+                                                                                "dataValue": "*green*"
+                                                                            }
+                                                                        }
+                                                                    ]
+                                                                }
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            ]
+                                        }
+                                    }
+                                },
+                                {
+                                    "paramName": "Par2",
+                                    "paramValue": {
+                                        "request": {
+                                            "source": "DataSource",
+                                            "element": "==",
+                                            "params": [
+                                                {
+                                                    "paramName": "Par1",
+                                                    "paramValue": {
+                                                        "request": {
+                                                            "source": "DataSource",
+                                                            "element": "sizeOf",
+                                                            "params": [
+                                                                {
+                                                                    "paramName": "Source",
+                                                                    "paramValue": {
+                                                                        "dataValue": "TSController"
+                                                                    }
+                                                                },
+                                                                {
+                                                                    "paramName": "Element",
+                                                                    "paramValue": {
+                                                                        "dataValue": "TSProcesses"
+                                                                    }
+                                                                }
+                                                            ]
+                                                        }
+                                                    }
+                                                },
+                                                {
+                                                    "paramName": "Par2",
+                                                    "paramValue": {
+                                                        "dataValue": "1"
+                                                    }
+                                                }
+                                            ]
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                },
+                "desktopData": {
+                    "top": 34,
+                    "left": 567,
+                    "visible": true,
+                    "openNodes": [
+                        [
+                            3
+                        ],
+                        [
+                            3,
+                            0
+                        ],
+                        [
+                            4
+                        ],
+                        [
+                            4,
+                            3
+                        ],
+                        [
+                            4,
+                            3,
+                            0
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            0
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            0,
+                            0
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            0,
+                            0,
+                            0
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            1
+                        ],
+                        [
+                            4,
+                            3,
+                            0,
+                            1,
+                            0
+                        ]
+                    ]
+                }
+            },
+            {
+                "id": "4",
+                "relativeTo": [
+                    "3"
+                ],
+                "startTime": 10,
+                "requests": [
+                    {
+                        "getData": {
+                            "source": "TSController",
+                            "element": "TSProcesses",
+                            "children": [
+                                {
+                                    "setData": {
+                                        "source": "TSController",
+                                        "element": "Delete",
+                                        "tp": 1,
+                                        "content": "0",
+                                        "params": [
+                                            {
+                                                "paramName": "TSProcess",
+                                                "paramValue": "%Parent0%"
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                ],
+                "desktopData": {
+                    "top": 174,
+                    "left": 971,
+                    "visible": true,
+                    "openNodes": [
+                        [
+                            3
+                        ],
+                        [
+                            3,
+                            0
+                        ],
+                        [
+                            4
+                        ]
+                    ]
+                },
+                "condition": {}
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Playlist/Playlists/admin/TS_Commands.json b/src/Microservices/Playlist/Playlists/admin/TS_Commands.json
new file mode 100644
index 0000000..1b470ae
--- /dev/null
+++ b/src/Microservices/Playlist/Playlists/admin/TS_Commands.json
@@ -0,0 +1,37 @@
+[
+    {
+        "apiUrl": "http://localhost:4001/api.dsapi",
+        "playlist": [
+            {
+                "relativeTo": [],
+                "id": "0",
+                "startTime": 3,
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "ExecCtrl",
+                            "element": "Start",
+                            "content": "1",
+                            "tp": 1,
+                            "params": []
+                        }
+                    }
+                ],
+                "desktopData": {
+                    "top": 56,
+                    "left": 271,
+                    "visible": true,
+                    "openNodes": [
+                        [
+                            3
+                        ],
+                        [
+                            4
+                        ]
+                    ]
+                },
+                "condition": {}
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/src/Microservices/Playlist/ScheduledPlaylist.py b/src/Microservices/Playlist/ScheduledPlaylist.py
new file mode 100644
index 0000000..0c0ab90
--- /dev/null
+++ b/src/Microservices/Playlist/ScheduledPlaylist.py
@@ -0,0 +1,185 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import sys, json, threading, urllib2, time, logging, re, cookielib, ssl
+
+class ScheduledPlaylist:
+
+    def __init__(self, descriptor, customCallbackForResponse = None):
+        self._logger = logging.getLogger(__name__)
+        self._status = {'node': {'val': '', 'tp': 11}}
+        self._finished = 0
+        self._descriptor = descriptor
+        self._callback = customCallbackForResponse
+        self._stopEvent = threading.Event()
+        self._continueEvent = threading.Event()
+        self._continueEvent.set()
+        self._urlOpeners = []
+        self._threads = []
+
+    def _waitOrExit(self):
+        if self._stopEvent.isSet():
+            return True
+        self._continueEvent.wait()
+        if self._stopEvent.isSet():
+            return True
+        else:
+            return False
+
+    def _wait(self, amount):
+        time.sleep(amount)
+
+    def _checkCondition(self, url, opener, expression):
+        requests = [
+            {
+                'getData': {
+                    'source': 'DataSource',
+                    'element': 'Sources',
+                    'filter': expression
+                }
+            }
+        ]
+        response = self._sendRequestsToUrl(opener, url, requests)
+        if 'node' in response['contentList'][0] and response['contentList'][0]['node']['tp'] == 0:
+            return False
+        else:
+            return True
+
+    def _sendRequestsToUrl(self, opener, url, requests):
+        return json.loads(opener.open(urllib2.Request(url, json.dumps({'requests': requests}))).read())
+
+    def _sendRequests(self, url, opener, requests):
+        if len(requests) > 0:
+            if 'getData' in requests[0]:
+                self._status['node']['val'] = '[led:green]Running - Last action: ' + requests[0]['getData']['element']
+            elif 'setData' in requests[0]:
+                self._status['node']['val'] = '[led:green]Running - Last action: ' + requests[0]['setData']['element']
+        return self._sendRequestsToUrl(opener, url, requests)
+
+    def _processPlaylist(self, url, opener, action):
+        try:
+            if 'condition' in action and 'expression' in action['condition']:
+                tries = 0
+                startTime = time.time()
+                while True:
+                    if self._waitOrExit():
+                        return
+                    self._continueEvent.wait()
+                    tries += 1
+                    if self._checkCondition(url, opener, action['condition']['expression']):
+                        break
+                    elif ('cancelingTimeout' in action['condition'] and time.time() - startTime > action['condition']['cancelingTimeout']) or ('numberOfExecutions' in action['condition'] and tries > action['condition']['numberOfExecutions']):
+                        self.stop()
+                        self._status['node']['val'] = '[led:red]Timeout'
+                        return
+                    if 'evaluatingPeriod' in action['condition']:
+                        self._wait(action['condition']['evaluatingPeriod'])
+            if 'startTime' in action:
+                self._wait(action['startTime'])
+            if self._waitOrExit():
+                return
+            response = self._sendRequests(url, opener, action['requests'])
+            if self._waitOrExit():
+                return
+            self._playlistFinished(action['id'])
+            if self._callback is not None:
+                self._callback(response)
+
+        except:
+            self._logger.exception('Error during playlist execution')
+            self.stop()
+            self._status['node']['val'] = '[led:red]Error'
+
+    def _playlistFinished(self, id):
+        self._finished += 1
+        if self._finished == len(self._threads):
+            self._status['node']['val'] = self._status['node']['val'].replace('[led:green]Running', '[led:blue]Finished')
+            self._logout()
+        if id is not None:
+            for thread in self._threads:
+                if 'relativeTo' in thread and id in thread['relativeTo']:
+                    thread['relativeTo'].remove(id)
+                    if len(thread['relativeTo']) == 0:
+                        thread['thread'].start()
+
+    def _getBaseUrl(self, url):
+        match = re.search(r'\w/', url)
+        if match:
+            return url[:match.start() + 1]
+        else:
+            return url
+
+    def _logout(self):
+        for url, opener in self._urlOpeners:
+            opener.open(urllib2.Request(self._getBaseUrl(url) + '/logout/api.authenticate'))
+
+    def _login(self, opener, url, userCredentials):
+        try:
+            opener.open(urllib2.Request(self._getBaseUrl(url) + '/login/api.authenticate', '{"username": "' + userCredentials['username'] + '", "password": "' + userCredentials['password'] + '"}'))
+            self._urlOpeners.append((url, opener))
+        except:
+            pass
+
+    def start(self, userCredentials):
+        for playlistObject in self._descriptor:
+            url = playlistObject['apiUrl']
+            try:
+                opener = urllib2.build_opener(urllib2.HTTPHandler(), urllib2.HTTPSHandler(context = ssl._create_unverified_context()), urllib2.HTTPCookieProcessor(cookielib.CookieJar()), urllib2.ProxyHandler({}))
+            except:
+                opener = urllib2.build_opener(urllib2.HTTPHandler(), urllib2.HTTPSHandler(), urllib2.HTTPCookieProcessor(cookielib.CookieJar()), urllib2.ProxyHandler({}))
+            if userCredentials['username'] is not None and userCredentials['password'] is not None:
+                self._login(opener, url, userCredentials)
+            playlist = playlistObject['playlist']
+            for action in playlist:
+                if 'id' not in action:
+                    action['id'] = None
+                requests = action['requests']
+                thread = threading.Thread(target = self._processPlaylist, args = (url, opener, action))
+                thread.daemon = True
+                self._threads.append({'thread': thread})
+                if 'relativeTo' not in action or len(action['relativeTo']) == 0:
+                    thread.start()
+                else:
+                    self._threads[-1]['relativeTo'] = action['relativeTo']
+        if len(self._threads) == 0:
+            self._status['node']['val'] = '[led:blue]Finished'
+        else:
+            self._status['node']['val'] = '[led:green]Running'
+
+    def stop(self):
+        self._stopEvent.set()
+        self._continueEvent.set()
+        self._status['node']['val'] = '[led:black]Stopped'
+        self._logout()
+
+    def pause(self):
+        if self._continueEvent.isSet():
+            self._continueEvent.clear()
+            self._status['node']['val'] = self._status['node']['val'].replace('[led:green]Running', '[led:yellow]Paused')
+        else:
+            self._continueEvent.set()
+            self._status['node']['val'] = self._status['node']['val'].replace('[led:yellow]Paused', '[led:green]Running')
+
+    def isRunning(self):
+        for thread in self._threads:
+            if thread['thread'].isAlive():
+                return True
+        return False
+
+    def getStatus(self):
+        return self._status
+
+if __name__ == '__main__':
+    def printResponse(response):
+        print json.dumps(response)
+
+    playlistDescriptor = []
+    with open(sys.argv[1], 'r') as f:
+        playlistDescriptor = json.load(f)
+    scheduledPlaylist = ScheduledPlaylist(playlistDescriptor, printResponse)
+    scheduledPlaylist.start({'username': 'admin', 'password': 'admin', 'groups': set(['admin'])})
+    time.sleep(3)
+    while scheduledPlaylist.isRunning():
+        time.sleep(1)
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Main.js b/src/Microservices/Playlist/WebApplication/Main.js
new file mode 100644
index 0000000..188c18f
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Main.js
@@ -0,0 +1,104 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+var WebApplications = WebApplications || [];
+
+WebApplications.push({'application': new Playlist_Application()});
+
+function Playlist_Application() {
+    "use strict";
+    
+    var v_params;
+    var v_appBase = new WebAppBase();
+    var v_webAppModel;
+
+    var v_model;
+    var v_viewmodel;
+    var v_view;
+    var v_binder;
+
+    this.info = function() {
+        return {
+            defaultIcon: "WebApplications/GuiEditor/Res/main_icon.png",
+            defaultName: "Playlist Editor"
+        };
+    };
+
+    this.load = function(p_webAppModel, p_params, p_framework)  {
+        v_params = p_params;
+        v_webAppModel = p_webAppModel;
+
+        new MultipleDirectoryListTask(
+            [
+                "WebApplications/Playlist/Models",
+                "WebApplications/Playlist/Views",
+                "WebApplications/Playlist/ViewModels"
+            ],
+            v_webAppModel.getFileHandler()
+        ).taskOperation(function(ok, resources) {
+            var jsfiles = resources.jsfiles;
+            jsfiles.push("Utils/DsRestAPI/DsRestAPI.js");
+            jsfiles.push("Utils/DsRestAPI/DsRestAPIComm.js");
+            jsfiles.push("WebApplications/GuiEditor/Views/View_Connections.js");
+            v_appBase.load(jsfiles, [], start, v_webAppModel.getFileHandler());
+        });
+    };
+
+    this.unload = function(webappUnloaded) {
+        v_appBase.unload(destroy);
+        webappUnloaded(true);
+    };
+
+    function destroy() {
+        v_view.destroy();
+
+        v_model = undefined;
+        v_view = undefined;
+        v_viewmodel = undefined;
+        v_binder = undefined;
+    }
+
+    function start(p_callback) {
+        v_model = new PlaylistEditor_Model(v_webAppModel);
+        v_viewmodel = new PlaylistEditor_ViewModel(v_model);
+        v_view = new PlaylistEditor_View(v_viewmodel, "TSGuiFrameworkMain", "PlaylistEditor_MainView");
+        v_binder = new PlaylistEditor_Binder(v_viewmodel, v_view);
+        v_viewmodel.setBinder(v_binder);
+
+        function callback(ok, data) {
+            if (ok) {
+                v_view.applicationCreated();
+                v_binder.fullRefresh();
+            } else {
+                alert(data);
+            }
+            if (p_callback != undefined) {
+                p_callback();
+            }
+        }
+
+        function playlistLoaded() {
+            new SyncTaskList([new GenericTask(v_viewmodel.init), new GenericTask(v_view.init)], callback).taskOperation();
+        }
+
+        if (v_params.playlist != undefined) {
+            v_model.loadPlaylist(v_params.playlist, playlistLoaded);
+        } else {
+            v_model.newPlaylist();
+            playlistLoaded();
+        }
+    }
+}
+
+function PlaylistEditor_Binder(p_viewModel, p_view) {
+    "use strict";
+    var v_viewmodel = p_viewModel;
+    var v_view = p_view;
+
+    this.fullRefresh = function() {
+        v_view.fullRefresh();
+    };
+}
+//# sourceURL=PlaylistEditor\Main.js
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Models/Model.js b/src/Microservices/Playlist/WebApplication/Models/Model.js
new file mode 100644
index 0000000..63e7fdf
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Models/Model.js
@@ -0,0 +1,332 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_Model(p_webAppModel) {
+    "use strict";
+
+    var v_baseModel = p_webAppModel;
+    var v_dsRestAPI = new DsRestAPI("appagent");
+
+    var v_currentName;
+    var v_playlist = {};
+
+    var v_this = this;
+
+    ///////////////////// PLAYLIST HANDLING /////////////////////
+
+    this.newPlaylist = function() {
+        v_playlist = {};
+        v_currentName = undefined
+        return v_playlist;
+    };
+
+    this.deletePlaylist = function(name, callback) {
+        function playlistDeleted(response) {
+            if (response != undefined && response.node != undefined && response.node.tp == 1) {
+                callback(true);
+            } else {
+                callback(false);
+            }
+        }
+
+        v_dsRestAPI.setData(playlistDeleted, "Playlist", "Delete", "0", 1, [{
+            "paramName": "Playlist",
+            "paramValue": name
+        }]);
+
+        if (name == v_currentName) {
+            v_currentName = undefined;
+        }
+    };
+
+    this.loadPlaylist = function(name, callback) {
+        function playlistLoaded(response) {
+            if (response != undefined && response.node != undefined && response.node.tp == 4) {
+                try {
+                    convertToVisualizablePlaylist(JSON.parse(response.node.val));
+                    v_currentName = name;
+                    callback(true);
+                } catch(e) {
+                    callback(false);
+                }
+            } else {
+                callback(false);
+            }
+        }
+
+        v_dsRestAPI.getData(playlistLoaded, "Playlist", "Descriptor", [{
+            "paramName": "Playlist",
+            "paramValue": name
+        }]);
+    };
+
+    this.savePlaylist = function(callback) {
+        function playlistSaved(response) {
+            if (response != undefined && response.node != undefined && response.node.tp == 4) {
+                callback(true);
+            } else {
+                callback(false);
+            }
+        }
+
+        v_dsRestAPI.setData(playlistSaved, "Playlist", "Descriptor", JSON.stringify(convertToUsablePlaylist(), null, 4), 4, [{
+            "paramName": "Playlist",
+            "paramValue": v_currentName
+        }]);
+    };
+
+    this.savePlaylistAs = function(name, callback) {
+        var oldName = v_currentName;
+        v_currentName = name;
+
+        function playlistSaved(ok) {
+            if (!ok) {
+                v_currentName = oldName;
+            }
+            callback(ok);
+        }
+
+        v_this.savePlaylist(playlistSaved);
+    };
+
+    this.currentlyEdited = function() {
+        return v_currentName;
+    };
+
+    this.listPlaylists = function(callback) {
+        function playlistsListed(response) {
+            var list = [];
+            if (response != undefined && response.list != undefined) {
+                for (var i = 0; i < response.list.length; ++i) {
+                    list.push(response.list[i].node.val);
+                }
+                callback(true, list);
+            } else {
+                callback(false);
+            }
+        }
+
+        v_dsRestAPI.getData(playlistsListed, "Playlist", "Playlists");
+    };
+
+    this.playlistExists = function(name, callback) {
+        function playlistsListed(ok, list) {
+            callback(ok && list.indexOf(name) != -1);
+        }
+
+        v_this.listPlaylists(playlistsListed);
+    };
+
+    this.listGroups = function(callback) {
+        function groupsListed(response) {
+            var list = [];
+            if (response != undefined && response.list != undefined) {
+                for (var i = 0; i < response.list.length; ++i) {
+                    list.push(response.list[i].node.val);
+                }
+                callback(true, list);
+            } else {
+                callback(false);
+            }
+        }
+
+        v_dsRestAPI.getData(groupsListed, "Authentication", "ListGroupsOfUser", [{
+            "paramName": "User",
+            "paramValue": localStorage.username
+        }]);
+    };
+
+    function convertToVisualizablePlaylist(playlist) {
+        v_playlist = {};
+        for (var i = 0; i < playlist.length; ++i) {
+            var api = playlist[i].apiUrl;
+            for (var j = 0; j < playlist[i].playlist.length; ++j) {
+                var nextPlaylist = playlist[i].playlist[j];
+                nextPlaylist.api = api;
+                var filter = undefined;
+                if (nextPlaylist.condition != undefined) {
+                    filter = nextPlaylist.condition.expression;
+                } else {
+                    nextPlaylist.condition = {}
+                }
+                nextPlaylist.condition.expression = [{
+                    "getData": {
+                        "source": "DataSource",
+                        "element": "Sources",
+                        "filter": filter
+                    }
+                }]
+                if (nextPlaylist.desktopData == undefined) {
+                    nextPlaylist.desktopData = {
+                        "top": i * 50,
+                        "left": j * 50,
+                        "visible": true
+                    }
+                }
+                v_playlist[nextPlaylist.id] = nextPlaylist;
+            }
+        }
+    }
+
+    function convertToUsablePlaylist() {
+        var playlist = [];
+        var apis = {};
+        var insertedIds = {};
+        fillPlaylistByApi(playlist, apis);
+        var notAllInserted = true;
+        var inserted = true;
+        if (Object.keys(v_playlist).length > 0) {
+            while (notAllInserted && inserted) {
+                notAllInserted = false;
+                inserted = false;
+                for (var id in v_playlist) {
+                    if (v_playlist[id].api != undefined && !insertedIds[id]) {
+                        if (numberOfUnfulfilledReferences(v_playlist[id], insertedIds) == 0) {
+                            var toInsert = mcopy(v_playlist[id]);
+                            toInsert.api = undefined;
+                            toInsert.condition.expression = toInsert.condition.expression[0].getData.filter;
+                            playlist[apis[v_playlist[id].api]].playlist.push(toInsert);
+                            insertedIds[id] = true;
+                            inserted = true;
+                        } else {
+                            notAllInserted = true
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!inserted) {
+            alert("Circular dependency found, using only the correct part.");
+        }
+
+        return playlist;
+    }
+
+    function fillPlaylistByApi(playlist, apis) {
+        var missingApi = false;
+        for (var id in v_playlist) {
+            var api = v_playlist[id].api;
+            if (api != undefined) {
+                if (apis[api] == undefined) {
+                    apis[api] = playlist.length;
+                    playlist.push({
+                        "apiUrl": api,
+                        "playlist": []
+                    });
+                }
+            } else {
+                missingApi = true;
+            }
+        }
+
+        if (missingApi) {
+            alert("Some elements are missing an api, they will not be used.")
+        }
+    }
+
+    function numberOfUnfulfilledReferences(element, insertedIds) {
+        var count = 0;
+        if (element.relativeTo != undefined) {
+            for (var i = 0; i < element.relativeTo.length; ++i) {
+                if (!insertedIds[element.relativeTo[i]]) {
+                    ++count;
+                }
+            }
+        }
+        return count
+    }
+
+    ///////////////////// EDITOR MODEL HANDLING /////////////////////
+
+    function getUnusedId() {
+        var i = 0;
+        while (v_playlist["" + i] != undefined) {
+            i += 1
+        }
+        return "" + i
+    }
+
+    this.getEditorModels = function() {
+        return v_playlist;
+    };
+
+    this.createEditorModel = function() {
+        var id = getUnusedId();
+        var playlist = {
+            "id": id,
+            "relativeTo": [],
+            "startTime": 0,
+            "api": window.location.protocol + "//" + window.location.host + "/api.appagent",
+            "requests": [],
+            "condition": {
+                "expression": [{
+                    "getData": {
+                        "source": "DataSource",
+                        "element": "Sources"
+                    }
+                }]
+            },
+            "desktopData": {
+                "top": 0,
+                "left": 300,
+                "visible": true,
+                "openNodes": []
+            }
+        };
+
+        v_playlist[id] = playlist;
+
+        return {
+            "id": id,
+            "model": playlist
+        };
+    };
+
+    this.deleteEditorModel = function(id) {
+        delete v_playlist[id];
+        for (var playlistId in v_playlist) {
+            var index = v_playlist[playlistId].relativeTo.indexOf(id);
+            if (index != -1) {
+                v_playlist[playlistId].relativeTo.splice(index, 1);
+            }
+        }
+    };
+
+    this.playlistConnection = function(p_from, p_to) {
+        var index = v_playlist[p_to].relativeTo.indexOf(p_from);
+        if (index == -1) {
+            v_playlist[p_to].relativeTo.push(p_from);
+        } else {
+            v_playlist[p_to].relativeTo.splice(index, 1);
+        }
+    };
+
+    ///////////////////// CONFIG HANDLING /////////////////////
+
+    this.getAppConfig = v_baseModel.getAppConfig;
+
+    ///////////////////// USEFUL FUNCTIONS FOR VIEWMODELS /////////////////////
+
+    function compareOptions(option1, option2) {
+        if (option1.text < option2.text) {
+            return -1;
+        } else if (option1.text > option2.text) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    this.sortOptions = function(options) {
+        options.sort(compareOptions);
+    };
+
+    this.getDsRestAPI = function() {
+        return v_dsRestAPI;
+    };
+
+    this.getFileHandler = v_baseModel.getFileHandler;
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel.js b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel.js
new file mode 100644
index 0000000..43b04ff
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel.js
@@ -0,0 +1,105 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_ViewModel(p_model) {
+
+    var v_model = p_model;
+    var v_dsRestAPI = v_model.getDsRestAPI();
+    var v_fileHandler = v_model.getFileHandler();
+    var v_binder;
+
+    var v_viewmodels = {};
+    var v_help;
+
+    this.elementEditorViewmodel = new PlaylistEditor_ElementEditor_ViewModel();
+    this.filterElementEditorViewmodel = new PlaylistEditor_FilterElementEditor_ViewModel();
+
+    this.init = function(p_callback) {
+        function helpArrived(ok, help) {
+            v_help = new HelpTreeBuilder(help).getHelpTree();
+            p_callback(true);
+        }
+
+        v_model.getDsRestAPI().getHelp(helpArrived);
+    };
+
+    this.setBinder = function(binder) {
+        v_binder = binder;
+    };
+
+    this.newPlaylist = v_model.newPlaylist;
+    this.deletePlaylist = v_model.deletePlaylist;
+    this.loadPlaylist = v_model.loadPlaylist;
+    this.savePlaylist = v_model.savePlaylist;
+
+    this.savePlaylistAs = function(group, name, callback) {
+        v_model.savePlaylistAs(group + '/' + name, callback);
+    };
+
+    this.currentlyEdited = v_model.currentlyEdited;
+
+    this.listPlaylists = function(callback) {
+        v_model.listPlaylists(function(ok, list) {
+            callback(createOptionsFromList(list))
+        });
+    }
+
+    this.listGroups = function(callback) {
+        v_model.listGroups(function(ok, list) {
+            callback(createOptionsFromList(list))
+        });
+    }
+
+    this.playlistExists = function(group, name, callback) {
+        v_model.playlistExists(group + '/' + name, callback);
+    };
+
+    this.getEditorViewmodels = function() {
+        v_viewmodels = {};
+        var models = v_model.getEditorModels();
+        for (var id in models) {
+            v_viewmodels[id] = new PlaylistEditor_PlaylistItem_ViewModel(models[id], v_help);
+        }
+        return v_viewmodels;
+    };
+
+    this.createEditorViewmodel = function() {
+        var obj = v_model.createEditorModel();
+        v_viewmodels[obj.id] = new PlaylistEditor_PlaylistItem_ViewModel(obj.model, v_help);
+        return {
+            "id": obj.id,
+            "viewmodel": v_viewmodels[obj.id]
+        };
+    };
+
+    this.deleteEditorViewmodel = function(id) {
+        v_model.deleteEditorModel(id);
+        delete v_viewmodels[id];
+    };
+
+    this.playlistConnection = v_model.playlistConnection;
+
+    this.getHelp = function() {
+        return v_help;
+    };
+
+    this.loadFile = v_model.getFileHandler().loadFile;
+
+    function createOptionsFromList(list) {
+        if (list == undefined) {
+            list = [];
+        }
+
+        var options = [];
+        for (var i = 0; i < list.length; ++i) {
+            options.push({
+                "text": list[i],
+                "value": list[i]
+            });
+        }
+
+        return options;
+    }
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_ElementEditor.js b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_ElementEditor.js
new file mode 100644
index 0000000..7ecea9f
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_ElementEditor.js
@@ -0,0 +1,149 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_ElementEditor_ViewModel() {
+    "use strict";
+
+    var SETDATA_SCHEMA = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "type": "object",
+        "title": "SetData request",
+        "properties": {
+            "source": {
+                "type": "string"
+            },
+            "ptcname": {
+                "type": "string"
+            },
+            "element": {
+                "type": "string"
+            },
+            "content": {
+                "type": "string"
+            },
+            "tp": {
+                "type": "integer"
+            },
+            "indxsInList": {
+                "type": "array",
+                "items": {
+                    "title": "index",
+                    "type": "integer"
+                },
+                "format": "table"
+            },
+            "params": {
+                "type": "array",
+                "items": {
+                    "title": "param",
+                    "type" : "object",
+                    "additionalProperties": false,
+                    "properties": {
+                        "paramName": {
+                            "type": "string"
+                        },
+                        "paramValue": {
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "paramName",
+                        "paramValue"
+                    ]
+                },
+                "format": "table"
+            }
+        },
+        "required": [
+            "source",
+            "element",
+            "content",
+            "tp"
+        ]
+    };
+
+    var GETDATA_SCHEMA = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "type": "object",
+        "title": "GetData request",
+        "properties": {
+            "source": {
+                "type": "string"
+            },
+            "ptcname": {
+                "type": "string"
+            },
+            "element": {
+                "type": "string"
+            },
+            "params": {
+                "type": "array",
+                "items": {
+                    "title": "param",
+                    "type" : "object",
+                    "additionalProperties": false,
+                    "properties": {
+                        "paramName": {
+                            "type": "string"
+                        },
+                        "paramValue": {
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "paramName",
+                        "paramValue"
+                    ]
+                },
+                "format": "table"
+            }
+        },
+        "required": [
+            "source",
+            "element"
+        ]
+    };
+
+    var v_request;
+
+    this.setRequest = function(p_request) {
+        v_request = p_request;
+    };
+
+    this.getJSONData = function(callback) {
+        var request;
+        if (v_request.getData != undefined) {
+            request = mcopy(v_request.getData);
+        } else {
+            request = mcopy(v_request.setData);
+        }
+        callback(request);
+    };
+
+    this.setJSONData = function(json) {
+        if (v_request.getData != undefined) {
+            v_request.getData.source = json.source;
+            v_request.getData.element = json.element;
+            v_request.getData.ptcname = json.ptcname;
+            v_request.getData.params = json.params;
+        } else {
+            v_request.setData.source = json.source;
+            v_request.setData.element = json.element;
+            v_request.setData.ptcname = json.ptcname;
+            v_request.setData.params = json.params;
+            v_request.setData.content = json.content;
+            v_request.setData.tp = json.tp;
+            v_request.setData.indxsInList = json.indxsInList;
+        }
+    };
+
+    this.getSchema = function() {
+        if (v_request.getData != undefined) {
+            return GETDATA_SCHEMA;
+        } else {
+            return SETDATA_SCHEMA;
+        }
+    };
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_FilterElementEditor.js b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_FilterElementEditor.js
new file mode 100644
index 0000000..809d137
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_FilterElementEditor.js
@@ -0,0 +1,93 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_FilterElementEditor_ViewModel() {
+    "use strict";
+
+    var FILTER_SCHEMA = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "title": "Edit filter",
+        "type": "object",
+        "oneOf": [
+            {
+                "title": "data value",
+                "type": "object",
+                "properties": {
+                    "dataValue": {
+                        "description": "A value",
+                        "type": "string",
+                        "default": "true"
+                    }
+                },
+                "required": ["dataValue"],
+                "additionalProperties": false
+            },
+            {
+                "title": "request",
+                "type": "object",
+                "properties": {
+                    "request": {
+                        "description": "A request that will be converted to a value during the filter evaluation",
+                        "type": "object",
+                        "properties": {
+                            "source": {
+                                "description": "The source",
+                                "type": "string"
+                            },
+                            "element": {
+                                "description": "The element",
+                                "type": "string"
+                            },
+                            "ptcname": {
+                                "description": "The ptc name",
+                                "type": "string"
+                            }
+                        },
+                        "required": ["source", "element"],
+                        "additionalProperties": false
+                    }
+                },
+                "required": ["request"],
+                "additionalProperties": false
+            }
+        ]
+    };
+
+    var v_filter;
+
+    this.setFilter = function(p_filter) {
+        v_filter = p_filter;
+    };
+
+    this.getJSONData = function(callback) {
+        var filter = mcopy(v_filter);
+        if (filter.request != undefined) {
+            delete filter.request.params;
+            delete filter.request.remapTo;
+        }
+        console.log(filter);
+        callback(filter);
+    };
+
+    this.setJSONData = function(json) {
+        if (json.dataValue != undefined) {
+            v_filter.dataValue = json.dataValue;
+            v_filter.request = undefined;
+        } else if (json.request != undefined) {
+            if (v_filter.request != undefined) {
+                v_filter.request.source = json.request.source;
+                v_filter.request.element = json.request.element;
+                v_filter.request.ptcname = json.request.ptcname;
+            } else {
+                v_filter.request = json.request;
+            }
+            v_filter.dataValue = undefined;
+        }
+    };
+
+    this.getSchema = function() {
+        return FILTER_SCHEMA;
+    };
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_PlaylistItem.js b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_PlaylistItem.js
new file mode 100644
index 0000000..d678ac9
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/ViewModels/Viewmodel_PlaylistItem.js
@@ -0,0 +1,159 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_PlaylistItem_ViewModel(p_model, p_help) {
+    "use strict";
+
+    var v_model = p_model;
+    var v_help = p_help;
+    var v_dataSourceUtils = new DataSourceUtils();
+
+    this.requestBuilder = new DSHelpToRequest_manual();
+    this.requestBuilder.setRequest(v_model.requests);
+    this.requestBuilder.setHelp(v_help);
+
+    this.filterBuilder = new DSHelpToRequest_manual();
+    this.filterBuilder.setRequest(v_model.condition.expression);
+    this.filterBuilder.setHelp(v_help);
+
+    this.getRelativeTo = function() {
+        return v_model.relativeTo;
+    };
+
+    this.getRepresentation = function() {
+        var tree = [
+            {
+                "text": "next step",
+                "data": {"tooltip": "Drag this to another 'next step' node to create a connection."}
+            },
+            {
+                "text": "api: " + v_model.api,
+                "data": {"tooltip": "The api url where the request and condition will be sent."}
+            },
+            {
+                "text": "startTime: " + v_model.startTime,
+                "data": {"tooltip": "Wait " + v_model.startTime + " seconds after previous steps were executed before the condition evaluation starts."}
+            },
+            {
+                "text": "requests",
+                "children": [],
+                "data": {"tooltip": "The request to send. Drag requests from the help on the left. Drop while pressing control to insert a SetData instead of a GetData request."}
+            },
+            {
+                "text": "condition",
+                "children": [
+                    {
+                        "text": "evaluatingPeriod" + (v_model.condition.evaluatingPeriod == undefined ? "" : ": " + v_model.condition.evaluatingPeriod),
+                        "data": {"tooltip": "The condition is evaluated periodically with this number of seconds period. If undefined, the condition will be evaluated instantly after the previous evaluation finishes."}
+                    },
+                    {
+                        "text": "numberOfExecutions" + (v_model.condition.numberOfExecutions == undefined ? "" : ": " + v_model.condition.numberOfExecutions),
+                        "data": {"tooltip": "The maximum number of condition evaluations. If the last evaluation is also false, the Playlist will terminate. If undefined, the condition will be checked until it becomes true."}
+                    },
+                    {
+                        "text": "cancelingTimeout" + (v_model.condition.cancelingTimeout == undefined ? "" : ": " + v_model.condition.cancelingTimeout),
+                        "data": {"tooltip": "The maximum time in seconds spent waiting for the condition to be true after the previous steps were executed."}
+                    },
+                    {
+                        "text": "expression",
+                        "children": [],
+                        "data": {"tooltip": "Only send the request if the condition evaluates to true."}
+                    }
+                ],
+                "data": {"tooltip": "The condition of the step."}
+            }
+        ];
+        traverseRequest(v_model.requests, tree[3].children);
+        this.traverseFilter(v_model.condition.expression[0].getData.filter, tree[4].children[3].children);
+
+        return tree;
+    };
+
+    this.getName = function() {
+        var name = 'Not set';
+        var request = v_model.requests[0];
+        while (request != undefined) {
+            if (request.setData != undefined) {
+                name = request.setData.element;
+                if (request.setData.params != undefined && request.setData.params.length != 0) {
+                    name += ' - ' + request.setData.params[0].paramName + ': ' + request.setData.params[0].paramValue;
+                }
+            }
+            if (request.getData != undefined && request.getData.children != undefined) {
+                request = request.getData.children[0];
+            } else {
+                request = undefined;
+            }
+        }
+        return name;
+    };
+
+    function traverseRequest(requests, treeData) {
+        for (var i = 0; i < requests.length; ++i) {
+            if (requests[i].getData != undefined) {
+                var highlight = requests[i].getData.filter == undefined ? undefined : "Filter";
+                var node = {"text": requests[i].getData.element, "data": {"tooltip": JSON.stringify(requests[i], null, 4), "highlight": highlight}};
+                if (requests[i].getData.children != undefined) {
+                    node.children = [];
+                    traverseRequest(requests[i].getData.children, node.children)
+                }
+                treeData.push(node);
+            } else if (requests[i].setData != undefined) {
+                var highlight = "Set";
+                if (requests[i].setData.tp < 1 || requests[i].setData.tp > 11) {
+                    highlight = "Warning";
+                }
+                treeData.push({"text": requests[i].setData.element, "data": {"tooltip": JSON.stringify(requests[i], null, 4), "highlight": highlight}});
+            }
+        }
+    }
+
+    this.traverseFilter = function(filter, treeData, paramName) {
+        if (filter != undefined) {
+            var treeNode;
+            var treeNodeText = "";
+            if (paramName != undefined) {
+                treeNodeText = paramName + ": ";
+            }
+            if (filter.dataValue != undefined) {
+                treeNode = {"text": treeNodeText + filter.dataValue, "data": {"tooltip": JSON.stringify(filter, null, 4)}};
+                treeData.push(treeNode);
+            } else if (filter.request != undefined) {
+                treeNode = {"text": treeNodeText + filter.request.element, "children": [], "data": {"tooltip": JSON.stringify(filter, null, 4)}};
+                treeData.push(treeNode);
+                if (filter.request.params != undefined && filter.request.params.length > 0) {
+                    for (var i = 0; i < filter.request.params.length; ++i) {
+                        this.traverseFilter(filter.request.params[i].paramValue, treeNode.children, filter.request.params[i].paramName);
+                    }
+                }
+                if (filter.request.remapTo != undefined) {
+                    this.traverseFilter(filter.request.remapTo, treeNode.children, "remapTo");
+                }
+            }
+        }
+    }
+
+    this.getDesktopData = function() {
+        return v_model.desktopData;
+    };
+
+    this.changeValue = function(path, value) {
+        if (path[0] == 1) {
+            v_model.api = (value == undefined ? (window.location.protocol + "//" + window.location.host + "/api.appagent") : value);
+        } else if (path[0] == 2) {
+            v_model.startTime = (isNaN(value) ? 0 : parseFloat(value));
+        } else if (path[1] == 0) {
+            v_model.condition.evaluatingPeriod = (isNaN(value) ? undefined : parseFloat(value));
+        } else if (path[1] == 1) {
+            v_model.condition.numberOfExecutions = (isNaN(value) ? undefined : parseInt(value));
+        } else if (path[1] == 2) {
+            v_model.condition.cancelingTimeout = (isNaN(value) ? undefined : parseFloat(value));
+        }
+    };
+
+    this.deleteRequest = function() {
+        v_model.requests.splice(0, v_model.requests.length);
+    };
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Views/View.css b/src/Microservices/Playlist/WebApplication/Views/View.css
new file mode 100644
index 0000000..de61832
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View.css
@@ -0,0 +1,197 @@
+/* Main tabs and title */
+
+#PlaylistEditor_MainView {
+    height: calc(100% - 45px);
+}
+
+#PlaylistEditor_PlaylistName {
+    margin-bottom: 4px;
+}
+
+#PlaylistEditor_PlaylistName label {
+    font-size: 20px;
+    font-weight: bold;
+    display: inline-block;
+}
+
+/* Playlist editor tab */
+
+#PlaylistEditor_RequestEditorSplit {
+    height: calc(100% - 57px);
+}
+
+#PlaylistEditor_Playground {
+    width: 100%;
+    height: 100%;
+    display: inline-block;
+    position: absolute;
+}
+
+/* Coloring and sorting trees */
+
+#PlaylistEditor_HelpSearch {
+    width: calc(100% - 10px);
+    height: 20px;
+    display: inline-block;
+    vertical-align: top;
+}
+
+/* Playlist editor's editors */
+
+DIV.PlaylistEditor_EditorHeader {
+    /*text-align: center;*/
+    cursor: move;
+    overflow: hidden;
+    white-space: nowrap;
+    display: block;
+    height: 16px;
+    background-color: #5c9ccc;
+}
+
+DIV.PlaylistEditor_FilterEditor DIV.PlaylistEditor_EditorHeader {
+    background-color: #9c5ccc;
+}
+
+DIV.PlaylistEditor_Editor, DIV.PlaylistEditor_FilterEditor {
+    background-color: #eee;
+    border: 1px solid #aaa;
+    position: absolute;
+    z-index: 800;
+    font-size: 10px;
+    width: auto;
+    height: auto;
+}
+
+label.PlaylistEditor_EditorLabel, label.PlaylistEditor_FilterEditorHeaderLabel {
+    cursor: move;
+    overflow: hidden;
+    white-space: nowrap;
+    display: inline-block;
+    width: calc(100% - 46px);
+    padding-left: 3px;
+    padding-right: 3px;
+}
+
+label.PlaylistEditor_FilterEditorHeaderLabel {
+    width: calc(100% - 81px);
+}
+
+button.PlaylistEditor_EditorButton, button.PlaylistEditor_EditorButtonLeft, button.PlaylistEditor_EditorButtonRight {
+    background-color: #d66;
+    font-size: 10px;
+    font-weight: bold;
+    width: 35px;
+    height: 100%;
+    display: inline-block;
+    color: #fff;
+    border: 1px solid #ccc;
+    vertical-align: top;
+    padding: 0px;
+    width: 20px;
+}
+
+button.PlaylistEditor_EditorButtonLeft {
+    width: 35px;
+}
+
+button:active.PlaylistEditor_EditorButton {
+    background-color: #f22;
+}
+
+button:hover.PlaylistEditor_EditorButton {
+    background-color: #e44;
+}
+
+.PlaylistEditor_Buttonbar {
+    margin: 5px 0 5px 0;
+    width: 100%;
+    height: 22px;
+}
+
+.PlaylistEditor_Button_Left {
+    float: left;
+    background-color: #ddd;
+    border: 2px solid #ddd;
+    height: 22px;
+}
+
+.PlaylistEditor_Button_Right {
+    float: right;
+    background-color: #ccc;
+    border: 2px solid #ccc;
+    height: 22px;
+}
+
+.PlaylistEditor_Button_Left:hover,
+.PlaylistEditor_Button_Right:hover {
+    background-color: #999;
+    border: 2px solid #999;
+    color: white;
+}
+
+.NodeFilter {
+    border: 1px solid purple;
+}
+
+.NodeSet {
+    border: 1px solid green;
+}
+
+.NodeWarning {
+    border: 1px solid red;
+}
+
+#PlaylistEditor_Legend {
+    position: fixed;
+    bottom: 0px;
+    right: 0px;
+    white-space: nowrap;
+    background-color: #e5e5e5;
+    opacity: 0.75;
+    z-index: 10000;
+    user-select: none;
+    cursor: default;
+}
+
+#PlaylistEditor_Legend td:first-child {
+    text-align: center;
+}
+
+/* 3rd party customization */
+
+.dialog-table TD {
+    font-size: 14pt;
+}
+
+.ui-dialog .ui-state-error {
+    padding: .3em;
+}
+
+.ui-dialog {
+    white-space: nowrap;
+    z-index: 10000 !important;
+}
+
+.ui-widget-overlay {
+    z-index: 9900 !important;
+}
+
+.validateTips {
+    white-space: normal;
+}
+
+.validateSearch {
+    width: 60%;
+}
+
+.validateSearchButton, .validateSearchClear {
+    width: 20%;
+}
+
+.vakata-context {
+    z-index: 4000 !important;
+}
+
+#jstree-marker {
+    z-index: 4000 !important;
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Views/View.html b/src/Microservices/Playlist/WebApplication/Views/View.html
new file mode 100644
index 0000000..99aa15c
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View.html
@@ -0,0 +1,42 @@
+<style id="PlaylistEditor_WebAppStyle" type="text/css"></style>
+
+<div id="PlaylistEditor_PlaylistName">
+    <label id="PlaylistEditor_PlaylistNameLabel"></label>
+</div>
+
+<div id="PlaylistEditor_Buttonbar" class="PlaylistEditor_Buttonbar">
+    <button id="PlaylistEditor_Button_New" class="PlaylistEditor_Button_Left">New</button>
+    <button id="PlaylistEditor_Button_Load" class="PlaylistEditor_Button_Left">Load...</button>
+    <button id="PlaylistEditor_Button_Save" class="PlaylistEditor_Button_Left">Save</button>
+    <button id="PlaylistEditor_Button_SaveAs" class="PlaylistEditor_Button_Left">Save as...</button>
+    
+    <button id="PlaylistEditor_Button_Add" class="PlaylistEditor_Button_Right">Add command</button>
+</div>
+<div class="line"></div>
+<div id="PlaylistEditor_RequestEditorSplit">
+    <div class="PlaylistEditor_Helpside">
+        <input id="PlaylistEditor_HelpSearch"></input>
+        <div id="PlaylistEditor_HelpTree"></div>
+    </div>
+    <div id="PlaylistEditor_Playground"></div>
+    <div id="PlaylistEditor_Legend">
+        <table id="PlaylistEditor_LegendTable">
+            <col width="50px"></col>
+            <col width="100px"></col>
+            <tbody>
+                <tr>
+                    <td class="NodeFilter"></td>
+                    <td>Filtered GetData</td>
+                </tr>
+                <tr>
+                    <td class="NodeSet"></td>
+                    <td>SetData</td>
+                </tr>
+                <tr>
+                    <td class="NodeWarning"></td>
+                    <td>Invalid SetData</td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Views/View.js b/src/Microservices/Playlist/WebApplication/Views/View.js
new file mode 100644
index 0000000..54ecfcc
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View.js
@@ -0,0 +1,256 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_View(p_viewModel, p_parentId, p_viewId) {
+    "use strict";
+
+    var HTML = "WebApplications/Playlist/Views/View.html";
+    var CSS = "WebApplications/Playlist/Views/View.css";
+
+    var v_parentId = p_parentId;
+    var v_viewId = p_viewId;
+    var v_viewmodel = p_viewModel;
+    var v_this = this;
+
+    // TODO
+    var v_editorContainer = new PlaylistEditor_EditorContainer_View(v_viewmodel, v_this);
+    var v_focused_obj;
+
+    ///////////////////// GETTER FOR SUBVIEWS //////////////////////////////
+
+    this.getRequestEditorView = function() {
+        return v_requestEditorView;
+    };
+
+    ///////////////////// GENERAL VIEW FUNCTIONS //////////////////////////////
+
+    this.init = function(p_callback) {
+        $("#" + v_parentId).append('<div id="' + v_viewId + '"></div>');
+
+        function htmlLoaded(ok, data) {
+            if (ok) {
+                $("#" + v_viewId).append(data);
+                v_this.toggleButtons(false);
+                $("#PlaylistEditor_WebAppStyle").load(CSS, function() {
+                    p_callback(true);
+                });
+            } else {
+                p_callback(false, "Error loading " + HTML);
+            }
+        }
+
+        v_viewmodel.loadFile(HTML, htmlLoaded);
+    };
+
+    function onWindowResize(event) {
+        if (event.target == window) {
+            $("#PlaylistEditor_MainView").height(ViewUtils.getSuggestedHeight("PlaylistEditor_MainView"));
+        }
+    }
+
+    this.applicationCreated = function() {
+        $("#PlaylistEditor_Button_New").on("click", newPlaylist);
+        $("#PlaylistEditor_Button_Load").on("click", loadPlaylist);
+        $("#PlaylistEditor_Button_Save").on("click", savePlaylist);
+        $("#PlaylistEditor_Button_SaveAs").on("click", savePlaylistAs);
+        $(document).on("keydown", keyPressed);
+
+        $(window).on("resize", onWindowResize);
+        $("#PlaylistEditor_MainView").height(ViewUtils.getSuggestedHeight("PlaylistEditor_MainView"));
+
+        v_editorContainer.applicationCreated();
+    };
+
+    this.destroy = function() {
+        v_editorContainer.destroy();
+        $("#" + v_viewId).remove();
+        $(document).off("keydown", keyPressed);
+        $(window).off("resize", onWindowResize);
+    };
+
+    this.fullRefresh = function() {
+        v_editorContainer.fullRefresh();
+        v_this.toggleButtons(true);
+        v_this.updatePlaylistName();
+        v_focused_obj = undefined;
+    };
+
+    ///////////////////// EVENT HANDLING FUNCTIONS //////////////////////////////
+
+    function keyPressed(event) {
+        if(event.keyCode === 46 && v_focused_obj != undefined && v_focused_obj.deletePressed != undefined) {
+            v_focused_obj.deletePressed();
+        }
+
+        if(event.keyCode === 83 && event.ctrlKey == true && $("#PlaylistEditor_Button_Save").attr("disabled") != true) {
+            savePlaylist();
+            event.preventDefault();
+            event.stopPropagation();
+        }
+    }
+
+    function newPlaylist() {
+        v_this.toggleButtons(false);
+        v_viewmodel.newPlaylist();
+        v_this.fullRefresh();
+    }
+
+    function deletePlaylist(value) {
+        var text = "Are you sure your want to delete " + value + "?";
+        if (v_viewmodel.currentlyEdited() == value) {
+            text += "<br><b>Warning! This is currently oepn!</b>";
+        }
+
+        function playlistDeleted(ok) {
+            if (!ok) {
+                alert("Failed to delete " + value);
+            }
+            v_this.updatePlaylistName();
+            loadPlaylist();
+        }
+
+        var dialog = new ConfirmationDialog(v_viewId, "PlaylistEditor_Dialog_DeletePlaylist", {
+            "header": "Delete",
+            "text": text,
+            "callback": function() {
+                v_viewmodel.deletePlaylist(value, playlistDeleted);
+            }
+        });
+        dialog.open();
+    }
+
+    function loadPlaylist() {
+        function gotPlaylistName(p_Playlist) {
+            v_this.toggleButtons(false);
+            v_viewmodel.loadPlaylist(p_Playlist, function callback(ok) {
+                if (!ok) {
+                    alert("Loading failed");
+                }
+                v_this.fullRefresh();
+            });
+        }
+
+        function optionsArrived(options) {
+            var dialog = new ChoiceDialogWithButton(v_viewId, "PlaylistEditor_Dialog_LoadPlaylist", {
+                "header": "Load",
+                "text": "Please select an entry from the table below.",
+                "choices": options,
+                "callback": gotPlaylistName,
+                "buttonHandler": deletePlaylist,
+                "buttonText": "X",
+                "buttonStyle": "color: red;",
+                "closeOnButtonPress": true
+            });
+            dialog.open();
+        }
+
+        v_viewmodel.listPlaylists(optionsArrived);
+    }
+
+    function savePlaylist() {
+        function playlistSaved(ok) {
+            if (!ok) {
+                alert("Failed to save " + v_viewmodel.currentlyEdited());
+            }
+            v_this.toggleButtons(true);
+        }
+
+        if (v_viewmodel.currentlyEdited() != undefined) {
+            v_this.toggleButtons(false);
+            v_viewmodel.savePlaylist(playlistSaved);
+        } else {
+            savePlaylistAs();
+        }
+    }
+
+    function savePlaylistAs() {
+        var groupName;
+        var newPlaylistName;
+
+        function PlaylistSaved(ok) {
+            if (!ok) {
+                alert("Failed to save " + newPlaylistName);
+            }
+            v_this.updatePlaylistName();
+            v_this.toggleButtons(true);
+        }
+
+        function gotPlaylistName(value) {
+            newPlaylistName = value;
+            v_viewmodel.playlistExists(groupName, newPlaylistName, function(exists) {
+                if (exists) {
+                    var confirmDialog = new ConfirmationDialog(v_viewId, "PlaylistEditor_Dialog_OverWrite", {
+                        "header": "Already exists",
+                        "text": "Overwrite?",
+                        "callback": function() {
+                            v_this.toggleButtons(false);
+                            v_viewmodel.savePlaylistAs(groupName, newPlaylistName, PlaylistSaved);
+                        }
+                    });
+                    confirmDialog.open();
+                } else {
+                    v_this.toggleButtons(false);
+                    v_viewmodel.savePlaylistAs(groupName, newPlaylistName, PlaylistSaved);
+                }
+            });
+        }
+
+        var date = new Date();
+        var dialog = new InputDialog(v_viewId, "PlaylistEditor_Dialog_SavePlaylistAs", {
+            "header": "Save As...",
+            "text": "Please enter the new name.",
+            "defaultValue": "" + date.getFullYear() + "-" + mpad((date.getMonth() + 1), 2) + "-" + mpad(date.getDate(), 2) + "_" + mpad(date.getHours(), 2) + "-" + mpad(date.getMinutes(), 2) + "-" + mpad(date.getSeconds(), 2),
+            "validator": function(value) {
+                var error = "";
+                var pattern = /^([A-Za-z0-9-_.])+$/;
+                if (!pattern.test(value))
+                    error += "The name can only con&shy;tain upper and lower case eng&shy;lish let&shy;ters, num&shy;bers, under&shy;scores, dots and dashes!";
+                if (error.length > 0)
+                  error += "<br/>";
+                if (value.length > 132)
+                    error += "The name is too long (max. 132 char&shy;ac&shy;ters allo&shy;wed)!";
+                return error;
+            },
+            "callback": gotPlaylistName
+        });
+
+        function optionsArrived(options) {
+            new ChoiceDialog(v_viewId, "PlaylistEditor_Dialog_SelectGroup", {
+                "header": "Select group",
+                "text": "Please select a group from the table below.",
+                "choices": options,
+                "callback": function(p_groupName) {
+                    groupName = p_groupName;
+                    dialog.open();
+                }
+            }).open();
+        }
+
+        v_viewmodel.listGroups(optionsArrived);
+    }
+
+    ///////////////////// USEFUL FUNCTION FOR VIEWS //////////////////////////////
+
+    this.toggleButtons = function(on) {
+        $(".PlaylistEditor_Button_Left").prop("disabled", !on);
+        $(".PlaylistEditor_Button_Right").prop("disabled", !on);
+    };
+
+    this.setFocusedObj = function(p_object) {
+        if (v_focused_obj != undefined) {
+            v_focused_obj.setDefaultZidx();
+        }
+        if (p_object != undefined) {
+            p_object.setZidx();
+        }
+        v_focused_obj = p_object;
+    };
+
+    this.updatePlaylistName = function() {
+        var name = v_viewmodel.currentlyEdited();
+        document.getElementById("PlaylistEditor_PlaylistNameLabel").innerHTML = (name == undefined ? "new playlist" : name);
+    };
+}
+//# sourceURL=PlaylistEditor\Views\View.js
diff --git a/src/Microservices/Playlist/WebApplication/Views/View_EditorContainer.js b/src/Microservices/Playlist/WebApplication/Views/View_EditorContainer.js
new file mode 100644
index 0000000..98d7736
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View_EditorContainer.js
@@ -0,0 +1,240 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_EditorContainer_View(p_viewModel, p_parent) {
+    "use strict";
+
+    var v_this = this;
+
+    var v_viewmodel = p_viewModel;
+    var v_parent = p_parent;
+    var v_dataSourceUtils = new DataSourceUtils();
+
+    var v_filterEditor;
+    var v_helpTree;
+    var v_views = {};
+    var v_connectionsView = new GuiEditor_Connections_View("PlaylistEditor_Playground", v_this);
+
+    this.ctrlToggle = false;
+    this.altToggle = false;
+
+    ///////////////////// GENERAL VIEW FUNCTIONS //////////////////////////////
+
+    this.applicationCreated = function() {
+        $("#PlaylistEditor_RequestEditorSplit").split({
+            orientation: "vertical",
+            limit: 100,
+            position: "20%"
+        });
+
+        v_helpTree = $("#PlaylistEditor_HelpTree");
+        createHelpJSTree(v_viewmodel.getHelp());
+
+        setupCallback();
+    };
+
+    this.setFocusedObj = function(obj) {
+        v_parent.setFocusedObj(obj);
+        v_connectionsView.refreshConnections();
+    };
+
+    this.fullRefresh = function() {
+        for (var id in v_views) {
+            v_views[id].destroy();
+        }
+        var editorViewmodels = v_viewmodel.getEditorViewmodels();
+        for (var id in editorViewmodels) {
+            v_views[id] = new PlaylistEditor_PlaylistItem_View(id, editorViewmodels[id], v_this, v_helpTree);
+            v_views[id].applicationCreated();
+        }
+
+        v_connectionsView.fullRebuild();
+        v_connectionsView.refreshConnections();
+    };
+
+    this.destroy = function() {
+        $("#PlaylistEditor_ElementEditor").off("remove");
+        $("#PlaylistEditor_ElementEditor").remove();
+        $("#PlaylistEditor_FilterElementEditor").off("remove");
+        $("#PlaylistEditor_FilterElementEditor").remove();
+        v_connectionsView.destroy();
+        $(document).off("keydown", toggle);
+        $(document).off("keyup", toggle);
+    };
+
+    ///////////////////// CREATING VIEW ELEMENTS AND INTERACTIONS //////////////////////////////
+
+    function createHelpJSTree(help) {
+        var data = v_dataSourceUtils.convertHelpToTreeDataArray(help.sources);
+        v_helpTree.jstree("destroy");
+        v_helpTree = v_helpTree.jstree({
+            "core": {
+                "data": data,
+                "check_callback" : function(operation, node, node_parent, node_position, more) {
+                     if (operation === "copy_node" || operation === "move_node") {
+                         return false;
+                     } else {
+                         return true;
+                     }
+                },
+                "multiple" : false,
+                "animation": false,
+                "worker": false
+            },
+            "plugins" : ["search", "dnd"],
+            "search" : {
+                "show_only_matches": true
+            },
+            "dnd": {
+                "always_copy": true
+            }
+        });
+
+        v_helpTree.bind("hover_node.jstree", function(e, data) {
+            $("#"+data.node.id).prop("title", data.node.data);
+        });
+    }
+
+    function createEditorView() {
+        var viewmodelObject = v_viewmodel.createEditorViewmodel();
+        v_views[viewmodelObject.id] = new PlaylistEditor_PlaylistItem_View(viewmodelObject.id, viewmodelObject.viewmodel, v_this, v_helpTree);
+        v_views[viewmodelObject.id].applicationCreated();
+    }
+
+    this.deleteEditorView = function(id) {
+        v_views[id].destroy();
+        delete v_views[id];
+        v_viewmodel.deleteEditorViewmodel(id);
+        v_connectionsView.fullRebuild();
+        v_connectionsView.refreshConnections();
+    };
+
+    function setupCallback() {
+        $("#PlaylistEditor_HelpSearch").on("input", function() {
+            v_helpTree.jstree("search", $(this).val());
+        });
+
+        $("#PlaylistEditor_Button_Add").click(createEditorView);
+
+        $(document).on("keydown", toggle);
+        $(document).on("keyup", toggle);
+    }
+
+    function toggle(event) {
+        if (event.keyCode == 17) {
+            if (event.type == "keydown") {
+                v_this.ctrlToggle = true;
+            } else {
+                v_this.ctrlToggle = false;
+            }
+        } else if (event.keyCode == 18) {
+            if (event.type == "keydown") {
+                v_this.altToggle = true;
+            } else {
+                v_this.altToggle = false;
+            }
+            event.preventDefault();
+            event.stopPropagation();
+        }
+    }
+
+    this.relativeToEvent = function(nodeId, to) {
+        for (var id in v_views) {
+            if (v_views[id].isNodeFromTree(nodeId)) {
+                v_viewmodel.playlistConnection(id, to);
+                break;
+            }
+        }
+        v_connectionsView.fullRebuild();
+        v_connectionsView.refreshConnections();
+    };
+
+    this.openElementEditor = function(request, offset, refresh) {
+        v_viewmodel.elementEditorViewmodel.setRequest(request);
+        $("#PlaylistEditor_ElementEditor").remove();
+        var customData = {
+            "headerText": "Edit request",
+            "closeable": true,
+            "draggable": true,
+            "offset": offset,
+            "editorOptions": {
+                "disable_array_reorder": true,
+                "disable_edit_json": true,
+                "disable_collapse": true,
+                "no_additional_properties": true
+            },
+            "css": {
+                "width": "600px",
+                "height": "506px",
+                "z-index": 2000
+            }
+        }
+        var editor = new CView_JSONEditor([v_viewmodel.elementEditorViewmodel], "PlaylistEditor_ElementEditor", "PlaylistEditor_Playground", customData);
+        editor.applicationCreated();
+        ViewUtils.applyCss(customData, "PlaylistEditor_ElementEditor");
+        editor.refresh(true);
+        ViewUtils.jumpToEditor("PlaylistEditor_ElementEditor");
+
+        $("#PlaylistEditor_ElementEditor").one("remove", refresh);
+    };
+
+    this.openFilterElementEditor = function(filter, offset, refresh) {
+        v_viewmodel.filterElementEditorViewmodel.setFilter(filter);
+        $("#PlaylistEditor_FilterElementEditor").remove();
+        var customData = {
+            "headerText": "Edit filter",
+            "closeable": true,
+            "draggable": true,
+            "offset": offset,
+            "editorOptions": {
+                "disable_array_reorder": true,
+                "disable_edit_json": true,
+                "disable_collapse": true,
+                "no_additional_properties": true
+            },
+            "css": {
+                "width": "600px",
+                "height": "506px",
+                "z-index": 2000
+            }
+        }
+        var editor = new CView_JSONEditor([v_viewmodel.filterElementEditorViewmodel], "PlaylistEditor_FilterElementEditor", "PlaylistEditor_Playground", customData);
+        editor.applicationCreated();
+        ViewUtils.applyCss(customData, "PlaylistEditor_FilterElementEditor");
+        editor.refresh(true);
+        ViewUtils.jumpToEditor("PlaylistEditor_FilterElementEditor");
+
+        $("#PlaylistEditor_FilterElementEditor").one("remove", refresh);
+    };
+
+    this.openFilterEditor = function(viewmodel, requestPath, requestTree, offset, refresh) {
+        if (v_filterEditor != undefined) {
+            v_filterEditor.destroy();
+        }
+        v_filterEditor = new PlaylistEditor_FilterEditor_View(viewmodel, v_this, requestPath, requestTree, v_helpTree, offset);
+        v_filterEditor.applicationCreated();
+        $("#PlaylistEditor_FilterEditor").one("remove", refresh);
+    };
+
+    this.getObjectsToConnect = function() {
+        var list = [];
+        for (var id in v_views) {
+            list.push(v_views[id]);
+        }
+        return list;
+    };
+
+    this.getEndpoint = function(connectionType, identifier) {
+        for (var id in v_views) {
+            var endpoint = v_views[id].getEndpoint(identifier);
+            if (endpoint != undefined) {
+                return endpoint;
+            }
+        }
+        alert("No endpoint found for " + identifier);
+    };
+
+    this.refreshConnections = v_connectionsView.refreshConnections;
+}
diff --git a/src/Microservices/Playlist/WebApplication/Views/View_FilterEditor.js b/src/Microservices/Playlist/WebApplication/Views/View_FilterEditor.js
new file mode 100644
index 0000000..9264115
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View_FilterEditor.js
@@ -0,0 +1,299 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_FilterEditor_View(p_viewmodel, p_parent, p_requestPath, p_requestTree, p_helpTree, p_offset) {
+
+    var v_viewmodel = p_viewmodel;
+    var v_id = "PlaylistEditor_FilterEditor";
+    var v_parent = p_parent;
+    var v_editorDiv;
+
+    var v_tree;
+    var v_requestPath = p_requestPath;
+    var v_requestTree = p_requestTree;
+    var v_helpTree = p_helpTree;
+    var v_offset = p_offset;
+
+    var v_jsTreeUtils = new JsTreeUtils();
+
+    var v_this = this;
+    var v_editorOpen = false;
+
+    ///////////////////// GENERAL VIEW FUNCTIONS //////////////////////////////
+
+    this.applicationCreated = function() {
+        var html = '' +
+        '<div id="' + v_id + '" class="PlaylistEditor_FilterEditor">' +
+            '<div id="' + v_id + '_Header' + '" class="PlaylistEditor_EditorHeader">' +
+                '<button id="' + v_id + '_Button_Add" class="PlaylistEditor_EditorButtonLeft">Create</button>' +
+                '<label id="' + v_id + '_HeaderText" class="PlaylistEditor_FilterEditorHeaderLabel">Edit filter</label>' +
+                '<button id="' + v_id + '_Button_Context" class="PlaylistEditor_EditorButtonRight"><img src="WebApplicationFramework/Res/grip_white.png"></button>' +
+                '<button id="' + v_id + '_Button_Close" class="PlaylistEditor_EditorButtonRight">X</button>' +
+            '</div>' +
+            '<div id="' + v_id + '_Tree"></div>' +
+        '</div>';
+
+        $("#PlaylistEditor_Playground").append(html);
+        v_tree = $("#" + v_id + "_Tree");
+        createTree();
+
+        v_editorDiv = $("#" + v_id);
+        v_editorDiv.draggable({
+            stop: function(event, ui) {
+                document.getElementById(v_id).style.height = "auto";
+                document.getElementById(v_id).style.width = "auto";
+            },
+            handle: "#" + v_id + "_Header",
+            containment: [$("#PlaylistEditor_Playground").offset().left, $("#PlaylistEditor_Playground").offset().top, 20000, 20000]
+        });
+        v_editorDiv.on('click', function(){
+            v_parent.setFocusedObj(v_this);
+        });
+        v_editorDiv.on('dragstart', function(){
+            v_parent.setFocusedObj(v_this);
+        });
+
+        v_editorDiv.offset(p_offset);
+        v_editorDiv.offset(p_offset);
+
+        setupCallbacks();
+        v_parent.setFocusedObj(v_this);
+    };
+
+    this.destroy = function() {
+        closeEditors();
+        v_editorDiv.remove();
+    };
+
+    this.setDefaultZidx = function() {
+        v_editorDiv.css("z-index", 1500);
+    };
+
+    this.setFocusedObj = v_parent.setFocusedObj;
+
+    this.setZidx = function() {
+        v_editorDiv.css("z-index", 1500);
+    };
+
+    this.deletePressed = function() {
+        var selected = v_tree.jstree("get_selected")[0];
+        if (selected.length != undefined) {
+
+            var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", selected)).path;
+            filterPath.shift();
+            closeEditors();
+            v_viewmodel.requestBuilder.deleteFilterPart(v_requestPath, filterPath);
+            setTimeout(createTree, 0);
+        }
+    };
+
+    ///////////////////// CREATING AND REFRESHING THE TREE //////////////////////////////
+
+    function closeEditors() {
+        $("#PlaylistEditor_FilterElementEditor").remove();
+    }
+
+    function createTree() {
+        var data = [];
+        v_viewmodel.traverseFilter(v_viewmodel.requestBuilder.getFilterPart(v_requestPath, []), data);
+        v_tree.jstree("destroy");
+        v_tree = v_tree.jstree({
+            "core": {
+                "data": data,
+                "check_callback": function(operation, node, node_parent, node_position, more) {
+                    if (v_editorOpen) {
+                        return false;
+                    } else if (operation === "copy_node" && v_jsTreeUtils.isNodeFromTree(node, v_helpTree) && !v_jsTreeUtils.isRoot(node_parent)) {
+                        return dragFromHelpValidate(node.id, node_parent.id);
+                    } else if (operation === "copy_node" && v_jsTreeUtils.isNodeFromTree(node, v_requestTree) && !v_jsTreeUtils.isRoot(node_parent)) {
+                        var path = v_jsTreeUtils.getPath(v_requestTree.jstree("get_node", node.id)).path;
+                        if (path.length > 1 && path[0] == 3) {
+                            path.shift();
+                            return hasPrefix(v_requestPath, path);
+                        } else {
+                            return false;
+                        }
+                    } else if (operation === "copy_node") {
+                        return false;
+                    } else {
+                        return true;
+                    }
+                },
+                "multiple": false,
+                "animation": false,
+                "worker": false
+            },
+            "plugins": ["contextmenu", "dnd"],
+            "dnd": {
+                "always_copy": true
+            },
+            "contextmenu": {
+                "items": function($node) {
+                    if (v_editorOpen) {
+                        return {};
+                    } else {
+                        return {
+                            "Edit": {
+                                "label": "Edit filter",
+                                "action": function(data) {
+                                    v_editorOpen = true;
+                                    var offset = data.reference.offset();
+                                    offset.left += data.reference.width();
+                                    var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.reference)).path;
+                                    filterPath.shift();
+                                    v_parent.openFilterElementEditor(v_viewmodel.requestBuilder.getFilterPart(v_requestPath, filterPath), offset, function() {
+                                        v_editorOpen = false;
+                                        setTimeout(createTree, 0);
+                                    });
+                                },
+                                "separator_after": true
+                            },
+                            "Add": {
+                                "label": "Add param",
+                                "action": function(data) {
+                                    var node = v_tree.jstree("get_node", data.reference);
+                                    var filterPath = v_jsTreeUtils.getPath(node).path;
+                                    var position = v_tree.jstree("get_children_dom", node).length;
+                                    filterPath.shift();
+                                    filterPath.push(position);
+                                    if (v_viewmodel.requestBuilder.isValidToAddParamToFilterRequest(v_requestPath, filterPath)) {
+                                        var paramName = prompt("New param name: ");
+                                        if (paramName != undefined) {
+                                            closeEditors();
+                                            v_viewmodel.requestBuilder.addFilterPart(v_requestPath, filterPath, paramName);
+                                            setTimeout(createTree, 0);
+                                        }
+                                    } else {
+                                        alert('Cannot add param to a dataValue');
+                                    }
+                                }
+                            },
+                            "RemapTo": {
+                                "label": "Add remapTo",
+                                "action": function(data) {
+
+                                    var node = v_tree.jstree("get_node", data.reference);
+                                    var filterPath = v_jsTreeUtils.getPath(node).path;
+                                    var position = v_tree.jstree("get_children_dom", node).length;
+                                    filterPath.shift();
+                                    filterPath.push(position);
+                                    if (v_viewmodel.requestBuilder.isValidToAddParamToFilterRequest(v_requestPath, filterPath)) {
+                                        var paramName = "remapTo";
+                                        closeEditors();
+                                        v_viewmodel.requestBuilder.addFilterPart(v_requestPath, filterPath, paramName);
+                                        setTimeout(createTree, 0);
+                                    } else {
+                                        alert('Cannot add param to a dataValue');
+                                    }
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.reference)).path;
+                                    filterPath.shift();
+                                    closeEditors();
+                                    v_viewmodel.requestBuilder.deleteFilterPart(v_requestPath, filterPath);
+                                    setTimeout(createTree, 0);
+                                },
+                                "separator_after": true
+                            },
+                            "ChangeParamName": {
+                                "label": "Change param name",
+                                "action": function(data) {
+                                    var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.reference)).path;
+                                    filterPath.shift();
+                                    var paramName = prompt("New param name: ");
+                                    if (paramName != undefined) {
+                                        closeEditors();
+                                        v_viewmodel.requestBuilder.changeParamNameOfFilterRequest(v_requestPath, filterPath, paramName);
+                                        setTimeout(createTree, 0);
+                                    }
+                                }
+                            }
+                        };
+                    }
+                },
+                "select_node": true
+            }
+        });
+
+        v_tree.bind("hover_node.jstree", function(e, data) {
+            var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.node.id)).path;
+            filterPath.shift();
+            var string = JSON.stringify(v_viewmodel.requestBuilder.getFilterPartCopy(v_requestPath, filterPath), null, 4);
+            $("#" + data.node.id).prop("title", string);
+        });
+
+        v_tree.bind("select_node.jstree", function (e, data) {
+            closeEditors();
+        });
+
+        v_tree.bind("after_close.jstree", function (e, data) {
+            v_tree.jstree("open_all");
+        });
+
+        v_tree.bind("redraw.jstree", function(e, data) {
+            document.getElementById(v_id).style.height = "auto";
+            document.getElementById(v_id).style.width = "auto";
+        });
+
+        v_tree.bind("copy_node.jstree", function(e, data) {
+            if (v_jsTreeUtils.isNodeFromTree(data.original, v_helpTree)) {
+                helpNodeCopied(data);
+            } else if (v_jsTreeUtils.isNodeFromTree(data.original, v_requestTree)) {
+                requestNodeCopied(data);
+            }
+        });
+
+        v_tree.bind("ready.jstree", function(e, data) {
+            v_tree.jstree("open_all");
+        });
+    }
+
+    ///////////////////// HANDLING EVENTS //////////////////////////////
+
+    function dragFromHelpValidate(p_helpId, p_filterId) {
+        var helpPath = v_jsTreeUtils.getPath(v_helpTree.jstree("get_node", p_helpId)).path;
+        var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", p_filterId)).path;
+        filterPath.shift();
+        return v_viewmodel.requestBuilder.isValidToConvertFilterToRequest(v_requestPath, filterPath, helpPath)
+    }
+
+    function helpNodeCopied(data) {
+        v_tree.jstree("delete_node", data.node.id);
+        var helpPath = v_jsTreeUtils.getPath(v_helpTree.jstree("get_node", data.original.id)).path;
+        var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.parent)).path;
+        filterPath.shift();
+        closeEditors();
+        v_viewmodel.requestBuilder.convertFilterPartToRequest(v_requestPath, filterPath, helpPath);
+        setTimeout(createTree, 0);
+    }
+
+    function requestNodeCopied(data) {
+        v_tree.jstree("delete_node", data.node.id);
+        var path = v_jsTreeUtils.getPath(v_requestTree.jstree("get_node", data.original.id)).path;
+        path.shift();
+        var dataValue = "%Parent" + (path.length - 1) + "%";
+        var filterPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.parent)).path;
+        filterPath.shift();
+        closeEditors();
+        v_viewmodel.requestBuilder.convertFilterPartToDataValue(v_requestPath, filterPath, dataValue);
+        setTimeout(createTree, 0);
+    }
+
+    function setupCallbacks() {
+        $('#' + v_id + '_Button_Close').click(v_this.destroy);
+        $('#' + v_id + '_Button_Context').click(function() {
+            $("#" + v_jsTreeUtils.getNodeIdFromPath(v_tree, [0]) + " > a").contextmenu();
+        });
+        $('#' + v_id + '_Button_Add').click(function() {
+            closeEditors();
+            v_viewmodel.requestBuilder.addFilterPart(v_requestPath, []);
+            setTimeout(createTree, 0);
+        });
+    }
+}
+//# sourceURL=PlaylistEditor\Views\View_FilterEditor.js
\ No newline at end of file
diff --git a/src/Microservices/Playlist/WebApplication/Views/View_PlaylistItem.js b/src/Microservices/Playlist/WebApplication/Views/View_PlaylistItem.js
new file mode 100644
index 0000000..c766639
--- /dev/null
+++ b/src/Microservices/Playlist/WebApplication/Views/View_PlaylistItem.js
@@ -0,0 +1,579 @@
+// Copyright (c) 2000-2017 Ericsson Telecom AB                                                       //
+// All rights reserved. This program and the accompanying materials are made available under the     //
+// terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at //
+// http://www.eclipse.org/legal/epl-v10.html                                                         //
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+function PlaylistEditor_PlaylistItem_View(p_id, p_viewmodel, p_parent, p_helpTree) {
+
+    var v_playlistId = p_id;
+    var v_id = 'Playlist_Editor_' + v_playlistId;
+    var v_viewmodel = p_viewmodel;
+    var v_parent = p_parent;
+    var v_desktopData = v_viewmodel.getDesktopData();
+    var v_helpTree = p_helpTree;
+    var v_tree;
+
+    var v_jsTreeUtils = new JsTreeUtils();
+    var v_zIndex = 800;
+
+    var v_this = this;
+
+    this.applicationCreated = function() {
+        createEditorDiv();
+        v_tree = $('#' + v_id + '_Tree');
+        setupCallbacks();
+
+        createTree();
+
+        $("#" + v_id).on('click', function(){
+            v_parent.setFocusedObj(v_this);
+        });
+        $("#" + v_id).on('dragstart', function(){
+            v_parent.setFocusedObj(v_this);
+        });
+    };
+
+    this.destroy = function() {
+        $("#" + v_id).remove();
+    };
+
+    this.setDefaultZidx = function() {
+        $("#" + v_id).css("z-index", 800);
+        v_zIndex = 800;
+        v_parent.refreshConnections();
+    };
+
+    this.setZidx = function() {
+        $("#" + v_id).css("z-index", 1000);
+        v_zIndex = 1000;
+        v_parent.refreshConnections();
+    };
+
+    this.deletePressed = function() {
+        var selected = v_tree.jstree("get_selected")[0];
+        if (selected.length != undefined) {
+            var path = v_jsTreeUtils.getPath(v_tree.jstree("get_node", selected)).path;
+            if ((path.length == 1 && (path[0] == 1 || path[0] == 2)) || (path.length == 2 && path[0] == 4 && path[1] < 3)) {
+                v_viewmodel.changeValue(path);
+            } else if (path.length == 1 && path[0] == 3) {
+                v_viewmodel.deleteRequest();
+            } else if (path.length > 1 && path[0] == 3) {
+                path.shift();
+                v_viewmodel.requestBuilder.deleteRequest(path);
+            } else if (path.length == 2 && path[0] == 4 && path[1] == 3) {
+                v_viewmodel.filterBuilder.deleteFilterPart([0], []);
+            } else if (path.length > 2 && path[0] == 4) {
+                path.shift();
+                path.shift();
+                path.shift();
+                v_viewmodel.filterBuilder.deleteFilterPart([0], path);
+            }
+
+            setTimeout(createTree, 0);
+        }
+    };
+
+    this.isNodeFromTree = function(id) {
+        return v_tree.jstree("get_node", id);
+    };
+
+    function createEditorDiv() {
+        var html =
+        '<div id="' + v_id + '" class="PlaylistEditor_Editor">' +
+            '<div id="' + v_id + '_Header" class="PlaylistEditor_EditorHeader">' +
+                '<label id="' + v_id + '_Name" class="PlaylistEditor_EditorLabel"></label>' +
+                '<button id="' + v_id + '_Minimize" class="PlaylistEditor_EditorButton">_</button>' +
+                '<button id="' + v_id + '_Close" class="PlaylistEditor_EditorButton">X</button>' +
+            '</div>' +
+            '<div id="' + v_id + '_Tree"></div>' +
+        '</div>'
+
+        $('#PlaylistEditor_Playground').append(html);
+
+        var parentOffset = $("#PlaylistEditor_Playground").offset();
+        var offset = {
+            "top": v_desktopData.top + parentOffset.top,
+            "left": v_desktopData.left + parentOffset.left
+        }
+
+        $("#" + v_id).offset(offset);
+        $("#" + v_id).offset(offset);
+
+        $("#" + v_id).draggable({
+            stop: function(event, ui) {
+                redrawOnDrag(event, ui);
+                $("#" + v_id).height("auto");
+                $("#" + v_id).width("auto");
+            },
+            handle: "#" + v_id + "_Header",
+            drag: redrawOnDrag,
+            containment: [0, 0, 20000, 20000]
+        });
+
+        if (!v_desktopData.visible) {
+            v_tree.slideToggle(0);
+            changeMinimizeButtonText();
+        }
+    }
+
+    function changeMinimizeButtonText() {
+        if (!v_desktopData.visible) {
+            document.getElementById(v_id + '_Minimize').innerHTML = "+";
+        } else {
+            document.getElementById(v_id + '_Minimize').innerHTML = "_";
+        }
+    }
+
+    function redrawOnDrag(event, ui) {
+        v_parent.refreshConnections(v_this);
+        if (event.type == "dragstop") {
+            var parentOffset = $("#PlaylistEditor_Playground").offset();
+            var offset = $("#" + v_id).offset();
+            v_desktopData.top = offset.top - parentOffset.top + $("#PlaylistEditor_Playground").scrollTop();
+            v_desktopData.left = offset.left - parentOffset.left + $("#PlaylistEditor_Playground").scrollLeft();
+        }
+    }
+
+    function setupCallbacks() {
+        $("#" + v_id + "_Minimize").click(function() {
+            v_desktopData.visible = !v_desktopData.visible;
+            v_tree.slideToggle("fast", function() {
+                v_parent.refreshConnections(v_this);
+            });
+            changeMinimizeButtonText();
+            $("#" + v_id).width("auto");
+        });
+
+        $("#" + v_id + "_Close").click(function() {
+            v_parent.deleteEditorView(v_playlistId);
+        });
+    }
+
+    function refresh() {
+        createTree();
+        v_parent.refreshConnections(v_this);
+    }
+
+    function createTree() {
+        var data = v_viewmodel.getRepresentation();
+        v_tree.jstree("destroy");
+        v_tree = v_tree.jstree({
+            "core": {
+                "data": data,
+                "check_callback" : function(operation, node, node_parent, node_position, more) {
+                    if (operation == "copy_node" && v_jsTreeUtils.isNodeFromTree(node, v_helpTree) && v_parent.ctrlToggle && v_jsTreeUtils.getDepth(node, v_helpTree) > 1) {
+                        return true;
+                    } else if (operation == "copy_node" && !v_jsTreeUtils.isNodeFromTree(node, v_tree) && node.text == "next step" && node_parent.text == "next step") {
+                        return true;
+                    } else if (operation == "copy_node" && v_jsTreeUtils.isNodeFromTree(node, v_helpTree)) {
+                        return dragFromHelpValidate(node.id, node_parent.id);
+                    } else if (operation == "copy_node") {
+                        return false;
+                    } else {
+                        return true;
+                    }
+                },
+                "multiple": false,
+                "animation": false,
+                "worker": false
+            },
+            "plugins" : ["contextmenu", "dnd"],
+            "dnd": {
+                "always_copy": true
+            },
+            "contextmenu": {
+                "items": function(node) {
+                    var path = v_jsTreeUtils.getPath(node).path;
+                    if ((path.length == 1 && (path[0] == 1 || path[0] == 2)) || (path.length == 2 && path[0] == 4 && path[1] < 3)) {
+                        return {
+                            "Edit": {
+                                "label": "Edit",
+                                "action": function(data) {
+                                    var value = prompt("Please enter the new value.");
+                                    if (value != undefined) {
+                                        v_viewmodel.changeValue(path, value);
+                                        setTimeout(refresh, 0);
+                                    }
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    v_viewmodel.changeValue(path);
+                                    setTimeout(refresh, 0);
+                                }
+                            }
+                        };
+                    } else if (path.length == 1 && path[0] == 3) {
+                        return {
+                            "Add": {
+                                "label": "Add empty request",
+                                "action": function(data) {
+                                    v_viewmodel.requestBuilder.createEmptyRequest(0);
+                                    setTimeout(refresh, 0);
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete whole request",
+                                "action": function(data) {
+                                    v_viewmodel.deleteRequest();
+                                    setTimeout(refresh, 0);
+                                }
+                            }
+                        };
+                    } else if (path.length > 1 && path[0] == 3 && (node.data.highlight == "Set" || node.data.highlight == "Warning")) {
+                        return {
+                            "Edit": {
+                                "label": "Edit request",
+                                "action": function(data) {
+                                    path.shift();
+                                    var offset = data.reference.offset();
+                                    offset.left += data.reference.width();
+                                    v_parent.openElementEditor(v_viewmodel.requestBuilder.getRequestFromPath(path), offset, refresh);
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.deleteRequest(path);
+                                    setTimeout(refresh, 0);
+                                },
+                                "separator_after": true
+                            },
+                            "ConvertToGetData": {
+                                "label": "Convert to getData",
+                                "action": function(data) {
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.convertToGetData(path);
+                                    setTimeout(refresh, 0);
+                                }
+                            }
+                        };
+                    } else if (path.length > 1 && path[0] == 3) {
+                        return {
+                            "Edit": {
+                                "label": "Edit request",
+                                "action": function(data) {
+                                    path.shift();
+                                    var offset = data.reference.offset();
+                                    offset.left += data.reference.width();
+                                    v_parent.openElementEditor(v_viewmodel.requestBuilder.getRequestFromPath(path), offset, refresh);
+                                }
+                            },
+                            "EditFilter": {
+                                "label": "Edit filter",
+                                "action": function(data) {
+                                    path.shift();
+                                    var offset = data.reference.offset();
+                                    offset.left += data.reference.width();
+                                    v_parent.openFilterEditor(v_viewmodel, path, v_tree, offset, refresh);
+                                },
+                                "separator_after": true
+                            },
+                            "Add": {
+                                "label": "Add child request",
+                                "action": function(data) {
+                                    v_desktopData.openNodes.push(path);
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.addEmptyChildRequest(path, 0);
+                                    setTimeout(refresh, 0);
+                                }
+                            },
+                            "Copy": {
+                                "label": "Copy",
+                                "action": function(data) {
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.copyRequest(path);
+                                    setTimeout(refresh, 0);
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.deleteRequest(path);
+                                    setTimeout(refresh, 0);
+                                },
+                                "separator_after": true
+                            },
+                            "ConvertToSetData": {
+                                "label": "Convert to setData",
+                                "action": function(data) {
+                                    path.shift();
+                                    v_viewmodel.requestBuilder.convertToSetData(path);
+                                    setTimeout(refresh, 0);
+                                }
+                            }
+                        }
+                    } else if (path.length == 2 && path[0] == 4 && path[1] == 3) {
+                        return {
+                            "Create": {
+                                "label": "Create",
+                                "action": function(data) {
+                                    v_viewmodel.filterBuilder.addFilterPart([0], []);
+                                    v_desktopData.openNodes.push(path);
+                                    setTimeout(refresh, 0);
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    v_viewmodel.filterBuilder.deleteFilterPart([0], []);
+                                    setTimeout(refresh, 0);
+                                }
+                            }
+                        };
+                    } else if (path.length > 2 && path[0] == 4) {
+                        return {
+                            "Edit": {
+                                "label": "Edit filter",
+                                "action": function(data) {
+                                    path.shift();
+                                    path.shift();
+                                    path.shift();
+                                    var offset = data.reference.offset();
+                                    offset.left += data.reference.width();
+                                    v_parent.openFilterElementEditor(v_viewmodel.filterBuilder.getFilterPart([0], path), offset, refresh);
+                                },
+                                "separator_after": true
+                            },
+                            "Add": {
+                                "label": "Add param",
+                                "action": function(data) {
+                                    var node = v_tree.jstree("get_node", data.reference);
+                                    var originalPath = mcopy(path);
+                                    var position = v_tree.jstree("get_children_dom", node).length;
+                                    path.push(position);
+                                    path.shift();
+                                    path.shift();
+                                    path.shift();
+                                    if (v_viewmodel.filterBuilder.isValidToAddParamToFilterRequest([0], path)) {
+                                        var paramName = prompt("New param name: ");
+                                        if (paramName != undefined) {
+                                            v_viewmodel.filterBuilder.addFilterPart([0], path, paramName);
+                                            v_desktopData.openNodes.push(originalPath);
+                                            setTimeout(refresh, 0);
+                                        }
+                                    } else {
+                                        alert('Cannot add param to a dataValue');
+                                    }
+                                }
+                            },
+                            "RemapTo": {
+                                "label": "Add remapTo",
+                                "action": function(data) {
+                                    var node = v_tree.jstree("get_node", data.reference);
+                                    var originalPath = mcopy(path);
+                                    var position = v_tree.jstree("get_children_dom", node).length;
+                                    path.push(position);
+                                    path.shift();
+                                    path.shift();
+                                    path.shift();
+                                    if (v_viewmodel.filterBuilder.isValidToAddParamToFilterRequest([0], path)) {
+                                        var paramName = "remapTo";
+                                        v_viewmodel.filterBuilder.addFilterPart([0], path, paramName);
+                                        v_desktopData.openNodes.push(originalPath);
+                                        setTimeout(refresh, 0);
+                                    } else {
+                                        alert('Cannot add remapTo to a dataValue');
+                                    }
+                                }
+                            },
+                            "Delete": {
+                                "label": "Delete",
+                                "action": function(data) {
+                                    path.shift();
+                                    path.shift();
+                                    path.shift();
+                                    v_viewmodel.filterBuilder.deleteFilterPart([0], path);
+                                    setTimeout(refresh, 0);
+                                },
+                                "separator_after": true
+                            },
+                            "ChangeParamName": {
+                                "label": "Change param name",
+                                "action": function(data) {
+                                    path.shift();
+                                    path.shift();
+                                    path.shift();
+                                    var paramName = prompt("New param name: ");
+                                    if (paramName != undefined) {
+                                        v_viewmodel.filterBuilder.changeParamNameOfFilterRequest([0], path, paramName);
+                                        setTimeout(refresh, 0);
+                                    }
+                                }
+                            }
+                        };
+                    }
+                }
+            }
+        });
+
+        v_tree.on("redraw.jstree", function(e, data) {
+            $("#" + v_id).height("auto");
+            $("#" + v_id).width("auto");
+        });
+
+        v_tree.on("after_open.jstree after_close.jstree", function(e, data) {
+            $("#" + v_id).height("auto");
+            $("#" + v_id).width("auto");
+            saveOpenNodes();
+            setTimeout(function() {
+                highlight(data.node);
+            }, 0);
+        });
+
+        v_tree.on("copy_node.jstree", function(e, data) {
+            v_tree.jstree("delete_node", data.node.id);
+            var toPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", data.parent)).path;
+
+            if (v_jsTreeUtils.isNodeFromTree(data.original, v_helpTree)) {
+                helpNodeCopied(data.original.id, data.parent, data.position);
+            } else {
+                v_parent.relativeToEvent(data.original, v_playlistId);
+            }
+        });
+
+        v_tree.on("hover_node.jstree", function(e, data) {
+            if (data.node.data != undefined && data.node.data.tooltip != undefined) {
+                $("#" + data.node.id).prop("title", data.node.data.tooltip);
+            }
+        });
+
+        openNodes();
+        $("#" + v_id + "_Name").text(v_viewmodel.getName());
+        setTimeout(highlight, 0);
+    }
+
+    function saveOpenNodes() {
+        v_desktopData.openNodes = v_jsTreeUtils.findOpenNodes(v_tree);
+    }
+
+    function openNodes() {
+        v_jsTreeUtils.openNodes(v_tree, v_desktopData.openNodes);
+    }
+
+    function highlight(node) {
+        var nodes = v_tree.jstree("get_json", node, {"flat": true});
+        for (var i = 0; i < nodes.length; ++i) {
+            $("#" + nodes[i].id + "_anchor").removeClass("NodeFiler NodeSet NodeWarning");
+            if (nodes[i].data.highlight != undefined) {
+                $("#" + nodes[i].id + "_anchor").addClass("Node" + nodes[i].data.highlight);
+            }
+        }
+    }
+
+    function dragFromHelpValidate(p_helpId, p_requestParentId) {
+        var helpPath = v_jsTreeUtils.getPath(v_helpTree.jstree("get_node", p_helpId)).path;
+        if (p_requestParentId != "#") {
+            var requestPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", p_requestParentId)).path;
+            if (requestPath.length == 1 && requestPath[0] == 3) {
+                return v_viewmodel.requestBuilder.isValidToCreateRequest(helpPath);
+            } else if (requestPath.length > 1 && requestPath[0] == 3) {
+                requestPath.shift();
+                return v_viewmodel.requestBuilder.isValidToAddRequest(helpPath, requestPath);
+            } else if (requestPath.length > 2 && requestPath[0] == 4 && requestPath[1] == 3) {
+                requestPath.shift();
+                requestPath.shift();
+                requestPath.shift();
+                return v_viewmodel.filterBuilder.isValidToConvertFilterToRequest([0], requestPath, helpPath);
+            }
+        }
+        return false;
+    }
+
+    function helpNodeCopied(p_helpId, p_requestParentId, p_position) {
+        var helpPath = v_jsTreeUtils.getPath(v_helpTree.jstree("get_node", p_helpId)).path;
+        if (p_requestParentId != "#") {
+            var requestPath = v_jsTreeUtils.getPath(v_tree.jstree("get_node", p_requestParentId)).path;
+            var originalRequestPath = mcopy(requestPath);
+            if (requestPath.length == 1 && requestPath[0] == 3) {
+                requestPath.shift();
+                v_viewmodel.requestBuilder.createRequest(helpPath, p_position);
+                v_desktopData.openNodes.push(originalRequestPath);
+                setTimeout(refresh, 0);
+            } else if (requestPath.length > 1 && requestPath[0] == 3) {
+                requestPath.shift();
+                v_viewmodel.requestBuilder.addChildRequest(helpPath, requestPath, p_position, v_parent.altToggle);
+                v_desktopData.openNodes.push(originalRequestPath);
+                setTimeout(refresh, 0);
+            } else if (requestPath.length > 2 && requestPath[0] == 4 && requestPath[1] == 3) {
+                requestPath.shift();
+                requestPath.shift();
+                requestPath.shift();
+                v_viewmodel.filterBuilder.convertFilterPartToRequest([0], requestPath, helpPath);
+                v_desktopData.openNodes.push(originalRequestPath);
+                setTimeout(refresh, 0);
+            }
+        }
+    }
+
+    function RelativeToConnection(p_playlistId) {
+        this.getOffset = function() {
+            var htmlObj = $("#" + v_id);
+            var offset = htmlObj.offset();
+            offset.top += htmlObj.height() / 2;
+            return offset;
+        };
+
+        this.getZIndex = function() {
+            return v_zIndex + 1;
+        };
+
+        this.isEnabled = function() {
+            return true;
+        };
+
+        this.object = v_this;
+
+        this.endpointType = "target";
+        this.identifier = p_playlistId;
+    }
+
+    var v_relativeToSource = {
+        "getOffset": function() {
+            var htmlObj;
+
+            if (!v_desktopData.visible) {
+                var id = v_id + "_Header";
+                htmlObj = $("#" + id);
+            } else {
+                var id = v_jsTreeUtils.getLastNodeIdFromPath(v_tree, [0]);
+                htmlObj = $("#" + id + "_anchor");
+            }
+
+            var offset = htmlObj.offset();
+            offset.left += htmlObj.width();
+            offset.top += htmlObj.height() / 2;
+            return offset;
+        },
+
+        "getZIndex": function() {
+            return v_zIndex + 1;
+        },
+
+        "isEnabled": function() {
+            return true;
+        },
+
+        "object": v_this
+    }
+
+    this.getEndpoint = function(identifier) {
+        if (identifier == v_playlistId) {
+            return v_relativeToSource;
+        } else {
+            return undefined;
+        }
+    };
+
+    this.getEndpoints = function() {
+        var connections = [];
+        var relativeTo = v_viewmodel.getRelativeTo();
+        for (var i = 0; i < relativeTo.length; ++i) {
+            connections.push(new RelativeToConnection(relativeTo[i]));
+        }
+        return connections;
+    };
+}
\ No newline at end of file
diff --git a/src/Microservices/Playlist/__init__.py b/src/Microservices/Playlist/__init__.py
new file mode 100644
index 0000000..82061d0
--- /dev/null
+++ b/src/Microservices/Playlist/__init__.py
@@ -0,0 +1,15 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+This application can be used to handle Playlists that can be used to issue specific requests by timers and conditions.
+'''
+
+from Playlist import Playlist
+
+EXTENSION = 'api.playlist'
+
+def createHandler(directory, *args):
+    return Playlist(directory)
\ No newline at end of file
diff --git a/src/Microservices/__init__.py b/src/Microservices/__init__.py
new file mode 100644
index 0000000..55aacbf
--- /dev/null
+++ b/src/Microservices/__init__.py
@@ -0,0 +1,49 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+'''
+This package contains the microservices.
+They are dynamically loaded by AppAgent before starting the http server.
+
+Microservices must implement the following interface (in their __init__.py):
+    EXTENSION
+        string (e.g '.control')
+    createHandler(directory)
+        function, that must return a http message handler that will be associated with the EXTENSION
+        the directory parameter is a path to the microservice's directory so the handler can load resources
+
+The message handlers must implement the following interface functions:
+    handleMessage(method, path, headers, body, userCredentials, response):
+        method: either GET or POST
+        path: the uri of the request
+        headers: the request headers
+        body: the request body
+        userCredentials: the user credentials dictionary with the following members:
+            username: the username
+            password: the password of the user
+            groups: the set of groups that the user belongs to
+        response: the response dictionary, the following members can be set:
+            returnCode: the return code (default: 200)
+            mimeType: the mime type (default: 'text/plain')
+            body: the response body (default: empty string)
+    close():
+        called when the server is stopped so the handler can do some cleanup
+
+Additionally, the handler can implement the following function:
+    getDataSourceHandlers():
+        this function must return a dictionary, for example:
+        {
+            'someSourceId': {
+                'getDataHandler': a_function_that_can_handle_getData_requests(request, userCredentials)
+                'setDataHandler': a_function_that_can_handle_setData_requests(request, userCredentials)
+            },
+            'otherSourceId': {...}
+        }
+
+        When the function exists it will be used to add the handlers to the DataSource.
+        Depending on the source of a given request, the DataSource will call the appropriate handler with the getData or setData request dictionary.
+
+If an microservice contains a GUI directory, AppAgent will create a symlink to that directory in the CustomizableContent, so it can appear on the GreenGUI.
+'''
\ No newline at end of file
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..c20f3a5
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,5 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/test/runTests.py b/test/runTests.py
new file mode 100644
index 0000000..d2d43d6
--- /dev/null
+++ b/test/runTests.py
@@ -0,0 +1,10 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import pytest, sys
+
+sys.path.insert(1, 'src')
+#pytest.main(["--junitxml", "result.xml", "--tb", "short", "test"])
+pytest.main(sys.argv[1:])
diff --git a/test/testcases/__init__.py b/test/testcases/__init__.py
new file mode 100644
index 0000000..c20f3a5
--- /dev/null
+++ b/test/testcases/__init__.py
@@ -0,0 +1,5 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/test/testcases/test_AppAgent.py b/test/testcases/test_AppAgent.py
new file mode 100644
index 0000000..f9443bf
--- /dev/null
+++ b/test/testcases/test_AppAgent.py
@@ -0,0 +1,166 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, threading, time, urllib2, shutil, os
+from AppAgent import *
+from test.utils.Utils import *
+from utils.MockedHandler import *
+
+class AppAgentTest(unittest.TestCase):
+
+    def runServer(self):
+        # start AppAgent
+        self.server.serve_forever()
+
+    def setUp(self):
+        # create test directory
+        if os.path.exists(HTTPSERVER_DIR):
+            shutil.rmtree(HTTPSERVER_DIR)
+        os.makedirs(HTTPSERVER_DIR)
+        # change to test directory that the http server will serve
+        self.directory = os.getcwd()
+        os.chdir(HTTPSERVER_DIR)
+        # a mocked microservice
+        self.handler = MockedDsRestApiHandler()
+        # create a server on a free port
+        self.port = 8000
+        while True:
+            try:
+                self.server = ThreadedHTTPServer(('localhost', self.port), MainHandler)
+                break
+            except:
+                self.port += 1
+        # mock user credentials to always return the 'admin' user
+        self.server.getUserCredentials = lambda headers: ADMIN
+        # add the mocked handler
+        self.server.requestHandlers = {'api.dummy': self.handler}
+        # start the server on a separate thread
+        self.serverThread = threading.Thread(target = self.runServer)
+        self.serverThread.daemon = True
+        self.serverThread.start()
+        # check if the server is running after 0.5 seconds
+        time.sleep(0.5)
+        self.assertTrue(self.serverThread.isAlive(), 'Server failed to start')
+
+    def tearDown(self):
+        # change back to the original directory
+        os.chdir(self.directory)
+        # stop the server
+        self.server.shutdown()
+        # check if the server stopped after 0.5 seconds
+        time.sleep(0.5)
+        self.assertFalse(self.serverThread.isAlive(), 'Server thread is still alive after shutdown')
+        # delete http server test dir
+        if os.path.exists(HTTPSERVER_DIR):
+            shutil.rmtree(HTTPSERVER_DIR)
+
+    def test_fileHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_fileHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - AppAgent
+        Requirement: file handling in AppAgent http server
+        Action_To_Be_taken:
+            check if server dir is empty
+            create a new directory and 2 new files inside it
+            check if the files can be listed
+            check if the file contents are correct
+            delete the new directory
+            check if server dir is empty again
+        Expected_Result: pass
+        '''
+
+        # check if server dir is empty
+        files = [file['fileName'] for file in json.loads(self.sendRequest('/', 'LSDIR'))['fileList']]
+        self.assertEqual(0, len(files), 'Http server test directory should be empty initially')
+        # create a new directory and 2 new files inside it
+        self.sendRequest('/newdir', 'MKDIR')
+        self.sendRequest('/newdir/file1.txt', 'PUT', 'str1')
+        self.sendRequest('/newdir/file2.txt', 'PUT', 'str2')
+        # check if the files can be listed
+        files = [file['fileName'] for file in json.loads(self.sendRequest('/newdir', 'LSDIR'))['fileList']]
+        self.assertEqual(2, len(files), 'Two files should have been created')
+        self.assertIn('newdir/file1.txt', files)
+        self.assertIn('newdir/file2.txt', files)
+        # check if the file contents are correct
+        self.assertEqual('str1', self.sendRequest('/newdir/file1.txt'))
+        self.assertEqual('str2', self.sendRequest('/newdir/file2.txt'))
+        # delete the new directory
+        self.sendRequest('/newdir', 'RMDIR')
+        # check if server dir is empty again
+        files = [file['fileName'] for file in json.loads(self.sendRequest('/', 'LSDIR'))['fileList']]
+        self.assertEqual(0, len(files))
+
+    def test_proxy(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_proxy
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - AppAgent
+        Requirement: proxy request handling in AppAgent http server
+        Action_To_Be_taken:
+            check if server dir is empty
+            create a new directory and 2 new files inside it
+            check if the files can be listed
+            check if the file contents are correct
+            delete the new directory
+            check if server dir is empty again
+        Expected_Result: pass
+        '''
+
+        # check if server dir is empty
+        files = [file['fileName'] for file in json.loads(self.sendRequest(self.proxyEncode('/'), 'LSDIR'))['fileList']]
+        self.assertEqual(0, len(files), 'Http server test directory should be empty initially')
+        # create a new directory and 2 new files inside it
+        self.sendRequest(self.proxyEncode('/newdir'), 'MKDIR')
+        self.sendRequest(self.proxyEncode('/newdir/file1.txt'), 'PUT', 'str1')
+        self.sendRequest(self.proxyEncode('/newdir/file2.txt'), 'PUT', 'str2')
+        # check if the files can be listed
+        files = [file['fileName'] for file in json.loads(self.sendRequest(self.proxyEncode('/newdir'), 'LSDIR'))['fileList']]
+        self.assertEqual(2, len(files), 'Two files should have been created')
+        self.assertIn('newdir/file1.txt', files)
+        self.assertIn('newdir/file2.txt', files)
+        # check if the file contents are correct
+        self.assertEqual('str1', self.sendRequest(self.proxyEncode('/newdir/file1.txt')))
+        self.assertEqual('str2', self.sendRequest(self.proxyEncode('/newdir/file2.txt')))
+        # delete the new directory
+        self.sendRequest(self.proxyEncode('/newdir'), 'RMDIR')
+        # check if server dir is empty again
+        files = [file['fileName'] for file in json.loads(self.sendRequest(self.proxyEncode('/'), 'LSDIR'))['fileList']]
+        self.assertEqual(0, len(files))
+
+    def test_handlerHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_handlerHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - AppAgent
+        Requirement: microservice request handler handling in AppAgent http server
+        Action_To_Be_taken:
+            check requests and their responses
+            check a request through proxy
+            check handling a wrong request
+        Expected_Result: pass
+        '''
+
+        # check requests and their responses
+        self.assertEqual(json.loads(self.sendRequest('/api.dummy', 'POST', '{"requests": [{"getData": {"source": "Dummy_DS", "element": "int"}}]}'))['contentList'][0], self.handler.elements['int'])
+        self.assertEqual(json.loads(self.sendRequest('/api.dummy', 'POST', '{"requests": [{"getData": {"source": "Dummy_DS", "element": "string"}}]}'))['contentList'][0], self.handler.elements['string'])
+        self.assertEqual(json.loads(self.sendRequest('/api.dummy', 'POST', '{"requests": [{"getData": {"source": "Dummy_DS", "element": "list"}}]}'))['contentList'][0], self.handler.elements['list'])
+        # check a request through proxy
+        self.assertEqual(json.loads(self.sendRequest(self.proxyEncode('/api.dummy'), 'POST', '{"requests": [{"getData": {"source": "Dummy_DS", "element": "int"}}]}'))['contentList'][0], self.handler.elements['int'])
+        # check handling a wrong request
+        with self.assertRaises(urllib2.HTTPError) as context:
+            self.sendRequest('/api.notExistingApp', 'POST', 'dummy_body')
+        self.assertIn('404', str(context.exception))
+
+    def proxyEncode(self, path):
+        return '/proxy/' + ('http://localhost:' + str(self.port)).encode('hex') + path
+
+    def sendRequest(self, path, method = 'GET', body = None):
+        request = urllib2.Request('http://localhost:' + str(self.port) + path, body)
+        request.get_method = lambda: method
+        return urllib2.urlopen(request).read()
diff --git a/test/testcases/test_Authentication.py b/test/testcases/test_Authentication.py
new file mode 100644
index 0000000..f2376d9
--- /dev/null
+++ b/test/testcases/test_Authentication.py
@@ -0,0 +1,225 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, json
+import Microservices.Authentication
+from test.utils.Utils import *
+
+class AuthenticationHandlerTest(unittest.TestCase):
+
+    def setUp(self):
+        # the "microservice" under test
+        self.handler = Microservices.Authentication.createHandler('src/Microservices/Authentication')
+        # the datasource handlers of the microservice
+        self.datasourceHandlers = self.handler.getDataSourceHandlers()
+        # mocked usar "database"
+        self.handler._userHandler._userGroups = {
+            "users": {
+                "admin": [
+                    "admin"
+                ]
+            },
+            "groups": [
+                "admin"
+            ]
+        }
+        # we do not save the "database"
+        self.handler._userHandler._saveUserGroups = lambda *args: None
+
+    def tearDown(self):
+        # close tested "microservice"
+        self.handler.close()
+
+    def test_authentication(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_authentication
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Authentication
+        Requirement: authentication and session handling
+        Action_To_Be_taken:
+            check empty credentials if there is no cookie
+            login
+            check set cookie header
+            check user credentials with valid cookie
+            logout
+            check logout success
+            check user credentials with old cookie
+        Expected_Result: pass
+        '''
+
+        # check empty credentials if there is no cookie
+        credentials = self.handler.getUserCredentials({'cookie': None})
+        self.assertEqual(credentials, NO_USER)
+        # login
+        response = getEmptyResponse()
+        self.handler.handleMessage('POST', 'login/api.authenticate', {}, '{"username": "admin", "password": "admin"}', NO_USER, response)
+        # check set cookie header
+        self.assertEqual(200, response['returnCode'])
+        self.assertIn('Set-Cookie', response['headers'])
+        self.assertIn('PYSESSID=', response['headers']['Set-Cookie'])
+        self.assertIn('Path=/;', response['headers']['Set-Cookie'])
+        self.assertIn('expires=', response['headers']['Set-Cookie'])
+        # check user credentials with valid cookie
+        cookie = response['headers']['Set-Cookie'].split(';')[0] + ';'
+        credentials = self.handler.getUserCredentials({'cookie': cookie})
+        self.assertEqual(credentials['username'], 'admin')
+        self.assertEqual(credentials['password'], 'admin')
+        # logout
+        response = getEmptyResponse()
+        self.handler.handleMessage('POST', 'logout/api.authenticate', {'cookie': cookie}, '', credentials, response)
+        # check logout success
+        self.assertEqual(200, response['returnCode'])
+        self.assertIn('Set-Cookie', response['headers'])
+        self.assertIn('PYSESSID=', response['headers']['Set-Cookie'])
+        self.assertIn('Path=/;', response['headers']['Set-Cookie'])
+        self.assertIn('expires=Thu, 01 Jan 1970 00:00:00 UTC;', response['headers']['Set-Cookie'])
+        # check user credentials with old cookie
+        credentials = self.handler.getUserCredentials({'cookie': cookie})
+        self.assertEqual(credentials, NO_USER)
+
+    def test_userHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_userHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Authentication
+        Requirement: user handling
+        Action_To_Be_taken:
+            create 'test_group' group
+            check if 'test_group' exists
+            add 'test_user[1-3]' users to 'test_group'
+            check if the users are in 'test_group'
+            check the groups of 'test_user2'
+            remove 'test_user2' from the 'test_group'
+            check if 'test_user2' was removed from 'test_group'
+            check the groups of 'test_user2'
+            delete 'test_group'
+            check if 'test_group' was deleted
+        Expected_Result: pass
+        '''
+
+        # create 'test_group' group
+        self.set('AddGroup', 'test_group', 4)
+        # check if 'test_group' exists
+        groups = [element['node']['val'] for element in self.get('ListGroups')['list']]
+        self.assertIn('test_group', groups)
+        # add 'test_user[1-3]' users to 'test_group'
+        self.set('AddUserToGroup', 'test_user1', 4, None, 'test_group')
+        self.set('AddUserToGroup', 'test_user2', 4, None, 'test_group')
+        self.set('AddUserToGroup', 'test_user3', 4, None, 'test_group')
+        # check if the users are in 'test_group'
+        users = [element['node']['val'] for element in self.get('ListUsersInGroup', None, 'test_group')['list']]
+        self.assertIn('test_user1', users)
+        self.assertIn('test_user2', users)
+        self.assertIn('test_user3', users)
+        self.assertEqual(3, len(users))
+        # check the groups of 'test_user2'
+        groupsOfUser = [element['node']['val'] for element in self.get('ListGroupsOfUser', 'test_user2')['list']]
+        self.assertIn('test_group', groupsOfUser)
+        self.assertEqual(1, len(groupsOfUser))
+        # remove 'test_user2' from the 'test_group'
+        self.set('RemoveUserFromGroup', '', 1, 'test_user2', 'test_group')
+        # check if 'test_user2' was removed from 'test_group'
+        users = [element['node']['val'] for element in self.get('ListUsersInGroup', None, 'test_group')['list']]
+        self.assertIn('test_user1', users)
+        self.assertNotIn('test_user2', users)
+        self.assertIn('test_user3', users)
+        self.assertEqual(2, len(users))
+        # check the groups of 'test_user2'
+        groupsOfUser = [element['node']['val'] for element in self.get('ListGroupsOfUser', 'test_user2')['list']]
+        self.assertEqual(0, len(groupsOfUser))
+        # delete 'test_group'
+        self.set('RemoveGroup', '', 1, None, 'test_group')
+        # check if 'test_group' was deleted
+        groups = [element['node']['val'] for element in self.get('ListGroups')['list']]
+        self.assertNotIn('test_group', groups)
+        groupsOfUser = [element['node']['val'] for element in self.get('ListGroupsOfUser', 'test_user1')['list']]
+        self.assertEqual(0, len(groupsOfUser))
+
+    def test_groupHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_userHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Authentication
+        Requirement: user group handling
+        Action_To_Be_taken:
+            add 'test_group' group
+            add 'admin' user to the group
+            login as admin
+            check if credentials are correct
+        Expected_Result: pass
+        '''
+
+        # add 'test_group' group
+        self.set('AddGroup', 'test_group', 4)
+        # add 'admin' user to the group
+        self.set('AddUserToGroup', 'admin', 4, None, 'test_group')
+        # login as admin
+        response = getEmptyResponse()
+        self.handler.handleMessage('POST', 'login/api.authenticate', {}, '{"username": "admin", "password": "admin"}', NO_USER, response)
+        # check if credentials are correct
+        cookie = response['headers']['Set-Cookie'].split(';')[0] + ';'
+        credentials = self.handler.getUserCredentials({'cookie': cookie})
+        self.assertEqual(credentials['username'], 'admin')
+        self.assertEqual(credentials['password'], 'admin')
+        self.assertIn('test_group', credentials['groups'])
+
+    def test_help(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_help
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Authentication
+        Requirement: datasource help in Authentication
+        Action_To_Be_taken:
+            get help
+            check help syntax
+        Expected_Result: pass
+        '''
+
+        # get help
+        help = self.get('help')['node']['val']
+        # check help syntax
+        json.loads(help.decode('hex'))
+
+    def get(self, element, user = None, group = None):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': []
+        }
+        if user is not None:
+            request['params'].append({
+                'paramName': 'User',
+                'paramValue': user
+            })
+        if group is not None:
+            request['params'].append({
+                'paramName': 'Group',
+                'paramValue': group
+            })
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler'](request, ADMIN)
+
+    def set(self, element, content, tp, user = None, group = None):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': [],
+            'tp': tp,
+            'content': content
+        }
+        if user is not None:
+            request['params'].append({
+                'paramName': 'User',
+                'paramValue': user
+            })
+        if group is not None:
+            request['params'].append({
+                'paramName': 'Group',
+                'paramValue': group
+            })
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['setDataHandler'](request, ADMIN)
diff --git a/test/testcases/test_BaseGuiConfigHandler.py b/test/testcases/test_BaseGuiConfigHandler.py
new file mode 100644
index 0000000..c9e7393
--- /dev/null
+++ b/test/testcases/test_BaseGuiConfigHandler.py
@@ -0,0 +1,96 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, json
+from Common.BaseGuiConfigHandler import BaseGuiConfigHandler
+from test.utils.Utils import *
+
+class DummyGuiConfigHandler(BaseGuiConfigHandler):
+
+    def createConfig(self, userGroups):
+        apps = BaseGuiConfigHandler.createConfig(self, userGroups)
+        if self.hasAccessRight('b', userGroups):
+            apps['availableApps'].insert(0, {'directory': 'WebApplications/B'})
+        if self.hasAccessRight('a', userGroups):
+            apps['availableApps'].insert(0, {'directory': 'WebApplications/A'})
+        return apps
+
+
+class AuthenticationHandlerTest(unittest.TestCase):
+
+    def setUp(self):
+        # the "microservice" under test
+        self.handler = DummyGuiConfigHandler(GROUPRIGHTS_DIR)
+        # change default rights so we can test access right handling
+        self.handler._groupRightsHandler._default = DEFAULT_RIGHTS
+        # the datasource handlers of the microservice
+        self.datasourceHandlers = self.handler.getDataSourceHandlers()
+
+    def tearDown(self):
+        # close tested "microservice"
+        self.handler.close()
+
+    def test_loginPage(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_loginPage
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - BaseGuiConfigHandler
+        Requirement: gui config for login page
+        Action_To_Be_taken:
+            get gui config without login info
+            check login in returned config
+        Expected_Result: pass
+        '''
+
+        # get gui config without login info
+        response = getEmptyResponse()
+        self.handler.handleMessage('GET', 'CustomizableContent/MainConfig.json', {}, '', NO_USER, response)
+        config = json.loads(response['body'])
+        # check login in returned config
+        self.assertEqual(1, len(config['availableApps']))
+        self.assertEqual('Login', config['availableApps'][0]['name'])
+
+    def test_whileLoggedIn(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_whileLoggedIn
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - BaseGuiConfigHandler
+        Requirement: gui config for a logged in user
+        Action_To_Be_taken:
+            get gui config with login info
+            check 'A' application in returned config
+            check logout in returned config
+        Expected_Result: pass
+        '''
+
+        # get gui config with login info
+        response = getEmptyResponse()
+        self.handler.handleMessage('GET', 'CustomizableContent/MainConfig.json', {}, '', ADMIN, response)
+        config = json.loads(response['body'])
+        # check 'A' application in returned config
+        self.assertEqual(2, len(config['availableApps']))
+        self.assertEqual('WebApplications/A', config['availableApps'][0]['directory'])
+        # check logout in returned config
+        self.assertEqual('Logout', config['availableApps'][1]['name'])
+
+    def test_help(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_help
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - BaseGuiConfigHandler
+        Requirement: datasource help in BaseGuiConfigHandler
+        Action_To_Be_taken:
+            get help
+            check help syntax
+        Expected_Result: pass
+        '''
+
+        # get help
+        help = self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler']({'source': self.handler.SOURCE_ID, 'element': 'help', 'params': []}, ADMIN)['node']['val']
+        # check help syntax
+        json.loads(help.decode('hex'))
diff --git a/test/testcases/test_DataSource.py b/test/testcases/test_DataSource.py
new file mode 100644
index 0000000..1e22edd
--- /dev/null
+++ b/test/testcases/test_DataSource.py
@@ -0,0 +1,260 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, json
+import Microservices.DataSource
+from utils.MockedHandler import *
+from test.utils.Utils import *
+
+class DataSourceTest(unittest.TestCase):
+
+    def setUp(self):
+        # the "microservice" under test
+        self.handler = Microservices.DataSource.createHandler('src/Microservices/DataSource')
+        # a mocked microservice
+        self.mockedHandler = MockedDsRestApiHandler()
+        # the datasource handlers of the microservice
+        self.datasourceHandlers = self.handler.getDataSourceHandlers()
+        # setting the handlers of DataSource
+        handlers = {}
+        handlers.update(self.mockedHandler.getDataSourceHandlers())
+        handlers.update(self.datasourceHandlers)
+        self.handler.setHandlers(handlers)
+
+    def tearDown(self):
+        # close tested "microservice"
+        self.handler.close()
+
+    def test_builtInElements(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_builtInElements
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DataSource
+        Requirement: DataSource built in elements
+        Action_To_Be_taken:
+            check source id
+            check request response pairs
+        Expected_Result: pass
+        '''
+
+        # check source id
+        self.assertEqual('DataSource', self.handler.SOURCE_ID)
+
+        # check request response pairs: expected, element, param1, param2
+        self.checkElement('true', 'not', 'false')
+        self.checkElement('false', 'not', 'true')
+
+        self.checkElement('true', '==', '10.0', '10.0')
+        self.checkElement('false', '==', '20.0', '10.0')
+
+        self.checkElement('false', '!=', '10.0', '10.0')
+        self.checkElement('true', '!=', '321.0', '10.0')
+
+        self.checkElement('false', '>', '10.0', '20.0')
+        self.checkElement('true', '>', '20.0', '10.0')
+        self.checkElement('false', '>', '10.0', '10.0')
+
+        self.checkElement('false', '>=', '10.0', '20.0')
+        self.checkElement('true', '>=', '20.0', '10.0')
+        self.checkElement('true', '>=', '10.0', '10.0')
+
+        self.checkElement('true', '<', '10.0', '20.0')
+        self.checkElement('false', '<', '20.0', '10.0')
+        self.checkElement('false', '<', '10.0', '10.0')
+
+        self.checkElement('true', '<=', '10.0', '20.0')
+        self.checkElement('false', '<=', '20.0', '10.0')
+        self.checkElement('true', '<=', '10.0', '10.0')
+
+        self.checkElement('true', 'and', 'true', 'true')
+        self.checkElement('false', 'and', 'true', 'false')
+        self.checkElement('false', 'and', 'false', 'true')
+        self.checkElement('false', 'and', 'false', 'false')
+
+        self.checkElement('true', 'or', 'true', 'true')
+        self.checkElement('true', 'or', 'true', 'false')
+        self.checkElement('true', 'or', 'false', 'true')
+        self.checkElement('false', 'or', 'false', 'false')
+
+        self.checkElement('true', 'match', 'apple', 'a*l?')
+        self.checkElement('false', 'match', 'apple', 'p*')
+
+        self.checkElement('false', 'not match', 'apple', 'a*l?')
+        self.checkElement('true', 'not match', 'apple', 'p*')
+
+        self.checkElement('6.0', 'sum', '["1", "2", "3"]')
+        self.checkElement('10.0', 'sum', '["5.0", "5.0"]')
+
+        self.checkElement('true', 'exists', '["false", "true", "false"]')
+        self.checkElement('false', 'exists', '["false", "false", "false"]')
+
+        self.checkElement('false', 'forAll', '["true", "false", "true"]')
+        self.checkElement('true', 'forAll', '["true", "true", "true"]')
+
+        response = self.get('Sources')
+        sources = [element['node']['val'] for element in response['list']]
+        self.assertEqual(2, len(sources))
+        self.assertIn(self.handler.SOURCE_ID, sources)
+        self.assertIn(self.mockedHandler.SOURCE_ID, sources)
+
+        self.checkEncodedRequest('2', 'sizeOf', 'Sources')
+        self.checkEncodedRequest('1', 'sizeOf', 'help')
+        self.checkEncodedRequest('true', 'dataElementPresent', 'not', 'Par1', 'false')
+        self.checkEncodedRequest('false', 'dataElementPresent', 'NoElement')
+
+
+    def test_externalElements(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_builtInElements
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DataSource
+        Requirement: handling external elements in DataSource
+        Action_To_Be_taken:
+            get element and check result
+        Expected_Result: pass
+        '''
+
+        response = self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler']({'source': 'Dummy_DS', 'element': 'string', 'params': []}, ADMIN)
+        self.assertEqual(response, self.mockedHandler.elements['string'])
+
+        response = self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler']({'source': 'NoSource', 'element': 'SomeElement', 'params': []}, ADMIN)
+        self.assertIsNone(response)
+
+    def test_setDataHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_setDataHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DataSource
+        Requirement: handling setData requests in DataSource
+        Action_To_Be_taken:
+            set element
+            check result
+        Expected_Result: pass
+        '''
+
+        self.datasourceHandlers[self.handler.SOURCE_ID]['setDataHandler']({'source': 'Dummy_DS', 'element': 'string', 'params': [], 'content': 'newstr', 'tp': 4, 'indxsInList': []}, ADMIN)
+        response = self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler']({'source': 'Dummy_DS', 'element': 'string', 'params': []}, ADMIN)
+        self.assertEqual(response['node']['val'], 'newstr')
+
+        response = self.datasourceHandlers[self.handler.SOURCE_ID]['setDataHandler']({'source': 'DataSource', 'element': 'Sources', 'params': []}, ADMIN)
+        self.assertIsNone(response)
+
+    def test_microService(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_microService
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DataSource
+        Requirement: DataSource behaving as a microservice
+        Action_To_Be_taken:
+            check api extension
+            send request
+            check response
+        Expected_Result: pass
+        '''
+
+        # check api extension
+        self.assertEqual('api.appagent', Microservices.DataSource.EXTENSION)
+        # send request
+        response = getEmptyResponse()
+        self.handler.handleMessage('POST', 'api.appagent', {}, '''{
+            "requests": [
+                {
+                    "getData": {
+                        "source": "DataSource",
+                        "element": "Sources"
+                    }
+                }
+            ]
+        }''', ADMIN, response)
+        # check response
+        sources = [element['node']['val'] for element in json.loads(response['body'])['contentList'][0]['list']]
+        self.assertEqual(2, len(sources))
+
+    def test_help(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_help
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DataSource
+        Requirement: DataSource help
+        Action_To_Be_taken:
+            get help
+            check help syntax
+            check sources
+        Expected_Result: pass
+        '''
+
+        # get help
+        help = self.get('help')['node']['val']
+        # check help syntax
+        help = json.loads(help.decode('hex'))
+        # check sources
+        sources = [element['source'] for element in help['sources']]
+        self.assertEqual(2, len(sources))
+        self.assertIn('DataSource', sources)
+        self.assertIn('Dummy_DS', sources)
+
+    def checkEncodedRequest(self, expected, element, originalElement, paramName = None, paramValue = None):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': [
+                {
+                    'paramName': 'Source',
+                    'paramValue': 'DataSource'
+                },
+                {
+                    'paramName': 'Element',
+                    'paramValue': originalElement
+                }
+            ]
+        }
+        if paramName is not None and paramValue is not None:
+            request['params'].append({
+                'paramName': 'ParamName',
+                'paramValue': paramName
+            })
+            request['params'].append({
+                'paramName': 'ParamValue',
+                'paramValue': paramValue
+            })
+        response = self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler'](request, ADMIN)
+        self.assertEqual(response['node']['val'], expected)
+
+    def checkElement(self, expected, element, par1 = None, par2 = None):
+        response = self.get(element, par1, par2)
+        self.assertEqual(response['node']['val'], expected)
+
+    def get(self, element, par1 = None, par2 = None):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': []
+        }
+        if par1 is not None:
+            request['params'].append({
+                'paramName': 'Par1',
+                'paramValue': par1
+            })
+        if par2 is not None:
+            request['params'].append({
+                'paramName': 'Par2',
+                'paramValue': par2
+            })
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler'](request, ADMIN)
+
+    def set(self, element, content, tp):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': [],
+            'tp': tp,
+            'content': content
+        }
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['setDataHandler'](request, ADMIN)
diff --git a/test/testcases/test_DsRestAPI.py b/test/testcases/test_DsRestAPI.py
new file mode 100644
index 0000000..4497377
--- /dev/null
+++ b/test/testcases/test_DsRestAPI.py
@@ -0,0 +1,151 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest
+from Common.DsRestAPI import DsRestAPI
+from utils.MockedHandler import *
+
+# basic requests
+INT_REQUEST = {"requests": [{"getData": {"source": "dummy", "element": "int"}}]}
+STRING_REQUEST = {"requests": [{"getData": {"source": "dummy", "element": "string"}}]}
+LIST_REQUEST = {"requests": [{"getData": {"source": "dummy", "element": "list"}}]}
+LIST_WITH_CHILDREN = {"requests": [{"getData": {"source": "dummy", "element": "list", "children": [{"getData": {"source": "dummy", "element": "stringOfList", "params": [{"paramName": "dummyParam", "paramValue": "%Parent0%"}]}}]}}]}
+LIST_WITH_SELECTION = {"requests": [{"getData": {"source": "dummy", "element": "list", "selection": [1], "children": [{"getData": {"source": "dummy", "element": "stringOfList", "params": [{"paramName": "dummyParam", "paramValue": "%Parent0%"}]}}]}}]}
+NODE_WITH_CHILDREN = {"requests": [{"getData": {"source": "dummy", "element": "int", "children": [{"getData": {"source": "dummy", "element": "string"}}]}}]}
+# prefilters
+INT_REQUEST_CTRUE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"dataValue": "true"}}}]}
+INT_REQUEST_CFALSE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"dataValue": "false"}}}]}
+INT_REQUEST_TRUE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"request": {"source": "dummy", "element": "true"}}}}]}
+INT_REQUEST_FALSE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"request": {"source": "dummy", "element": "false"}}}}]}
+INT_REQUEST_PARAM_TRUE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"request": {"source": "dummy", "element": "boolOfParam", "params": [{"paramName": "dummyParam", "paramValue": {"request": {"source": "dummy", "element": "int"}}}]}}}}]}
+INT_REQUEST_PARAM_FALSE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"request": {"source": "dummy", "element": "boolOfParam", "params": [{"paramName": "dummyParam", "paramValue": {"request": {"source": "dummy", "element": "string"}}}]}}}}]}
+INT_REQUEST_REMAP_TRUE = {"requests": [{"getData": {"source": "dummy", "element": "int", "filter": {"request": {"source": "dummy", "element": "false", "remapTo": {"request": {"source": "dummy", "element": "true"}}}}}}]}
+# postfilter
+NODE_REQUEST_PARENT_FALSE = {"requests": [{"getData": {"source": "dummy", "element": "false", "filter": {"dataValue": "%Parent0%"}}}]}
+NODE_REQUEST_PARENT_TRUE = {"requests": [{"getData": {"source": "dummy", "element": "true", "filter": {"dataValue": "%Parent0%"}}}]}
+LIST_REQUEST_FILTERED = {"requests": [{"getData": {"source": "dummy", "element": "list", "children": [{"getData": {"source": "dummy", "element": "stringOfList", "params": [{"paramName": "dummyParam", "paramValue": "%Parent0%"}]}}], "filter": {"request": {"source": "dummy", "element": "boolOfList", "params": [{"paramName": "dummyParam", "paramValue": {"dataValue": "%Parent0%"}}]}}}}]}
+# set requests
+INT_REQUEST_SET = {"requests": [{"setData": {"source": "dummy", "element": "int", "tp": 1, "content": "-1"}}]}
+LIST_ELEMENT_SET = {"requests": [{"setData": {"source": "dummy", "element": "stringOfList", "tp": 4, "content": "other_string", "params": [{"paramName": "dummyParam", "paramValue": "0"}]}}]}
+LIST_SET = {"requests": [{"setData": {"source": "dummy", "element": "list", "tp": 8, "content": "-1", "indxsInList": [1]}}]}
+# rangefilter
+LIST_REQUEST_WITH_RANGEFILTER = {"requests": [{"getData": {"source": "dummy", "element": "list", "rangeFilter": {"offset": 1, "count": 1}}}]}
+
+class DsRestAPITest(unittest.TestCase):
+
+    def setUp(self):
+        # a mocked microservice
+        self.mockedHandler = MockedDsRestApiHandler()
+        # the "feature" under test
+        self.dsRestAPI = DsRestAPI(self.mockedHandler.mockedGetDataHandler, self.mockedHandler.mockedSetDataHandler)
+
+    def test_getData(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_getData
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DsRestAPI
+        Requirement: getData request handling
+        Action_To_Be_taken:
+            process requests and check responses
+        Expected_Result: pass
+        '''
+
+        # basic
+        self.sendRequestAndCompare(INT_REQUEST, self.mockedHandler.elements['int'])
+        self.sendRequestAndCompare(STRING_REQUEST, self.mockedHandler.elements['string'])
+
+        # user credentials
+        self.sendRequestAndCompare(INT_REQUEST, self.mockedHandler.elements['userError'], WRONG_USER)
+
+        # prefilter
+        self.sendRequestAndCompare(INT_REQUEST_CTRUE, self.mockedHandler.elements['int'])
+        self.sendRequestAndCompare(INT_REQUEST_CFALSE, self.mockedHandler.elements['filteredItem'])
+        self.sendRequestAndCompare(INT_REQUEST_TRUE, self.mockedHandler.elements['int'])
+        self.sendRequestAndCompare(INT_REQUEST_FALSE, self.mockedHandler.elements['filteredItem'])
+        self.sendRequestAndCompare(INT_REQUEST_PARAM_TRUE, self.mockedHandler.elements['int'])
+        self.sendRequestAndCompare(INT_REQUEST_PARAM_FALSE, self.mockedHandler.elements['filteredItem'])
+        self.sendRequestAndCompare(INT_REQUEST_REMAP_TRUE, self.mockedHandler.elements['int'])
+
+        # node with children
+        response = self.dsRestAPI.parseRequest(NODE_WITH_CHILDREN, ADMIN)
+        self.assertEqual(response['contentList'][0]['node']['val'], self.mockedHandler.elements['int']['node']['val'])
+        self.assertEqual(response['contentList'][0]['node']['childVals'][0], self.mockedHandler.elements['string'])
+
+        # list with children
+        response = self.dsRestAPI.parseRequest(LIST_WITH_CHILDREN, ADMIN)
+        self.assertEqual(len(response['contentList'][0]['list']), 3)
+        for index, element in enumerate(response['contentList'][0]['list']):
+            self.assertEqual(element['node']['val'], str(index))
+            self.assertEqual(len(element['node']['childVals']), 1)
+            self.assertEqual(element['node']['childVals'][0]['node']['val'], 'string_' + str(index))
+
+        # list with selection
+        response = self.dsRestAPI.parseRequest(LIST_WITH_SELECTION, ADMIN)
+        self.assertEqual(len(response['contentList'][0]['list']), 3)
+        self.assertNotIn('childVals', response['contentList'][0]['list'][0]['node'])
+        self.assertEqual('string_1',  response['contentList'][0]['list'][1]['node']['childVals'][0]['node']['val'])
+        self.assertNotIn('childVals', response['contentList'][0]['list'][2]['node'])
+
+        # postfilter
+        response = self.dsRestAPI.parseRequest(LIST_REQUEST_FILTERED, ADMIN)
+        self.assertEqual(len(response['contentList'][0]['list']), 2)
+        for index, element in enumerate(response['contentList'][0]['list']):
+            self.assertEqual(element['node']['val'], str(index))
+            self.assertEqual(len(element['node']['childVals']), 1)
+            self.assertEqual(element['node']['childVals'][0]['node']['val'], 'string_' + str(index))
+
+        self.sendRequestAndCompare(NODE_REQUEST_PARENT_TRUE, self.mockedHandler.elements['true'])
+        response = self.dsRestAPI.parseRequest(NODE_REQUEST_PARENT_FALSE, ADMIN)
+        self.assertEqual(response['contentList'][0]['node']['val'], '')
+        self.assertEqual(response['contentList'][0]['node']['tp'], 0)
+
+        # rangefilter
+        response = self.dsRestAPI.parseRequest(LIST_REQUEST_WITH_RANGEFILTER, ADMIN)
+        self.assertEqual(len(response['contentList'][0]['list']), 1)
+        self.assertEqual(response['contentList'][0]['list'][0]['node']['val'], '1')
+
+    def test_setData(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_setData
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - DsRestAPI
+        Requirement: setData request handling
+        Action_To_Be_taken:
+            process requests and check responses
+        Expected_Result: pass
+        '''
+
+        # user credentials
+        self.dsRestAPI.parseRequest(INT_REQUEST_SET, WRONG_USER)
+        response = self.dsRestAPI.parseRequest(INT_REQUEST, ADMIN)
+        self.assertEqual(response['contentList'][0]['node']['val'], "1")
+
+        # simple set
+        self.dsRestAPI.parseRequest(INT_REQUEST_SET, ADMIN)
+        response = self.dsRestAPI.parseRequest(INT_REQUEST, ADMIN)
+        self.assertEqual(response['contentList'][0]['node']['val'], "-1")
+
+        # set with params
+        self.dsRestAPI.parseRequest(LIST_ELEMENT_SET, ADMIN)
+        response = self.dsRestAPI.parseRequest(LIST_WITH_CHILDREN, ADMIN)
+        self.assertEqual(response['contentList'][0]['list'][0]['node']['val'], "0")
+        self.assertEqual(response['contentList'][0]['list'][1]['node']['val'], "1")
+        self.assertEqual(response['contentList'][0]['list'][2]['node']['val'], "2")
+        self.assertEqual(response['contentList'][0]['list'][0]['node']['childVals'][0]['node']['val'], "other_string")
+        self.assertEqual(response['contentList'][0]['list'][1]['node']['childVals'][0]['node']['val'], "string_1")
+        self.assertEqual(response['contentList'][0]['list'][2]['node']['childVals'][0]['node']['val'], "string_2")
+
+        # set list element
+        self.dsRestAPI.parseRequest(LIST_SET, ADMIN)
+        response = self.dsRestAPI.parseRequest(LIST_REQUEST, ADMIN)
+        self.assertEqual(response['contentList'][0]['list'][0]['node']['val'], "0")
+        self.assertEqual(response['contentList'][0]['list'][1]['node']['val'], "-1")
+        self.assertEqual(response['contentList'][0]['list'][2]['node']['val'], "2")
+
+    def sendRequestAndCompare(self, request, expected, userCredentials = ADMIN):
+        response = self.dsRestAPI.parseRequest(request, userCredentials)
+        self.assertEqual(response['contentList'][0], expected)
diff --git a/test/testcases/test_EditableGroupRights.py b/test/testcases/test_EditableGroupRights.py
new file mode 100644
index 0000000..0141bf6
--- /dev/null
+++ b/test/testcases/test_EditableGroupRights.py
@@ -0,0 +1,92 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, json
+from Common.EditableGroupRights import EditableGroupRights
+from test.utils.Utils import *
+
+class EditableGroupRightsTest(unittest.TestCase):
+
+    def setUp(self):
+        # the "feature" under test
+        self.handler = EditableGroupRights(GROUPRIGHTS_DIR, DEFAULT_RIGHTS)
+        # we do not save the "database"
+        self.handler._saveGroupRights = lambda *args: None
+
+    def test_handlingGroupRights(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_handlingGroupRights
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - EditableGroupRights
+        Requirement: group rights handling in ServiceFramework
+        Action_To_Be_taken:
+            check group rigths schema
+            set group rigths of group 'test_group1' and 'test_group2'
+            check group rigths of 'test_group1' via function call
+            check group rigths of 'test_group1' via api call
+            check handled groups
+            delete 'test_group1' rights
+            check group rights of 'test_group1'
+            check handled groups
+        Expected_Result: pass
+        '''
+
+        # check group rigths schema
+        response = self.get('GroupRightsSchema')
+        schema = json.loads(response['node']['val'])
+        self.assertIn('$schema', schema)
+        self.assertEqual('array', schema['type'])
+        self.assertEqual('string', schema['items']['type'])
+        self.assertEqual(3, len(schema['items']['enum']))
+        # set group rigths of group 'test_group1' and 'test_group2'
+        self.set('GroupRights', '["a", "b", "c"]', 'test_group1')
+        self.set('GroupRights', '["a", "b"]', 'test_group2')
+        # check group rigths of 'test_group1' via function call
+        rights = self.handler.getGroupRights('test_group1')
+        self.assertEqual(3, len(rights))
+        self.assertIn('a', rights)
+        self.assertIn('b', rights)
+        self.assertIn('c', rights)
+        # check group rigths of 'test_group1' via api call
+        rights = json.loads(self.get('GroupRights', 'test_group1')['node']['val'])
+        self.assertEqual(3, len(rights))
+        self.assertIn('a', rights)
+        self.assertIn('b', rights)
+        self.assertIn('c', rights)
+        # check handled groups
+        groups = [element['node']['val'] for element in self.get('HandledGroups', 'test_group1')['list']]
+        self.assertEqual(2, len(groups))
+        self.assertIn('test_group1', groups)
+        self.assertIn('test_group2', groups)
+        # delete 'test_group1' rights
+        self.set('GroupRights', 'null', 'test_group1')
+        # check group rights of 'test_group1'
+        rights = self.handler.getGroupRights('test_group1')
+        self.assertEqual(1, len(rights))
+        self.assertIn('a', rights)
+        # check handled groups
+        groups = [element['node']['val'] for element in self.get('HandledGroups', 'test_group1')['list']]
+        self.assertEqual(1, len(groups))
+        self.assertIn('test_group2', groups)
+
+    def get(self, element, group = None):
+        params = []
+        if group is not None:
+            params.append({
+                'paramName': 'Group',
+                'paramValue': group
+            })
+        return self.handler.handleGetData(element, params, ADMIN)
+
+    def set(self, element, content, group = None):
+        params = []
+        if group is not None:
+            params.append({
+                'paramName': 'Group',
+                'paramValue': group
+            })
+        return self.handler.handleSetData(element, params, content, ADMIN)
+
diff --git a/test/testcases/test_Playlist.py b/test/testcases/test_Playlist.py
new file mode 100644
index 0000000..c8db916
--- /dev/null
+++ b/test/testcases/test_Playlist.py
@@ -0,0 +1,514 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, json, time, os
+import Microservices.Playlist
+from Microservices.Playlist.ScheduledPlaylist import ScheduledPlaylist
+from test.utils.Utils import *
+
+def loadPlaylist(path):
+    with open(path, 'r') as f:
+        playlist = json.load(f)
+    return playlist
+
+class ScheduledPlaylistTest(unittest.TestCase):
+
+    def setUp(self):
+        self.next = 0
+        self.failed = False
+        self.reason = ''
+
+    def test_requestHandlingWithConditionTrue(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_requestHandlingWithConditionTrue
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            execute the playlist and check expected behaviour
+        Expected_Result: pass
+        '''
+
+        self.expected = (
+            (
+                lambda type, url, credentials: (type, url, credentials['username']),
+                ('LOGIN', 'http://localhost:8000/api.appagent', 'admin'),
+                None
+            ),
+            (
+                lambda type, url, requests: (type, url, requests[0]['getData']['filter']),
+                ('REQUEST', 'http://localhost:8000/api.appagent', {'dataValue': 'false'}),
+                {'contentList': [{'node': {'val': '1', 'tp': 1}}]}
+            ),
+            (
+                lambda type, amount: (type, amount),
+                ('WAIT', 0.2),
+                None
+            ),
+            (
+                lambda type, url, requests: (type, url, requests[0]['setData']['element']),
+                ('REQUEST', 'http://localhost:8000/api.appagent', 'string'),
+                {'contentList': [{'node': {'val': '1', 'tp': 1}}]}
+            ),
+            (
+                lambda type: type,
+                'LOGOUT',
+                None
+            )
+        )
+
+        self.runPlaylist(PLAYLIST_DIR + '/Playlists/admin/simple.json', 'blue')
+
+    def test_relativeTo(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_relativeTo
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            execute the playlist and check expected behaviour
+        Expected_Result: pass
+        '''
+
+        self.expected = (
+            (
+                lambda type, url, credentials: (type, url, credentials['username']),
+                ('LOGIN', 'http://localhost:8000/api.appagent', 'admin'),
+                None
+            ),
+            (
+                lambda type, url, requests: (time.sleep(0.1), requests[0]['setData']['content']),
+                (None, 'new_str1'),
+                {'contentList': [{'node': {'val': '1', 'tp': 1}}]}
+            ),
+            (
+                lambda type, url, requests: (time.sleep(0.1), requests[0]['setData']['content']),
+                (None, 'new_str2'),
+                {'contentList': [{'node': {'val': '1', 'tp': 1}}]}
+            ),
+            (
+                lambda type, url, requests: (time.sleep(0.1), requests[0]['setData']['content']),
+                (None, 'new_str3'),
+                {'contentList': [{'node': {'val': '1', 'tp': 1}}]}
+            ),
+            (
+                lambda type: type,
+                'LOGOUT',
+                None
+            )
+        )
+
+        self.runPlaylist(PLAYLIST_DIR + '/Playlists/admin/relativeto.json', 'blue')
+
+    def test_conditionMaxTriesExceeded(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_conditionMaxTriesExceeded
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            execute the playlist and check expected behaviour
+        Expected_Result: pass
+        '''
+
+        self.expected = (
+            (
+                lambda type, url, credentials: (type, url, credentials['username']),
+                ('LOGIN', 'http://localhost:8000/api.appagent', 'admin'),
+                None
+            ),
+            (
+                lambda type, url, requests: (type, url, requests[0]['getData']['filter']),
+                ('REQUEST', 'http://localhost:8000/api.appagent', {'dataValue': 'false'}),
+                {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+            ),
+            (
+                lambda type, amount: (type, amount),
+                ('WAIT', 0.1),
+                None
+            ),
+            (
+                lambda type, url, requests: (type, url, requests[0]['getData']['filter']),
+                ('REQUEST', 'http://localhost:8000/api.appagent', {'dataValue': 'false'}),
+                {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+            ),
+            (
+                lambda type, amount: (type, amount),
+                ('WAIT', 0.1),
+                None
+            ),
+            (
+                lambda type, url, requests: (type, url, requests[0]['getData']['filter']),
+                ('REQUEST', 'http://localhost:8000/api.appagent', {'dataValue': 'false'}),
+                {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+            ),
+            (
+                lambda type: type,
+                'LOGOUT',
+                None
+            )
+        )
+
+        self.runPlaylist(PLAYLIST_DIR + '/Playlists/admin/try.json', 'red')
+
+    def test_conditionsMaxTimeExceeded(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_conditionsMaxTimeExceeded
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            execute the playlist and check expected behaviour
+        Expected_Result: pass
+        '''
+
+        self.expected = (
+            (
+                lambda type, url, credentials: (type, url, credentials['username']),
+                ('LOGIN', 'http://localhost:8000/api.appagent', 'admin'),
+                None
+            ),
+            (
+                lambda type, url, requests: time.sleep(0.5),
+                None,
+                {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+            ),
+            (
+                lambda type: type,
+                'LOGOUT',
+                None
+            )
+        )
+
+        self.runPlaylist(PLAYLIST_DIR + '/Playlists/admin/wait.json', 'red')
+
+    def test_stop(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_stop
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            execute the playlist and check expected behaviour
+        Expected_Result: pass
+        '''
+
+        self.expected = (
+            (
+                lambda type, url, credentials: (type, url, credentials['username']),
+                ('LOGIN', 'http://localhost:8000/api.appagent', 'admin'),
+                None
+            ),
+            (
+                lambda type, url, requests: self.handler.stop(),
+                None,
+                {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+            ),
+            (
+                lambda type: type,
+                'LOGOUT',
+                None
+            )
+        )
+
+        self.runPlaylist(PLAYLIST_DIR + '/Playlists/admin/relativeto.json', 'black')
+
+    def test_pause(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_pause
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - ScheduledPlaylist
+        Requirement: playlist execution
+        Action_To_Be_taken:
+            start the playlist
+            pause the playlist
+            check if the playlist was paused
+            resume the playlist
+            check if it was resumed
+            stop the playlist
+        Expected_Result: pass
+        '''
+
+        # we count the number of 'actions'; when paused, the counter should not change
+        count = [0] # we can't modify an outer variable directly...
+
+        def magicHandler(*args):
+            count[0] += 1
+            time.sleep(0.1)
+            return {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+
+        # start the playlist
+        self.handler = ScheduledPlaylist(loadPlaylist(PLAYLIST_DIR + '/Playlists/admin/longtry.json'))
+        self.handler._login = magicHandler
+        self.handler._logout = magicHandler
+        self.handler._wait = magicHandler
+        self.handler._sendRequestsToUrl = magicHandler
+        self.handler.start(ADMIN)
+
+        # pause the playlist
+        time.sleep(0.5)
+        self.handler.pause()
+        # check if the playlist was paused
+        time.sleep(0.2)
+        currentCount = count[0]
+        time.sleep(0.5)
+        self.assertEqual(count[0], currentCount, 'there should be no calls when paused') # there were no additional calls
+        # resume the playlist
+        self.handler.pause()
+        # check if it was resumed
+        time.sleep(0.5)
+        self.assertNotEqual(count[0], currentCount, 'there should be calls when unpaused') # there were calls after unpause
+        # stop the playlist
+        self.handler.stop()
+        time.sleep(0.2)
+
+    def startPlaylist(self, fileName):
+        self.handler = ScheduledPlaylist(loadPlaylist(fileName))
+        self.handler._login = self.mockedLogin
+        self.handler._logout = self.mockedLogout
+        self.handler._wait = self.mockedWait
+        self.handler._sendRequestsToUrl = self.mockedSendRequest
+        self.handler.start(ADMIN)
+
+    def runPlaylist(self, fileName, color, timeout = 1):
+        self.startPlaylist(fileName)
+        startTime = time.time()
+        while self.handler.isRunning():
+            if time.time() - startTime > timeout:
+                self.fail('Playlist execution timed out')
+            time.sleep(0.1)
+        while color not in self.handler.getStatus()['node']['val']:
+            if time.time() - startTime > timeout:
+                self.fail('Playlist status is wrong, expected ' + color + ', got ' + self.handler.getStatus()['node']['val'])
+            time.sleep(0.1)
+        if self.failed:
+            self.fail(self.reason)
+
+    def handle(self, *args):
+        time.sleep(0.1) # this is needed to imitate non-blocking behaviour (this is the only way we can check the status led color reliably)
+        if self.next >= len(self.expected):
+            self.failed = True
+            self.reason = 'Too few expected values: ' + str(len(self.expected))
+            return None
+        if len(self.expected[self.next]) < 3:
+            self.failed = True
+            self.reason = 'Not all 3 arguments present, only got: ' + str(len(self.expected[self.next]))
+            return None
+        transformer, expected, returnValue = self.expected[self.next]
+        self.next += 1
+        try:
+            self.assertEqual(transformer(*args), expected)
+        except Exception as e:
+            self.failed = True
+            self.reason = e
+        return returnValue
+
+    def mockedLogin(self, opener, url, userCredentials):
+        return self.handle('LOGIN', url, userCredentials)
+
+    def mockedLogout(self):
+        return self.handle('LOGOUT')
+
+    def mockedWait(self, amount):
+        return self.handle('WAIT', amount)
+
+    def mockedSendRequest(self, opener, url, requests):
+        return self.handle('REQUEST', url, requests)
+
+class PlaylistTest(unittest.TestCase):
+
+    def magicHandler(self, *args):
+        # a function that mocks parts of the ScheduledPlaylist
+        time.sleep(0.1)
+        return {'contentList': [{'node': {'val': '', 'tp': 0}}]}
+
+    def createPlaylistExecutor(self, descriptor):
+        # create a mocked ScheduledPlaylist
+        executor = ScheduledPlaylist(descriptor)
+        executor._login = self.magicHandler
+        executor._logout = self.magicHandler
+        executor._wait = self.magicHandler
+        executor._sendRequestsToUrl = self.magicHandler
+        return executor
+
+    def setUp(self):
+        # the "microservice" under test
+        self.handler = Microservices.Playlist.createHandler(PLAYLIST_DIR)
+        # the datasource handlers of the microservice
+        self.datasourceHandlers = self.handler.getDataSourceHandlers()
+        # mock the creation of ScheduledPlaylist
+        self.handler._createPlaylistExecutor = self.createPlaylistExecutor
+        # a list of created files to delete at the end
+        self.created = []
+
+    def tearDown(self):
+        # close tested "microservice"
+        self.handler.close()
+        # delete the created files
+        for fileName in self.created:
+            if os.path.exists(fileName):
+                os.unlink(fileName)
+
+    def test_playlistHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_playlistHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Playlist
+        Requirement: playlist microservice request handling
+        Action_To_Be_taken:
+            start a playlist
+            check elements of a running and an idle playlist
+            pause the running playlist
+            check status led
+            resume playlist
+            check status led
+            stop playlist
+            check status led
+        Expected_Result: pass
+        '''
+
+        # start a playlist
+        self.set('Start', '0', 1, 'admin/longtry')
+        time.sleep(0.2)
+        # check elements of a running and an idle playlist
+        self.check('0', 'Start', 'admin/longtry')
+        self.check('0', 'Start', 'admin/try')
+        self.check('0', 'Stop', 'admin/longtry')
+        self.check('0', 'Stop', 'admin/try')
+        self.check('0', 'Pause', 'admin/longtry')
+        self.check('0', 'Pause', 'admin/try')
+        self.check('0', 'Start', 'admin/longtry')
+        self.check('0', 'Start', 'admin/try')
+        self.check('green', 'Status', 'admin/longtry')
+        self.check('blue', 'Status', 'admin/try')
+        # pause the running playlist
+        self.set('Pause', '0', 1, 'admin/longtry')
+        time.sleep(0.2)
+        # check status led
+        self.check('yellow', 'Status', 'admin/longtry')
+        # resume playlist
+        self.set('Pause', '0', 1, 'admin/longtry')
+        time.sleep(0.2)
+        # check status led
+        self.check('green', 'Status', 'admin/longtry')
+        # stop playlist
+        self.set('Stop', '0', 1, 'admin/longtry')
+        time.sleep(0.2)
+        # check status led
+        self.check('black', 'Status', 'admin/longtry')
+
+    def test_playlistFileHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_playlistFileHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Playlist
+        Requirement: playlist microservice file handling
+        Action_To_Be_taken:
+            create a playlist
+            check if it was created
+            check if the descriptor is the same as the one that was saved
+            delete the playlist
+            check that it no longer exists
+        Expected_Result: pass
+        '''
+
+        # create a playlist
+        self.created.append(PLAYLIST_DIR + '/Playlists/admin/test.json')
+        self.set('Descriptor', '[]', 4, 'admin/test')
+        # check if it was created
+        self.assertIn('admin/test', self.makeList(self.get('Playlists')))
+        # check if the descriptor is the same as the one that was saved
+        self.check('[]', 'Descriptor', 'admin/test')
+        # delete the playlist
+        self.set('Delete', '1', 1, 'admin/test')
+        # check that it no longer exists
+        self.assertNotIn('admin/test', self.makeList(self.get('Playlists')))
+
+    def test_microService(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_microService
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Playlist
+        Requirement: playlist behaviour as a microservice
+        Action_To_Be_taken:
+            invoke the request handler with a simple request and check the result
+        Expected_Result: pass
+        '''
+
+        response = getEmptyResponse()
+        self.handler.handleMessage('POST', 'api.playlist', {}, '''{
+            "requests": [
+                {
+                    "getData": {
+                        "source": "Playlist",
+                        "element": "Playlists"
+                    }
+                }
+            ]
+        }''', ADMIN, response)
+        playlists = self.makeList(json.loads(response['body'])['contentList'][0])
+        self.assertIn('admin/longtry', playlists)
+
+    def test_help(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_help
+        Tested component: ServiceFramework
+        Feature: ServiceFramework - Playlist
+        Requirement: datasource help in Playlist
+        Action_To_Be_taken:
+            get help
+            check help syntax
+        Expected_Result: pass
+        '''
+
+        # get help
+        help = self.get('help')['node']['val']
+        # check help syntax
+        json.loads(help.decode('hex'))
+
+    def makeList(self, response):
+        return [element['node']['val'] for element in response['list']]
+
+    def check(self, expected, element, playlist = None, user = ADMIN):
+        response = self.get(element, playlist, user)
+        self.assertIn(expected, response['node']['val'])
+
+    def get(self, element, playlist = None, user = ADMIN):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': []
+        }
+        if playlist is not None:
+            request['params'].append({
+                'paramName': 'Playlist',
+                'paramValue': playlist
+            })
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['getDataHandler'](request, user)
+
+    def set(self, element, content, tp, playlist = None, user = ADMIN):
+        request = {
+            'source': self.handler.SOURCE_ID,
+            'element': element,
+            'params': [],
+            'tp': tp,
+            'content': content
+        }
+        if playlist is not None:
+            request['params'].append({
+                'paramName': 'Playlist',
+                'paramValue': playlist
+            })
+        return self.datasourceHandlers[self.handler.SOURCE_ID]['setDataHandler'](request, user)
diff --git a/test/testcases/test_integration.py b/test/testcases/test_integration.py
new file mode 100644
index 0000000..6a8477a
--- /dev/null
+++ b/test/testcases/test_integration.py
@@ -0,0 +1,170 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import unittest, threading, time, urllib2, cookielib, os
+from AppAgent import *
+from test.utils.Utils import *
+
+TEST_PLAYLIST = '''[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "requests": [
+                    {
+                        "getData": {
+                            "source": "DataSource",
+                            "element": "Sources"
+                        }
+                    }
+                ],
+                "condition": {
+                    "evaluatingPeriod": 0.1,
+                    "numberOfExecutions": 200,
+                    "expression": {
+                        "dataValue": "false"
+                    }
+                }
+            }
+        ]
+    }
+]'''
+
+class IntegrationTest(unittest.TestCase):
+
+    def runServer(self):
+        # start AppAgent
+        self.server.serve_forever()
+
+    def setUp(self):
+        # url opener that acts like a browser
+        self.opener = urllib2.build_opener(urllib2.HTTPHandler(), urllib2.HTTPSHandler(), urllib2.HTTPCookieProcessor(cookielib.CookieJar()), urllib2.ProxyHandler({}))
+        # create a server on a free port
+        directory = os.path.abspath('src')
+        self.port = 8000
+        while True:
+            try:
+                self.server = ThreadedHTTPServer(('localhost', self.port), MainHandler)
+                break
+            except:
+                self.port += 1
+        # start the server on a separate thread
+        self.serverThread = threading.Thread(target = runAppAgent, args = (self.server, directory, 'WebGUI/src/WebGUI/htdocs', False))
+        self.serverThread.daemon = True
+        self.serverThread.start()
+        # check if the server is running after 0.5 seconds
+        time.sleep(0.5)
+        self.assertTrue(self.serverThread.isAlive(), 'Server failed to start')
+
+    def tearDown(self):
+        # stop the server
+        self.server.shutdown()
+        # check if the server stopped after 0.5 seconds
+        time.sleep(0.5)
+        self.assertFalse(self.serverThread.isAlive(), 'Server thread is still alive after shutdown')
+
+    def test_microservicesIntegration(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_microservicesIntegration
+        Tested component: ServiceFramework
+        Feature: ServiceFramework
+        Requirement: microservices' integration into ServiceFramework
+        Action_To_Be_taken:
+            login
+            check DataSource sources
+            check playlist microservice
+            check whether 'admin' user is in the 'admin' group
+            logout
+            try and fail to use proxy without being logged in
+        Expected_Result: pass
+        '''
+
+        # login
+        self.sendRequest('/login/api.authenticate', 'POST', '{"username": "admin", "password": "admin"}')
+        # check DataSource sources
+        sources = [element['node']['val'] for element in json.loads(self.sendRequest('/api.appagent', 'POST', '{"requests": [{"getData": {"source": "DataSource", "element": "Sources"}}]}'))['contentList'][0]['list']]
+        self.assertEqual(3, len(sources), 'List lenght should be 3, got: ' + str(sources))
+        self.assertIn('DataSource', sources)
+        self.assertIn('Playlist', sources)
+        self.assertIn('Authentication', sources)
+        # check playlist microservice
+        response = json.loads(self.sendRequest('/api.playlist', 'POST', '{"requests": [{"getData": {"source": "Playlist", "element": "Playlists"}}]}'))['contentList'][0]
+        self.assertIn('list', response)
+        # check whether 'admin' user is in the 'admin' group
+        groups = [element['node']['val'] for element in json.loads(self.sendRequest(self.proxyEncode('/api.authenticate'), 'POST', '{"requests": [{"getData": {"source": "Authentication", "element": "ListGroups"}}]}'))['contentList'][0]['list']]
+        self.assertIn('admin', groups)
+        # logout
+        self.sendRequest('/logout/api.authenticate', 'POST', '')
+        # try and fail to use proxy without being logged in
+        with self.assertRaises(urllib2.HTTPError) as context:
+            self.sendRequest(self.proxyEncode('/api.authenticate'), 'POST', '{"requests": [{"getData": {"source": "Authentication", "element": "ListGroups"}}]}')
+        self.assertIn('401', str(context.exception))
+
+    def test_playlistIntegration(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_playlistIntegration
+        Tested component: ServiceFramework
+        Feature: ServiceFramework
+        Requirement: playlist integration into ServiceFramework
+        Action_To_Be_taken:
+            login
+            create a new playlist
+            start the playlist
+            check if playlist status led is green
+            stop the playlist
+            check if playlist status led is black
+            delete playlist
+            check if the playlist no longer exists
+        Expected_Result: pass
+        '''
+
+        # login
+        self.sendRequest('/login/api.authenticate', 'POST', '{"username": "admin", "password": "admin"}')
+        # create a new playlist
+        self.sendRequest('/api.playlist', 'POST', '{"requests": [{"setData": {"source": "Playlist", "element": "Descriptor", "tp": 4, "content": "' + TEST_PLAYLIST.replace('8000', str(self.port)).replace('\n', '').replace('"', r'\"') + '", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}')
+        # start the playlist
+        self.sendRequest('/api.playlist', 'POST', '{"requests": [{"setData": {"source": "Playlist", "element": "Start", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}')
+        time.sleep(0.5)
+        # check if playlist status led is green
+        self.assertIn('green', json.loads(self.sendRequest('/api.playlist', 'POST', '{"requests": [{"getData": {"source": "Playlist", "element": "Status", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}'))['contentList'][0]['node']['val'])
+        # stop the playlist
+        self.sendRequest('/api.playlist', 'POST', '{"requests": [{"setData": {"source": "Playlist", "element": "Stop", "tp": 1, "content": "1", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}')
+        time.sleep(0.5)
+        # check if playlist status led is black
+        self.assertIn('black', json.loads(self.sendRequest('/api.playlist', 'POST', '{"requests": [{"getData": {"source": "Playlist", "element": "Status", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}'))['contentList'][0]['node']['val'])
+        # delete playlist
+        self.sendRequest('/api.playlist', 'POST', '{"requests": [{"setData": {"source": "Playlist", "element": "Delete", "tp": 1, "content": "1", "params": [{"paramName": "Playlist", "paramValue": "admin/test"}]}}]}')
+        time.sleep(0.5)
+        # check if the playlist no longer exists
+        playlists = [element['node']['val'] for element in json.loads(self.sendRequest('/api.playlist', 'POST', '{"requests": [{"getData": {"source": "Playlist", "element": "Playlists"}}]}'))['contentList'][0]['list']]
+        self.assertNotIn('admin/test', playlists)
+
+    def test_fileHandling(self):
+        '''
+        Author: EDNIGBO Daniel Gobor
+        Testcase: test_fileHandling
+        Tested component: ServiceFramework
+        Feature: ServiceFramework
+        Requirement: serving files for browsers
+        Action_To_Be_taken:
+            get index.html
+            check contents
+        Expected_Result: pass
+        '''
+
+        # get index.html
+        response = self.sendRequest('')
+        # check contents
+        self.assertIn('TSGuiFrameworkMain', response)
+
+    def proxyEncode(self, path):
+        return '/proxy/' + ('http://localhost:' + str(self.port)).encode('hex') + path
+
+    def sendRequest(self, path, method = 'GET', body = None):
+        request = urllib2.Request('http://localhost:' + str(self.port) + path, body)
+        request.get_method = lambda: method
+        return self.opener.open(request).read()
diff --git a/test/utils/MockedHandler.py b/test/utils/MockedHandler.py
new file mode 100644
index 0000000..6054b38
--- /dev/null
+++ b/test/utils/MockedHandler.py
@@ -0,0 +1,93 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+import copy, json
+from Common.DsRestAPI import DsRestAPI
+from Utils import *
+
+class MockedDsRestApiHandler:
+
+    SOURCE_ID = 'Dummy_DS'
+
+    def __init__(self):
+        self.elements = {
+            "int": {"node": {"val": "1", "tp": 1}},
+            "string": {"node": {"val": "str", "tp": 4}},
+            "true": {"node": {"val": "true", "tp": 3}},
+            "false": {"node": {"val": "false", "tp": 3}},
+            "list": {"list": [{"node": {"val": str(i), "tp": 8}} for i in range(3)]},
+            "stringOfList": {
+                "0": {"node": {"val": "string_0", "tp": 4}},
+                "1": {"node": {"val": "string_1", "tp": 4}},
+                "2": {"node": {"val": "string_2", "tp": 4}}
+            },
+            "boolOfList": {
+                "0": {"node": {"val": "true", "tp": 3}},
+                "1": {"node": {"val": "true", "tp": 3}},
+                "2": {"node": {"val": "false", "tp": 3}}
+            },
+            "boolOfParam": {
+                "1": {"node": {"val": "true", "tp": 3}},
+                "str": {"node": {"val": "false", "tp": 3}}
+            },
+            "userError": {"node": {"val": "User Error", "tp": -4}},
+            "filteredItem": {"node": {"val": "", "tp": 0}},
+            "help": {"node": {"val": json.dumps({'sources': [{'source': self.SOURCE_ID, 'dataElements': []}]}).encode('hex'), "tp": 5}}
+        }
+
+        self._dsRestAPI = DsRestAPI(self.mockedGetDataHandler, self.mockedSetDataHandler)
+
+    def mockedGetDataHandler(self, request, userCredentials):
+        if userCredentials['username'] == ADMIN['username'] and userCredentials['password'] == ADMIN['password'] and len(set(userCredentials['groups']) ^ set(ADMIN['groups'])) == 0:
+            element = request['element']
+            params = request['params']
+            params.reverse()
+        else:
+            element = 'userError'
+            params = []
+
+        try:
+            response = self.elements[element]
+            while len(params) > 0:
+                response = response[params.pop()['paramValue']]
+            return copy.deepcopy(response)
+        except:
+            pass
+
+    def mockedSetDataHandler(self, request, userCredentials):
+        element = request['element']
+        params = request['params']
+        content = request['content']
+        params.reverse()
+
+        try:
+            if userCredentials['username'] == ADMIN['username'] and userCredentials['password'] == ADMIN['password'] and len(set(userCredentials['groups']) ^ set(ADMIN['groups'])) == 0:
+                response = self.elements[element]
+                while len(params) > 0:
+                    response = response[params.pop()['paramValue']]
+                if len(request['indxsInList']) > 0:
+                    response = response['list'][request['indxsInList'][0]]
+                response['node']['val'] = content
+                return response
+        except:
+            pass
+
+    def handleMessage(self, method, path, headers, body, userCredentials, response):
+        response['body'] = json.dumps(self._dsRestAPI.parseRequest(json.loads(body), userCredentials))
+        response['headers']['Content-Type'] = 'application/json'
+
+    def getDataSourceHandlers(self):
+        return {
+            self.SOURCE_ID: {
+                'getDataHandler': self.mockedGetDataHandler,
+                'setDataHandler': self.mockedSetDataHandler
+            }
+        }
+
+    def close(self):
+        for processId in self._processes:
+            if self._processes[processId]['Process'].poll() is None:
+                os.killpg(os.getpgid(self._processes[processId]['Process'].pid), signal.SIGTERM)
+                self._logger.info('Stopped TitanSim instance:', processId)
\ No newline at end of file
diff --git a/test/utils/MockedPlaylist/Playlists/admin/longtry.json b/test/utils/MockedPlaylist/Playlists/admin/longtry.json
new file mode 100644
index 0000000..a441235
--- /dev/null
+++ b/test/utils/MockedPlaylist/Playlists/admin/longtry.json
@@ -0,0 +1,26 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str",
+                            "tp": 4
+                        }
+                    }
+                ],
+                "condition": {
+                    "evaluatingPeriod": 0.1,
+                    "numberOfExecutions": 200,
+                    "expression": {
+                        "dataValue": "false"
+                    }
+                }
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/test/utils/MockedPlaylist/Playlists/admin/relativeto.json b/test/utils/MockedPlaylist/Playlists/admin/relativeto.json
new file mode 100644
index 0000000..ce04e84
--- /dev/null
+++ b/test/utils/MockedPlaylist/Playlists/admin/relativeto.json
@@ -0,0 +1,48 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "id": "0",
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str1",
+                            "tp": 4
+                        }
+                    }
+                ]
+            },
+            {
+                "relativeTo": ["0"],
+                "id": "1",
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str2",
+                            "tp": 1
+                        }
+                    }
+                ]
+            },
+            {
+                "relativeTo": ["0", "1"],
+                "id": "2",
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str3",
+                            "tp": 4
+                        }
+                    }
+                ]
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/test/utils/MockedPlaylist/Playlists/admin/simple.json b/test/utils/MockedPlaylist/Playlists/admin/simple.json
new file mode 100644
index 0000000..099236b
--- /dev/null
+++ b/test/utils/MockedPlaylist/Playlists/admin/simple.json
@@ -0,0 +1,25 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "startTime": 0.2,
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str",
+                            "tp": 4
+                        }
+                    }
+                ],
+                "condition": {
+                    "expression": {
+                        "dataValue": "false"
+                    }
+                }
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/test/utils/MockedPlaylist/Playlists/admin/try.json b/test/utils/MockedPlaylist/Playlists/admin/try.json
new file mode 100644
index 0000000..c07aad7
--- /dev/null
+++ b/test/utils/MockedPlaylist/Playlists/admin/try.json
@@ -0,0 +1,26 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str",
+                            "tp": 4
+                        }
+                    }
+                ],
+                "condition": {
+                    "evaluatingPeriod": 0.1,
+                    "numberOfExecutions": 2,
+                    "expression": {
+                        "dataValue": "false"
+                    }
+                }
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/test/utils/MockedPlaylist/Playlists/admin/wait.json b/test/utils/MockedPlaylist/Playlists/admin/wait.json
new file mode 100644
index 0000000..f9a1658
--- /dev/null
+++ b/test/utils/MockedPlaylist/Playlists/admin/wait.json
@@ -0,0 +1,26 @@
+[
+    {
+        "apiUrl": "http://localhost:8000/api.appagent",
+        "playlist": [
+            {
+                "requests": [
+                    {
+                        "setData": {
+                            "source": "Dummy_DS",
+                            "element": "string",
+                            "content": "new_str",
+                            "tp": 4
+                        }
+                    }
+                ],
+                "condition": {
+                    "evaluatingPeriod": 0.1,
+                    "cancelingTimeout": 0.3,
+                    "expression": {
+                        "dataValue": "false"
+                    }
+                }
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/test/utils/MockedRights/groupRights.json b/test/utils/MockedRights/groupRights.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/test/utils/MockedRights/groupRights.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/test/utils/MockedRights/groupRightsSchema.json b/test/utils/MockedRights/groupRightsSchema.json
new file mode 100644
index 0000000..885ba45
--- /dev/null
+++ b/test/utils/MockedRights/groupRightsSchema.json
@@ -0,0 +1,13 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "array",
+    "items": {
+        "type": "string",
+        "enum": [
+            "a",
+            "b",
+            "c"
+        ]
+    },
+    "uniqueItems": true
+}
\ No newline at end of file
diff --git a/test/utils/Utils.py b/test/utils/Utils.py
new file mode 100644
index 0000000..e139d6b
--- /dev/null
+++ b/test/utils/Utils.py
@@ -0,0 +1,37 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////
+ADMIN = {
+    "username": "admin",
+    "password": "admin",
+    "groups": set(["admin"])
+}
+
+NO_USER = {
+    "username": None,
+    "password": None,
+    "groups": set([])
+}
+
+WRONG_USER = {
+    'username': 'not_admin',
+    'password': 'not_admin_password',
+    'groups': set(['not_admin_group'])
+}
+
+GROUPRIGHTS_DIR = 'test/utils/MockedRights'
+DEFAULT_RIGHTS = ['a']
+
+PLAYLIST_DIR = 'test/utils/MockedPlaylist'
+
+HTTPSERVER_DIR = 'test/utils/HttpServerDir'
+
+def getEmptyResponse():
+    return {
+        'returnCode': 200,
+        'mimeType': 'text/plain',
+        'body': '',
+        'headers': {}
+    }
\ No newline at end of file
diff --git a/test/utils/__init__.py b/test/utils/__init__.py
new file mode 100644
index 0000000..c20f3a5
--- /dev/null
+++ b/test/utils/__init__.py
@@ -0,0 +1,5 @@
+#// Copyright (c) 2000-2017 Ericsson Telecom AB                                                         //
+#// All rights reserved. This program and the accompanying materials are made available under the terms //
+#// of the Eclipse Public License v1.0 which accompanies this distribution, and is available at         //
+#// http://www.eclipse.org/legal/epl-v10.html                                                           //
+#/////////////////////////////////////////////////////////////////////////////////////////////////////////