commit 85caef8573d4e7d5c4437c694b089d3b0361c045
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Wed Nov 14 22:59:19 2018 +0100

    Node filters refactoring

diff --git a/sources/src/main/webapp/js/components/filterModalWindow.js b/sources/src/main/webapp/js/components/filterModalWindow.js
index 2dffbb6..7e38413 100644
--- a/sources/src/main/webapp/js/components/filterModalWindow.js
+++ b/sources/src/main/webapp/js/components/filterModalWindow.js
@@ -5,45 +5,43 @@ class FilterModalWindow extends ModalWindow {
 	constructor() {
 		super();
 
-		this._filterOptions = {
+		this._baseFilterOptions = {
 			nodeType: 'Node type',
 			vertexArchetype: 'Vertex archetype',
 			vertexAttribute: 'Vertex attribute',
 		};
-
-		this._vertexAttributeTypeOptions = function() {
-			return app.attributeTypeList.map(attributeType => `${attributeType.name} (${attributeType.dataType.toLowerCase()})`);
-		};
-
 		this._operationOptions = {
 			STRING: {
-				eq: 'equals',
-				neq: 'not equals',
-				contains: 'contains',
-				startsWith: 'starts with',
-				endsWith: 'ends with',
-				regexp: 'regular expression',
+				EQ: 'equals',
+				NEQ: 'not equals',
+				CONTAINS: 'contains',
+				STARTS_WITH: 'starts with',
+				ENDS_WITH: 'ends with',
+				REGEXP: 'regular expression',
 			},
 			ENUM: {
-				eq: 'equals',
-				neq: 'not equals',
+				EQ: 'equals',
+				NEQ: 'not equals',
 			},
 			DATE: {
-				eq: 'equals',
-				neq: 'not equals',
-				range: 'is in range',
+				EQ: 'equals',
+				NEQ: 'not equals',
+				RANGE: 'is in range',
 			},
 			NUMBER: {
-				eq: 'equals',
-				neq: 'not equals',
-				lt: 'lower than',
-				lte: 'lower than or equals',
-				gt: 'greater than',
-				gte: 'greater than or equals',
-				range: 'is in range',
+				EQ: 'equals',
+				NEQ: 'not equals',
+				LT: 'lower than',
+				LTE: 'lower than or equals',
+				GT: 'greater than',
+				GTE: 'greater than or equals',
+				RANGE: 'is in range',
 			},
 		};
 
+		this._vertexAttributeTypeOptions = function() {
+			return app.attributeTypeList.map(attributeType => `${attributeType.name} (${attributeType.dataType.toLowerCase()})`);
+		};
 		this._nodeTypeEnumValues = function() {
 			return ['Vertex', 'Group'];
 		};
@@ -82,15 +80,15 @@ class FilterModalWindow extends ModalWindow {
 				DOM.h('tr', {}, [
 					DOM.h('td', {}, [
 						DOM.h('label', {
-							for: 'filter',
+							for: 'baseFilter',
 							innerText: 'Filter:',
 						}),
 					]),
 					DOM.h('td', {}, [
 						DOM.h('select', {
-							name: 'filter',
-							id: 'filter',
-							onChange: e => this._onFilterChange(e.target.value),
+							name: 'baseFilter',
+							id: 'baseFilter',
+							onChange: e => this._onBaseFilterChange(e.target.value),
 						}),
 						DOM.h('select', {
 							name: 'additionalFilter',
@@ -141,39 +139,16 @@ class FilterModalWindow extends ModalWindow {
 		this._bodyElement.appendChild(this._form);
 
 		// set values
-		for (let key in this._filterOptions) {
-			let value = this._filterOptions[key];
+		for (let key in this._baseFilterOptions) {
+			let value = this._baseFilterOptions[key];
 
-			this._form.filter.appendChild(DOM.h('option', {
+			this._form.baseFilter.appendChild(DOM.h('option', {
 				value: key,
 				innerText: value,
 			}));
 		}
 
-		this._onFilterChange('nodeType');
-
-		/*
-			// filter mode
-			DOM.h('select', {
-				name: 'filterMode',
-			}, [
-				DOM.h('option', {
-					value: 'and',
-					innerText: 'and',
-				}),
-				DOM.h('option', {
-					value: 'or',
-					innerText: 'or',
-				}),
-				DOM.h('option', {
-					value: 'xor',
-					innerText: 'xor',
-				}),
-			]),
-
-		this._filterList = DOM.h('ul');
-		this._bodyElement.appendChild(this._filterList);
-		*/
+		this._onBaseFilterChange('nodeType');
 
 		return this._rootElement;
 	}
@@ -236,15 +211,15 @@ class FilterModalWindow extends ModalWindow {
 	 * 
 	 * @param {string} value 
 	 */
-	_onFilterChange(value) {
+	_onBaseFilterChange(value) {
 		switch (value) {
 			case 'nodeType':
 				this._form.additionalFilter.setAttribute('hidden', 'hidden');
 
-				this._form.dataType.value = 'ENUM';
+				this._form.dataType.value = FilterDataType.ENUM;
 
 				this._setOperationOptions();
-				this._onOperationChange('equals');
+				this._onOperationChange(EnumFilterOperation.EQ);
 
 				this._setEnumOptions(this._nodeTypeEnumValues());
 
@@ -253,10 +228,10 @@ class FilterModalWindow extends ModalWindow {
 			case 'vertexArchetype':
 				this._form.additionalFilter.setAttribute('hidden', 'hidden');
 
-				this._form.dataType.value = 'ENUM';
+				this._form.dataType.value = FilterDataType.ENUM;
 
 				this._setOperationOptions();
-				this._onOperationChange('equals');
+				this._onOperationChange(EnumFilterOperation.EQ);
 
 				this._setEnumOptions(this._vertexArchetypeEnumValues());
 
@@ -291,9 +266,9 @@ class FilterModalWindow extends ModalWindow {
 
 		this._form.dataType.value = attributeType.dataType;
 		this._setOperationOptions();
-		this._onOperationChange('equals');
+		this._onOperationChange('EQ');
 
-		if (attributeType.dataType === 'ENUM') {
+		if (attributeType.dataType === FilterDataType.ENUM) {
 			this._setEnumOptions(this._vertexAttributeEnumValues(value));
 		}
 	}
@@ -321,21 +296,24 @@ class FilterModalWindow extends ModalWindow {
 
 		this._valueCell.innerHTML = '';
 		switch (dataType) {
-			case 'STRING':
+			case FilterDataType.STRING:
 				this._valueCell.appendChild(this._stringField);
 				break;
-			case 'ENUM':
+
+			case FilterDataType.ENUM:
 				this._valueCell.appendChild(this._enumField);
 				break;
-			case 'DATE':
-				if (value === 'range') {
+
+			case FilterDataType.DATE:
+				if (value === DateFilterOperation.RANGE) {
 					this._valueCell.appendChild(this._dateRangeField);
 				} else {
 					this._valueCell.appendChild(this._dateField);
 				}
 				break;
-			case 'NUMBER':
-				if (value === 'range') {
+
+			case FilterDataType.NUMBER:
+				if (value === NumberFilterOperation.RANGE) {
 					this._valueCell.appendChild(this._numberRangeField);
 				} else {
 					this._valueCell.appendChild(this._numberField);
@@ -354,10 +332,6 @@ class FilterModalWindow extends ModalWindow {
 		});
 	}
 
-	/**
-	 * 
-	 * @param {*} e 
-	 */
 	_onFilterFormSubmit(e) {
 		e.preventDefault();
 
@@ -368,7 +342,7 @@ class FilterModalWindow extends ModalWindow {
 	}
 
 	_onFilterFormReset() {
-		this._onFilterChange('nodeType');
+		this._onBaseFilterChange('nodeType');
 
 		app.nodeList.forEach(node => {
 			node.isFound = false;
@@ -377,7 +351,7 @@ class FilterModalWindow extends ModalWindow {
 
 	_displayFilter(formData) {
 		// filter
-		console.log(this._filterOptions[formData.get('filter')]);
+		console.log(this._baseFilterOptions[formData.get('filter')]);
 
 		// additional filter
 		switch (formData.get('filter')) {
@@ -391,10 +365,11 @@ class FilterModalWindow extends ModalWindow {
 
 		// value(s)
 		switch (formData.get('dataType')) {
-			case 'STRING':
+			case FilterDataType.STRING:
 				console.log(formData.get('value'));
 				break;
-			case 'ENUM':
+
+			case FilterDataType.ENUM:
 				switch (formData.get('filter')) {
 					case 'nodeType':
 						console.log(formData.getAll('value'));
@@ -407,15 +382,17 @@ class FilterModalWindow extends ModalWindow {
 						break;
 				}
 				break;
-			case 'DATE':
-				if (formData.get('operation') === 'range') {
+
+			case FilterDataType.DATE:
+				if (formData.get('operation') === DateFilterOperation.range) {
 					console.log(formData.get('value-from'), formData.get('value-to'));
 				} else {
 					console.log(formData.get('value'));
 				}
 				break;
-			case 'NUMBER':
-				if (formData.get('operation') === 'range') {
+
+			case FilterDataType.NUMBER:
+				if (formData.get('operation') === NumberFilterOperation.range) {
 					console.log(formData.get('value-from'), formData.get('value-to'));
 				} else {
 					console.log(formData.get('value'));
@@ -425,204 +402,28 @@ class FilterModalWindow extends ModalWindow {
 	}
 
 	_applyFilter(formData) {
-		let nodeListCopy = app.nodeList.slice(0);
-
-		const filter = formData.get('filter');
+		const baseFilter = formData.get('baseFilter');
 		const additionalFilter = formData.get('additionalFilter');
 		const dataType = formData.get('dataType');
 		const operation = formData.get('operation');
 
-		let filterFunction;
-
-		switch (filter) {
+		let nodeFilter;
+		switch (baseFilter) {
 			case 'nodeType':
-				var values = formData.getAll('value').map(value => parseInt(value));
-
-				// datatype is always enum
-				switch (operation) {
-					case 'eq':
-						filterFunction = node => {
-							return (node instanceof Vertex && values.indexOf(0) > -1)
-								|| (node instanceof Group && values.indexOf(1) > -1);
-						};
-						break;
-
-					case 'neq':
-						filterFunction = node => {
-							return (node instanceof Vertex && values.indexOf(0) < 0)
-								|| (node instanceof Group && values.indexOf(1) < 0);
-						};
-						break;
-				}
+				nodeFilter = new NodeTypeFilter(operation, formData);
 				break;
 
 			case 'vertexArchetype':
-				// prefilter
-				nodeListCopy = nodeListCopy.filter(node => {
-					return node instanceof Vertex;
-				});
-
-				var values = formData.getAll('value').map(value => parseInt(value));
-
-				// datatype is always enum
-				switch (operation) {
-					case 'eq':
-						filterFunction = vertex => {
-							return (values.indexOf(vertex.archetype) > -1);
-						};
-						break;
-
-					case 'neq':
-						filterFunction = vertex => {
-							return (values.indexOf(vertex.archetype) < 0);
-						};
-						break;
-				}
+				nodeFilter = new VertexArchetypeFilter(operation, formData);
 				break;
 
 			case 'vertexAttribute':
-				const filterAttributeName = app.attributeTypeList[additionalFilter].name;
-
-				// prefilter
-				nodeListCopy = nodeListCopy.filter(node => {
-					return node instanceof Vertex;
-				}).filter(vertex => {
-					return vertex.attributes.some(attribute => attribute[0] === filterAttributeName);
-				});
-
-				switch (dataType) {
-					case 'STRING':
-						var comparatorFn;
-						switch (operation) {
-							case 'eq':
-								comparatorFn = (a, b) => a === b;
-								break;
-							case 'neq':
-								comparatorFn = (a, b) => a !== b;
-								break;
-							case 'contains':
-								comparatorFn = (a, b) => a.includes(b);
-								break;
-							case 'startsWith':
-								comparatorFn = (a, b) => a.startsWith(b);
-								break;
-							case 'endsWith':
-								comparatorFn = (a, b) => a.endsWith(b);
-								break;
-							case 'regexp':
-								comparatorFn = (a, b) => a.match(new RegExp(b, 'i'));
-								break;
-						}
-
-						filterFunction = vertex => {
-							const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-							return attribute != null && comparatorFn(attribute[1], formData.get('value'));
-						};
-						break;
-
-					case 'ENUM':
-						var values = formData.getAll('value').map(value => app.possibleEnumValues[additionalFilter][parseInt(value)]);
-
-						switch (operation) {
-							case 'eq':
-								filterFunction = vertex => {
-									const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-									const attributeValues = attribute[1].split(', ');
-
-									// some (at least one) of the attribute items should be contained in the filters
-									return attributeValues.some(attributeValue => {
-										return values.indexOf(attributeValue) > -1;
-									});
-								};
-								break;
-
-							case 'neq':
-								filterFunction = vertex => {
-									const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-									const attributeValues = attribute[1].split(', ');
-
-									// every of the attribute items should not be contained in the filters (translated: none of the items should be contained in the filters)
-									return attributeValues.every(attributeValue => {
-										return values.indexOf(attributeValue) < 0;
-									});
-								};
-								break;
-						}
-						break;
-
-					case 'DATE':
-						if (operation === 'range') {
-							filterFunction = vertex => {
-								const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-								const a = parseInt(attribute[1]);
-								const b = formData.get('value-from') !== '' ? Date.parse(formData.get('value-from')) : 0;
-								const c = formData.get('value-to') !== '' ? Date.parse(formData.get('value-to')) : Date.now();
-								return (a >= b) && (a <= c);
-							};
-
-						} else {
-							var comparatorFn;
-							switch (operation) {
-								case 'eq':
-									comparatorFn = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
-									break;
-								case 'neq':
-									comparatorFn = (a, b) => a.getFullYear() !== b.getFullYear() || a.getMonth() !== b.getMonth() || a.getDate() !== b.getDate();
-									break;
-							}
-
-							filterFunction = vertex => {
-								const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-								return comparatorFn(new Date(parseInt(attribute[1])), new Date(formData.get('value')));
-							};
-						}
-						break;
-
-					case 'NUMBER':
-						if (operation === 'range') {
-							filterFunction = vertex => {
-								const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-								const a = parseFloat(attribute[1]);
-								const b = formData.get('value-from') !== '' ? parseFloat(formData.get('value-from')) : Number.MIN_VALUE;
-								const c = formData.get('value-to') !== '' ? parseFloat(formData.get('value-to')) : Number.MAX_VALUE;
-								return (a >= b) && (a <= c);
-							};
-
-						} else {
-							var comparatorFn;
-							switch (operation) {
-								case 'eq':
-									comparatorFn = (a, b) => a === b;
-									break;
-								case 'neq':
-									comparatorFn = (a, b) => a !== b;
-									break;
-								case 'lt':
-									comparatorFn = (a, b) => a < b;
-									break;
-								case 'lte':
-									comparatorFn = (a, b) => a <= b;
-									break;
-								case 'gt':
-									comparatorFn = (a, b) => a > b;
-									break;
-								case 'gte':
-									comparatorFn = (a, b) => a >= b;
-									break;
-							}
-
-							filterFunction = vertex => {
-								const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
-								return attribute != null && comparatorFn(parseFloat(attribute[1]), parseFloat(formData.get('value')));
-							};
-						}
-						break;
-				}
+				nodeFilter = new VertexAttributeFilter(additionalFilter, dataType, operation, formData);
 				break;
 		}
 
-		nodeListCopy.filter(filterFunction).forEach(node => {
+		nodeFilter.run(app.nodeList).forEach(node => {
 			node.isFound = true;
 		});
 	}
-}
\ No newline at end of file
+}
diff --git a/sources/src/main/webapp/js/services/filter.js b/sources/src/main/webapp/js/services/filter.js
deleted file mode 100644
index 244e8f9..0000000
--- a/sources/src/main/webapp/js/services/filter.js
+++ /dev/null
@@ -1,37 +0,0 @@
-class Filter {
-
-	constructor() {
-		this._filterList = [];
-	}
-
-	run() {
-		console.log(this._filterList);
-	}
-
-	/**
-     * Function add values from input list to archetype selector.
-     * @param vertexArchetypes list of vertex archetypes
-     */
-    initializeSelectors(vertexArchetypes) {
-        let vertexArchetypeSelection = document.getElementById('vertexArchetypeSelection');
-
-        vertexArchetypes.forEach(function(archetype) {
-            let option = document.createElement("option");
-            option.text = archetype.name;
-            option.value = archetype.name;
-            vertexArchetypeSelection.add(option);
-        });
-    }
-
-    /**
-     * Function remove all options from archetype selector.
-     */
-    resetSelectors() {
-        let vertexArchetypeSelection = document.getElementById('vertexArchetypeSelection');
-
-        while (vertexArchetypeSelection.length > 0){
-            vertexArchetypeSelection.remove(0);
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/services/filters/AbstractFilter.js b/sources/src/main/webapp/js/services/filters/AbstractFilter.js
new file mode 100644
index 0000000..b75f62f
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/AbstractFilter.js
@@ -0,0 +1,10 @@
+class AbstractFilter {
+	/**
+	 * @abstract
+	 * @param {array<Node>} nodeList List of nodes to be filtered.
+	 * @returns {array<Node>} Filtered list of nodes.
+	 */
+	run(nodeList) {
+		throw new AbstractMethodError;
+	}
+}
diff --git a/sources/src/main/webapp/js/services/filters/DateFilterOperation.js b/sources/src/main/webapp/js/services/filters/DateFilterOperation.js
new file mode 100644
index 0000000..02ceaa3
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/DateFilterOperation.js
@@ -0,0 +1,5 @@
+const DateFilterOperation = {
+	EQ: 'EQ',
+	NEQ: 'NEQ',
+	RANGE: 'RANGE',
+};
diff --git a/sources/src/main/webapp/js/services/filters/EnumFilterOperation.js b/sources/src/main/webapp/js/services/filters/EnumFilterOperation.js
new file mode 100644
index 0000000..3cc7434
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/EnumFilterOperation.js
@@ -0,0 +1,4 @@
+const EnumFilterOperation = {
+	EQ: 'EQ',
+	NEQ: 'NEQ',
+};
diff --git a/sources/src/main/webapp/js/services/filters/FilterDataType.js b/sources/src/main/webapp/js/services/filters/FilterDataType.js
new file mode 100644
index 0000000..c5b6f55
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/FilterDataType.js
@@ -0,0 +1,6 @@
+const FilterDataType = {
+	STRING: 'STRING',
+	ENUM: 'ENUM',
+	DATE: 'DATE',
+	NUMBER: 'NUMBER',
+};
diff --git a/sources/src/main/webapp/js/services/filters/NodeTypeFilter.js b/sources/src/main/webapp/js/services/filters/NodeTypeFilter.js
new file mode 100644
index 0000000..9394aaf
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/NodeTypeFilter.js
@@ -0,0 +1,52 @@
+class NodeTypeFilter extends AbstractFilter {
+	/**
+	 * @constructor
+	 * @param {string} operation Filtering operation.
+	 * @param {FormData} formData Raw, unprocessed form content.
+	 */
+	constructor(operation, formData) {
+		super();
+
+		this._operation = operation;
+		this._formData = formData;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	run(nodeList) {
+		return nodeList.filter(this._getFilterFunction());
+	}
+
+	/**
+	 * @returns {function} Function to be used for filtering.
+	 */
+	_getFilterFunction() {
+		const filterValues = this._formData.getAll('value').map(value => parseInt(value));
+
+		let filterFn;
+		switch (this._operation) {
+			case EnumFilterOperation.EQ:
+				filterFn = node => {
+					return (node instanceof Vertex && filterValues.indexOf(NodeTypeFilter.VERTEX) > -1)
+						|| (node instanceof Group && filterValues.indexOf(NodeTypeFilter.GROUP) > -1);
+				};
+				break;
+
+			case EnumFilterOperation.NEQ:
+				filterFn = node => {
+					return (node instanceof Vertex && filterValues.indexOf(NodeTypeFilter.VERTEX) < 0)
+						|| (node instanceof Group && filterValues.indexOf(NodeTypeFilter.GROUP) < 0);
+				};
+				break;
+
+			default:
+				throw new InvalidArgumentError;
+		}
+
+		return filterFn;
+	}
+}
+
+NodeTypeFilter.VERTEX = 0;
+NodeTypeFilter.GROUP = 1;
diff --git a/sources/src/main/webapp/js/services/filters/NumberFilterOperation.js b/sources/src/main/webapp/js/services/filters/NumberFilterOperation.js
new file mode 100644
index 0000000..020fe2a
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/NumberFilterOperation.js
@@ -0,0 +1,9 @@
+const NumberFilterOperation = {
+	EQ: 'EQ',
+	NEQ: 'NEQ',
+	LT: 'LT',
+	LTE: 'LTE',
+	GT: 'GT',
+	GTE: 'GTE',
+	RANGE: 'RANGE',
+};
diff --git a/sources/src/main/webapp/js/services/filters/StringFilterOperation.js b/sources/src/main/webapp/js/services/filters/StringFilterOperation.js
new file mode 100644
index 0000000..16a77e7
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/StringFilterOperation.js
@@ -0,0 +1,8 @@
+const StringFilterOperation = {
+	EQ: 'EQ',
+	NEQ: 'NEQ',
+	CONTAINS: 'CONTAINS',
+	STARTS_WITH: 'STARTS_WITH',
+	ENDS_WITH: 'ENDS_WITH',
+	REGEXP: 'REGEXP',
+};
diff --git a/sources/src/main/webapp/js/services/filters/VertexArchetypeFilter.js b/sources/src/main/webapp/js/services/filters/VertexArchetypeFilter.js
new file mode 100644
index 0000000..b8cad25
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/VertexArchetypeFilter.js
@@ -0,0 +1,53 @@
+class VertexArchetypeFilter extends AbstractFilter {
+	/**
+	 * @constructor
+	 * @param {string} operation Filtering operation.
+	 * @param {FormData} formData Raw, unprocessed form content.
+	 */
+	constructor(operation, formData) {
+		super();
+
+		this._operation = operation;
+		this._formData = formData;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	run(nodeList) {
+		// prefilter
+		nodeList = nodeList.filter(node => {
+			return node instanceof Vertex;
+		});
+
+		// filter
+		return nodeList.filter(this._getFilterFunction());
+	}
+
+	/**
+	 * @returns {function} Function to be used for filtering.
+	 */
+	_getFilterFunction() {
+		const filterValues = this._formData.getAll('value').map(value => parseInt(value));
+
+		let filterFn;
+		switch (this._operation) {
+			case EnumFilterOperation.EQ:
+				filterFn = vertex => {
+					return filterValues.indexOf(vertex.archetype) > -1;
+				};
+				break;
+
+			case EnumFilterOperation.NEQ:
+				filterFn = vertex => {
+					return filterValues.indexOf(vertex.archetype) < 0;
+				};
+				break;
+
+			default:
+				throw new InvalidArgumentError;
+		}
+
+		return filterFn;
+	}
+}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/services/filters/VertexAttributeFilter.js b/sources/src/main/webapp/js/services/filters/VertexAttributeFilter.js
new file mode 100644
index 0000000..348336a
--- /dev/null
+++ b/sources/src/main/webapp/js/services/filters/VertexAttributeFilter.js
@@ -0,0 +1,231 @@
+class VertexAttributeFilter extends AbstractFilter {
+	/**
+	 * @constructor
+	 * @param {string} additionalFilter 
+	 * @param {string} dataType 
+	 * @param {string} operation Filtering operation.
+	 * @param {FormData} formData Raw, unprocessed form content.
+	 */
+	constructor(additionalFilter, dataType, operation, formData) {
+		super();
+
+		this._additionalFilter = additionalFilter;
+		this._dataType = dataType;
+		this._operation = operation;
+		this._formData = formData;
+		this._filterAttributeName = app.attributeTypeList[additionalFilter].name;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	run(nodeList) {
+		// prefilter
+		nodeList = nodeList.filter(node => {
+			return node instanceof Vertex;
+		}).filter(vertex => {
+			return vertex.attributes.some(attribute => attribute[0] === this._filterAttributeName);
+		});
+
+		// filter
+		return nodeList.filter(this._getFilterFunction());
+	}
+
+	/**
+	 * @returns {function} Function to be used for filtering.
+	 */
+	_getFilterFunction() {
+		let filterFn;
+		switch (this._dataType) {
+			case FilterDataType.STRING:
+				filterFn = this._getStringFilterFunction(this._operation, this._filterAttributeName, this._formData);
+				break;
+
+			case FilterDataType.ENUM:
+				filterFn = this._getEnumFilterFunction(this._additionalFilter, this._operation, this._filterAttributeName, this._formData);
+				break;
+
+			case FilterDataType.DATE:
+				filterFn = this._getDateFilterFunction(this._operation, this._filterAttributeName, this._formData);
+				break;
+
+			case FilterDataType.NUMBER:
+				filterFn = this._getNumberFilterFunction(this._operation, this._filterAttributeName, this._formData);
+				break;
+
+			default:
+				throw new InvalidArgumentError;
+		}
+
+		return filterFn;
+	}
+
+	_getStringFilterFunction(operation, filterAttributeName, formData) {
+		const filterValue = formData.get('value');
+
+		let comparatorFn;
+		switch (operation) {
+			case StringFilterOperation.EQ:
+				comparatorFn = (a, b) => a === b;
+				break;
+
+			case StringFilterOperation.NEQ:
+				comparatorFn = (a, b) => a !== b;
+				break;
+
+			case StringFilterOperation.CONTAINS:
+				comparatorFn = (a, b) => a.includes(b);
+				break;
+
+			case StringFilterOperation.STARTS_WITH:
+				comparatorFn = (a, b) => a.startsWith(b);
+				break;
+
+			case StringFilterOperation.ENDS_WITH:
+				comparatorFn = (a, b) => a.endsWith(b);
+				break;
+
+			case StringFilterOperation.REGEXP:
+				comparatorFn = (a, b) => a.match(new RegExp(b, 'i'));
+				break;
+
+			default:
+				throw new InvalidArgumentError;
+		}
+
+		return vertex => {
+			const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+			return attribute != null && comparatorFn(attribute[1], filterValue);
+		};
+	}
+
+	_getEnumFilterFunction(additionalFilter, operation, filterAttributeName, formData) {
+		const filterValues = formData.getAll('value').map(value => app.possibleEnumValues[additionalFilter][parseInt(value)]);
+
+		let filterFn;
+		switch (operation) {
+			case EnumFilterOperation.EQ:
+				filterFn = vertex => {
+					const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+					const attributeValues = attribute[1].split(', ');
+
+					// some (at least one) of the attribute items should be contained in the filters
+					return attributeValues.some(attributeValue => {
+						return filterValues.indexOf(attributeValue) > -1;
+					});
+				};
+				break;
+
+			case EnumFilterOperation.NEQ:
+				filterFn = vertex => {
+					const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+					const attributeValues = attribute[1].split(', ');
+
+					// every of the attribute items should not be contained in the filters (translated: none of the items should be contained in the filters)
+					return attributeValues.every(attributeValue => {
+						return filterValues.indexOf(attributeValue) < 0;
+					});
+				};
+				break;
+
+			default:
+				throw new InvalidArgumentError;
+		}
+
+		return filterFn;
+	}
+
+	_getDateFilterFunction(operation, filterAttributeName, formData) {
+		let filterFn;
+		if (operation === DateFilterOperation.RANGE) {
+			const filterValueFrom = formData.get('value-from') !== '' ? Date.parse(formData.get('value-from')) : 0;
+			const filterValueTo = formData.get('value-to') !== '' ? Date.parse(formData.get('value-to')) : Date.now();
+
+			filterFn = vertex => {
+				const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+				const attributeValue = parseInt(attribute[1]);
+				return (attributeValue >= filterValueFrom) && (attributeValue <= filterValueTo);
+			};
+
+		} else {
+			const filterValue = new Date(formData.get('value'));
+
+			let comparatorFn;
+			switch (operation) {
+				case DateFilterOperation.EQ:
+					comparatorFn = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
+					break;
+
+				case DateFilterOperation.NEQ:
+					comparatorFn = (a, b) => a.getFullYear() !== b.getFullYear() || a.getMonth() !== b.getMonth() || a.getDate() !== b.getDate();
+					break;
+
+				default:
+					throw new InvalidArgumentError;
+			}
+
+			filterFn = vertex => {
+				const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+				const attributeValue = new Date(parseInt(attribute[1]));
+				return comparatorFn(attributeValue, filterValue);
+			};
+		}
+
+		return filterFn;
+	}
+
+	_getNumberFilterFunction(operation, filterAttributeName, formData) {
+		let filterFn;
+		if (operation === NumberFilterOperation.RANGE) {
+			const filterValueFrom = formData.get('value-from') !== '' ? parseFloat(formData.get('value-from')) : Number.MIN_VALUE;
+			const filterValueTo = formData.get('value-to') !== '' ? parseFloat(formData.get('value-to')) : Number.MAX_VALUE
+
+			filterFn = vertex => {
+				const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+				const attributeValue = parseFloat(attribute[1]);
+				return (attributeValue >= filterValueFrom) && (attributeValue <= filterValueTo);
+			};
+
+		} else {
+			const filterValue = parseFloat(formData.get('value'));
+
+			let comparatorFn;
+			switch (operation) {
+				case NumberFilterOperation.EQ:
+					comparatorFn = (a, b) => a === b;
+					break;
+
+				case NumberFilterOperation.NEQ:
+					comparatorFn = (a, b) => a !== b;
+					break;
+
+				case NumberFilterOperation.LT:
+					comparatorFn = (a, b) => a < b;
+					break;
+
+				case NumberFilterOperation.LTE:
+					comparatorFn = (a, b) => a <= b;
+					break;
+
+				case NumberFilterOperation.GT:
+					comparatorFn = (a, b) => a > b;
+					break;
+
+				case NumberFilterOperation.GTE:
+					comparatorFn = (a, b) => a >= b;
+					break;
+
+				default:
+					throw new InvalidArgumentError;
+			}
+
+			filterFn = vertex => {
+				const attribute = vertex.attributes.find(attribute => attribute[0] === filterAttributeName);
+				const attributeValue = parseFloat(attribute[1]);
+				return attribute != null && comparatorFn(attributeValue, filterValue);
+			};
+		}
+
+		return filterFn;
+	}
+}
diff --git a/sources/src/main/webapp/js/showGraphApp.js b/sources/src/main/webapp/js/showGraphApp.js
index a143b8c..9d16fcf 100644
--- a/sources/src/main/webapp/js/showGraphApp.js
+++ b/sources/src/main/webapp/js/showGraphApp.js
@@ -16,7 +16,6 @@ class ShowGraphApp extends App {
 		this.graphExporter = new GraphExporter;
 		this.zoom = new Zoom(0.8);
 		this.markSymbol = new MarkSymbol;
-		this.filter = new Filter;
 
 		/** @prop {array<Edge>} edgeList */
 		this.edgeList = [];
diff --git a/sources/src/main/webapp/showGraph.jsp b/sources/src/main/webapp/showGraph.jsp
index 5e7e300..5636120 100644
--- a/sources/src/main/webapp/showGraph.jsp
+++ b/sources/src/main/webapp/showGraph.jsp
@@ -75,7 +75,15 @@
 		<script src="js/events/loggedOutEvent.js"></script>
 		<script src="js/events/registeredEvent.js"></script>
 
-		<script src="js/services/filter.js"></script>
+		<script src="js/services/filters/AbstractFilter.js"></script>
+		<script src="js/services/filters/DateFilterOperation.js"></script>
+		<script src="js/services/filters/EnumFilterOperation.js"></script>
+		<script src="js/services/filters/FilterDataType.js"></script>
+		<script src="js/services/filters/NodeTypeFilter.js"></script>
+		<script src="js/services/filters/NumberFilterOperation.js"></script>
+		<script src="js/services/filters/StringFilterOperation.js"></script>
+		<script src="js/services/filters/VertexArchetypeFilter.js"></script>
+		<script src="js/services/filters/VertexAttributeFilter.js"></script>
 		<script src="js/services/forceDirected.js"></script>
 		<script src="js/services/graphLoader.js"></script>
 		<script src="js/services/graphExporter.js"></script>
diff --git a/sources/src/main/webapp/tests/filter.robot b/sources/src/main/webapp/tests/filter.robot
index 3db6306..d482e06 100644
--- a/sources/src/main/webapp/tests/filter.robot
+++ b/sources/src/main/webapp/tests/filter.robot
@@ -15,7 +15,7 @@ ${filterButton}						//button[@id="filterButton"]
 ${filterModalWindow}				//div[contains(@class, "filter-modal")]
 ${filterModalWindowCloseButton}		${filterModalWindow}//button[contains(@class, "close-button")]
 
-${baseFilterDropdown}				${filterModalWindow}//select[@name="filter"]
+${baseFilterDropdown}				${filterModalWindow}//select[@name="baseFilter"]
 ${additionalFilterDropdown}			${filterModalWindow}//select[@name="additionalFilter"]
 ${operationDropdown}				${filterModalWindow}//select[@name="operation"]
 
@@ -54,7 +54,7 @@ Close filter modal by backdrop click
 
 Focus form fields
 	Click Button					${filterButton}
-	Click Element					${filterModalWindow}//label[@for="filter"]
+	Click Element					${filterModalWindow}//label[@for="baseFilter"]
 	Element Should Be Focused		${baseFilterDropdown}
 	Click Element					${filterModalWindow}//label[@for="operation"]
 	Element Should Be Focused		${operationDropdown}
