diff --git a/README.md b/README.md index b06b4c9..48ccda1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,62 @@ # Linkchecker -Тестовое задание. Разработать REST-сервис, проверяющий работоспособность любой последовательности узлов. Каждый узел имеет уникальное имя, -вероятность, с которой откажет при обращении к нему, и счетчик успешно выполненных запросов. \ No newline at end of file +## Оригинальный текст задания + +Разработать REST-сервис, проверяющий работоспособность любой последовательности узлов. +Каждый узел имеет уникальное имя, вероятность, с которой откажет при обращении к нему, и счетчик успешно выполненных запросов. + +Сервис должен реализовывать два POST-метода: + +1. setNodes устанавливает граф из узлов, описанных выше. Формат входных данных - JSON. +Программа должна исключать циклические связи узлов. +2. checkRoute принимает набор вершин (или их идентификаторов) в формате JSON +и проходит по этим вершинам, проверяя на каждом пройденном узле, не отказал ли он. +Если путь существует в графе и ни один из узлов пути не отказал, следует увеличить счетчик +в каждом из узлов пути. В противном случае отображать ошибку в ответе POST-метода (произвольный формат). +3. Узлы и связи должны храниться в базе данных. + +## Изменённый вариант задания + +После введённых корректировок, итоговый вид тестового задания выглядит следующим образом: + +Разработать REST-сервис, проверяющий работоспособность любой последовательности узлов. + +1. Каждый узел имеет уникальное имя и счетчик успешно выполненных запросов, +так же для хранения в базе данных есть уникальный идентификатор, который +присваивается автоматически при записи в БД. Был исключен элемент вероятность отказа узла. +Этот параметр перестал быть нужным, так как вероятность отказа узла стала случайным фактором, +возникающая автоматически, во время проверки последовательности узлов. +1. Граф в программе неориентированный, т.е. вершины графа связаны друг с другом рёбрами, +не имеющими направления. В базе данных, хранение графа осуществляется в двух таблицах. +В одной таблице осуществляется хранение набора узлов графа, в другой набор рёбер графа. +Более подробно смотрите запись в wiki [Описание данных](./../wikis/Описание%20данных) +1. Программа позволяет делать следующее: + 1. Работать с графом обобщённо: + 1. Создавать новый граф (при этом информация о прежнем графе будет удалена с БД) + 1. Извлекать информацию о графе в заданном формате + 1. Удалять целиком весь граф + 1. Проверять работоспособность заданной последовательности узлов + (по условию задачи) выполнив соответствующий запрос. +1. Работать с узлами и ребрами по отдельности, т.е. добавлять, удалять, +искать информацию по заданным параметрам. Ручное изменение какой-либо информации о узле и ребре не предусмотрена, +т.е. возможно либо добавления узла или ребра в БД или удаление из БД) + +Так как по условию задания, граф должен исключить все виды циклов +(т.е. граф должен быть ациклическим), то при любом запросе информации о графе целиком +или при проверки набора заданных узлов будет, происходить автоматический поиск +и удаление циклов из графа. Удаление циклов происходит при помощи удаления набора рёбер, +создающие циклы, поэтому в случае обнаружения циклов в графе, набор рёбер графа будет +изменён и данные изменения попадут в БД. + +Автоматический поиск и удаление циклов не срабатывает, если происходит работа только +с набором данных рёбер графа в отдельности, т.е. можно добавлять в базу рёбра, +образующие циклы. + +**Используемый стек** : **Spring Boot**, **Spring Data**, **ORM (Hibernate)**, +[**JGraphT**](https://jgrapht.org/) (для работы с графом), +**GSON** (используется вместо используемого по умолчанию Jackson для работы с json), +**Thymeleaf** и **Bootstrap** (используется для формирования стартовой информационной страницы), +**Mockito** (идёт вместе с Spring Boot), +**Powermock** (подключается отдельной библиотекой, используется в дополнении к mockito для тестов) + +**Хранилище данных** : PostgeSQL (для production), H2 (для тестов) \ No newline at end of file diff --git a/TestRESTful/Edges/addEdge.http b/TestRESTful/Edges/addEdge.http new file mode 100644 index 0000000..68d939f --- /dev/null +++ b/TestRESTful/Edges/addEdge.http @@ -0,0 +1,15 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/rest/v1/graph/edges/create +Content-Type: application/json +Cache-Control: no-cache + +{"nodeOne": "v6", "nodeTwo": "v7"} + +### \ No newline at end of file diff --git a/TestRESTful/Edges/addEdges.http b/TestRESTful/Edges/addEdges.http new file mode 100644 index 0000000..49a9939 --- /dev/null +++ b/TestRESTful/Edges/addEdges.http @@ -0,0 +1,15 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/rest/v1/graph/edges/create/byBatch +Content-Type: application/json +Cache-Control: no-cache + +[{"nodeOne": "v2", "nodeTwo": "v6"}, {"nodeOne": "v6", "nodeTwo": "v7"}, {"nodeOne": "v7", "nodeTwo": "v8"}] + +### \ No newline at end of file diff --git a/TestRESTful/Edges/deleteAllEdges.http b/TestRESTful/Edges/deleteAllEdges.http new file mode 100644 index 0000000..b252296 --- /dev/null +++ b/TestRESTful/Edges/deleteAllEdges.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/edges +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/deleteEdgeById.http b/TestRESTful/Edges/deleteEdgeById.http new file mode 100644 index 0000000..7480f1d --- /dev/null +++ b/TestRESTful/Edges/deleteEdgeById.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/edges/byId/5005 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/deleteEdgeByName.http b/TestRESTful/Edges/deleteEdgeByName.http new file mode 100644 index 0000000..ec96a98 --- /dev/null +++ b/TestRESTful/Edges/deleteEdgeByName.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/edges/byName/v1 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/deleteEdgeByNames.http b/TestRESTful/Edges/deleteEdgeByNames.http new file mode 100644 index 0000000..af95cdf --- /dev/null +++ b/TestRESTful/Edges/deleteEdgeByNames.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/edges/byName?nodeOne=v3&nodeTwo=v4 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/getEdgeById.http b/TestRESTful/Edges/getEdgeById.http new file mode 100644 index 0000000..fc6f9df --- /dev/null +++ b/TestRESTful/Edges/getEdgeById.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/edges/byId/5050 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/getEdgeByNames.http b/TestRESTful/Edges/getEdgeByNames.http new file mode 100644 index 0000000..0b613ab --- /dev/null +++ b/TestRESTful/Edges/getEdgeByNames.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/edges/byName?nodeOne=v1&nodeTwo=v2 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/getEdges.http b/TestRESTful/Edges/getEdges.http new file mode 100644 index 0000000..43fd6fd --- /dev/null +++ b/TestRESTful/Edges/getEdges.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/edges +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Edges/getEdgesByName.http b/TestRESTful/Edges/getEdgesByName.http new file mode 100644 index 0000000..e351509 --- /dev/null +++ b/TestRESTful/Edges/getEdgesByName.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/edges/byName/v1 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/addNode.http b/TestRESTful/Nodes/addNode.http new file mode 100644 index 0000000..7070c69 --- /dev/null +++ b/TestRESTful/Nodes/addNode.http @@ -0,0 +1,15 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/rest/v1/graph/nodes/create +Content-Type: application/json +Cache-Control: no-cache + +{"name":"v6"} + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/addNodes.http b/TestRESTful/Nodes/addNodes.http new file mode 100644 index 0000000..4800432 --- /dev/null +++ b/TestRESTful/Nodes/addNodes.http @@ -0,0 +1,15 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/rest/v1/graph/nodes/create/byBatch +Content-Type: application/json +Cache-Control: no-cache + +[{"name":"6"}, {"name":"v7"}, {"name":"v8"}] + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/deleteAllNodes.http b/TestRESTful/Nodes/deleteAllNodes.http new file mode 100644 index 0000000..4aaecf8 --- /dev/null +++ b/TestRESTful/Nodes/deleteAllNodes.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/nodes +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/deleteNodeById.http b/TestRESTful/Nodes/deleteNodeById.http new file mode 100644 index 0000000..58b2826 --- /dev/null +++ b/TestRESTful/Nodes/deleteNodeById.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/nodes/byId/5010 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/deleteNodeByName.http b/TestRESTful/Nodes/deleteNodeByName.http new file mode 100644 index 0000000..c4868f0 --- /dev/null +++ b/TestRESTful/Nodes/deleteNodeByName.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/nodes/byName/6 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/deleteNodeByObject.http b/TestRESTful/Nodes/deleteNodeByObject.http new file mode 100644 index 0000000..96b90a4 --- /dev/null +++ b/TestRESTful/Nodes/deleteNodeByObject.http @@ -0,0 +1,15 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph/nodes/byObj +Content-Type: application/json +Cache-Control: no-cache + +{"id": 5003, "name": "v4"} + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/getNodeById.http b/TestRESTful/Nodes/getNodeById.http new file mode 100644 index 0000000..3224002 --- /dev/null +++ b/TestRESTful/Nodes/getNodeById.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/nodes/byId/5000 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/getNodeByName.http b/TestRESTful/Nodes/getNodeByName.http new file mode 100644 index 0000000..4373651 --- /dev/null +++ b/TestRESTful/Nodes/getNodeByName.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/nodes/byName/v1 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/Nodes/getNodes.http b/TestRESTful/Nodes/getNodes.http new file mode 100644 index 0000000..6f4f5f3 --- /dev/null +++ b/TestRESTful/Nodes/getNodes.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/nodes +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/addGraph_1.http b/TestRESTful/addGraph_1.http new file mode 100644 index 0000000..629019a --- /dev/null +++ b/TestRESTful/addGraph_1.http @@ -0,0 +1,41 @@ +POST http://localhost:8080/rest/v1/graph/create +Content-Type: application/json +Cache-Control: no-cache + +{ + "nodes": [ + {"name":"v1"}, + {"name":"v2"}, + {"name":"v3"}, + {"name":"v4"}, + {"name":"v5"}, + {"name":"v6"}, + {"name":"v7"}, + {"name":"v8"}, + {"name":"v9"}, + {"name":"v10"} + ], + "edges":[ + {"nodeOne":"v1","nodeTwo":"v2"}, + {"nodeOne":"v1","nodeTwo":"v3"}, + {"nodeOne":"v1","nodeTwo":"v4"}, + {"nodeOne":"v1","nodeTwo":"v5"}, + {"nodeOne":"v2","nodeTwo":"v3"}, + {"nodeOne":"v2","nodeTwo":"v4"}, + {"nodeOne":"v2","nodeTwo":"v5"}, + {"nodeOne":"v3","nodeTwo":"v4"}, + {"nodeOne":"v3","nodeTwo":"v5"}, + {"nodeOne":"v4","nodeTwo":"v5"}, + {"nodeOne":"v6","nodeTwo":"v7"}, + {"nodeOne":"v7","nodeTwo":"v8"}, + {"nodeOne":"v8","nodeTwo":"v9"}, + {"nodeOne":"v8","nodeTwo":"v10"}, + {"nodeOne":"v9","nodeTwo":"v10"}, + {"nodeOne":"v10","nodeTwo":"v7"} + ] +} + +<> 2019-04-05T122503.200.json +<> 2019-04-05T122347.405.html + +### \ No newline at end of file diff --git a/TestRESTful/addGraph_2.http b/TestRESTful/addGraph_2.http new file mode 100644 index 0000000..9cda96b --- /dev/null +++ b/TestRESTful/addGraph_2.http @@ -0,0 +1,27 @@ +POST http://localhost:8080/rest/v1/graph/create +Content-Type: application/json +Cache-Control: no-cache + +{ + "nodes":[ + {"name":"v1"}, + {"name":"v2"}, + {"name":"v3"}, + {"name":"v4"}, + {"name":"v5"} + ], + "edges":[ + {"nodeOne":"v1","nodeTwo":"v2"}, + {"nodeOne":"v2","nodeTwo":"v3"}, + {"nodeOne":"v3","nodeTwo":"v4"}, + {"nodeOne":"v3","nodeTwo":"v5"}, + {"nodeOne":"v5","nodeTwo":"v4"}, + {"nodeOne":"v5","nodeTwo":"v2"} + ] +} + +<> 2019-04-04T124349.200.json +<> 2019-04-03T023130.200.json + +### + diff --git a/TestRESTful/checkRoute.http b/TestRESTful/checkRoute.http new file mode 100644 index 0000000..af08252 --- /dev/null +++ b/TestRESTful/checkRoute.http @@ -0,0 +1,16 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/rest/v1/graph/checkroute +Content-Type: application/json +Cache-Control: no-cache + + +["v1", "v2", "v3"] + + +### \ No newline at end of file diff --git a/TestRESTful/getGraph.http b/TestRESTful/getGraph.http new file mode 100644 index 0000000..79a09a4 --- /dev/null +++ b/TestRESTful/getGraph.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/123 +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/graphViz.http b/TestRESTful/graphViz.http new file mode 100644 index 0000000..6a7bc40 --- /dev/null +++ b/TestRESTful/graphViz.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +GET http://localhost:8080/rest/v1/graph/export +Content-Type: text/html +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/options.http b/TestRESTful/options.http new file mode 100644 index 0000000..49b6369 --- /dev/null +++ b/TestRESTful/options.http @@ -0,0 +1,13 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +OPTIONS http://localhost:8080/rest/v1/graph +Accept: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/TestRESTful/removeGraph.http b/TestRESTful/removeGraph.http new file mode 100644 index 0000000..e337f60 --- /dev/null +++ b/TestRESTful/removeGraph.http @@ -0,0 +1,12 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +DELETE http://localhost:8080/rest/v1/graph +Content-Type: application/json +Cache-Control: no-cache + +### \ No newline at end of file diff --git a/linkchecker_linux.sh b/linkchecker_linux.sh new file mode 100755 index 0000000..fa443a1 --- /dev/null +++ b/linkchecker_linux.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +EXECUTABLE_FILE=linkchecker.jar +PROPERTIES_FILE=application-production.properties + +HELP="Usage: linkchecker_linux [KEY] + +Script without key is run program in DEMO mode. Also vailable next switches: + +--debug - running program in DEMO mode with extended debug information. + +--production - running program in PRODUCTION mode. For running in this mode needed additional +file application-production.properties with PostgreSQL dataset information. Also for this mode +available addition key --debug for runnning program with extended debug information. + +--help - display this is message + +Examples: + +linkchecker_linux - run program in DEMO mode + +linkchecker_linux --debug - run program in DEMO mode with extended debug information. + +linkchecker_linux --production - run program in PRODUCTION mode. + +linkchecker_linux --production --debug - run program in PRODUCTION mode with extended debug information. + +For more information see https://gitlab.com/Aleksandrov/linkchecker/wikis/ +" + +PROPERTIES_FILE_NOT_FOUND=" +WARNING! + +You try run program in PRODUCTION mode. For this mode need PostgreSQL but file +$PROPERTIES_FILE with dataset information is not found. Please fill next information and run program again! + +" + +if [ -f "$EXECUTABLE_FILE" ]; then + if [ -z "$1" ]; then + echo "Running program in DEMO mode" + java -jar linkchecker.jar + else + case "$1" in + --help) + echo "$HELP" + ;; + --debug) + echo "Running program in DEMO mode with extended debug information" + java -jar linkchecker.jar --spring.profiles.active=demo,debug + ;; + --production) + if [ -f "$PROPERTIES_FILE" ]; then + if [ -z "$2" ]; then + echo "Running program in PRODUCTION mode" + java -jar linkchecker.jar --spring.profiles.active=production + else + if [ "$2" = "--debug" ]; then + echo "Running program in PRODUCTION mode with extended debug information" + java -jar linkchecker.jar --spring.profiles.active=production,debug + else + echo "linkchecker: unknown option $2" + echo "Try 'linkchecker --help' for more information." + fi + fi + else + echo "$PROPERTIES_FILE_NOT_FOUND" + printf 'PostgreSQL database host name or IP address (default localhost): ' + read -r LINKCHECKER_PGSQL_DB_HOST + if [ -z "$LINKCHECKER_PGSQL_DB_HOST" ]; then + LINKCHECKER_PGSQL_DB_HOST="jdbc:postgresql://localhost" + else + LINKCHECKER_PGSQL_DB_HOST="jdbc:postgresql://$LINKCHECKER_PGSQL_DB_HOST" + fi + printf 'PostgreSQL database port (default 5432): ' + read -r LINKCHECKER_PGSQL_DB_PORT + if [ -z "$LINKCHECKER_PGSQL_DB_PORT" ]; then + LINKCHECKER_PGSQL_DB_PORT=5432 + fi + printf 'PostgreSQL database name (default linkchecker): ' + read -r LINKCHECKER_PGSQL_DB_NAME + if [ -z "$LINKCHECKER_PGSQL_DB_NAME" ]; then + LINKCHECKER_PGSQL_DB_NAME="linkchecker" + fi + printf 'PostgreSQL database user name: ' + read -r LINKCHECKER_PGSQL_DB_USER + printf 'PostgreSQL database password: ' + read -r -s LINKCHECKER_PGSQL_DB_PASSWORD + echo + touch "$PROPERTIES_FILE" + { + echo "LINKCHECKER_PGSQL_DB_HOST=$LINKCHECKER_PGSQL_DB_HOST" + echo "LINKCHECKER_PGSQL_DB_PORT=$LINKCHECKER_PGSQL_DB_PORT" + echo "LINKCHECKER_PGSQL_DB_NAME=$LINKCHECKER_PGSQL_DB_NAME" + echo "LINKCHECKER_PGSQL_DB_USER=$LINKCHECKER_PGSQL_DB_USER" + echo "LINKCHECKER_PGSQL_DB_PASSWORD=$LINKCHECKER_PGSQL_DB_PASSWORD" + } > "$PROPERTIES_FILE" + fi + ;; + *) + echo "linkchecker_linux: unknown option $1" + echo "Try 'linkchecker_linux --help' for more information." + ;; + esac + fi +else + echo "Executable file linkchecker.jar is not found!" +fi \ No newline at end of file diff --git a/linkchecker_win.bat b/linkchecker_win.bat new file mode 100644 index 0000000..3e0728b --- /dev/null +++ b/linkchecker_win.bat @@ -0,0 +1,136 @@ +@echo off + +set EXECUTABLE_FILE=linkchecker.jar +set PROPERTIES_FILE=application-production.properties + +if not exist %EXECUTABLE_FILE% ( + echo Executable file linkchecker.jar is not found! + exit /b +) + +if ""=="%1" ( + echo Running program in DEMO mode + java -jar %EXECUTABLE_FILE% +) else ( + if "--help"=="%1" goto :Help + if "--debug"=="%1" ( + echo Running program in DEMO mode with extended debug information + java -jar linkchecker.jar --spring.profiles.active=demo,debug + ) else ( + if "--production"=="%1" ( + if exist %PROPERTIES_FILE% ( + if ""=="%2" ( + echo Running program in PRODUCTION mode + java -jar linkchecker.jar --spring.profiles.active=production + ) else ( + if "--debug"=="%2" ( + echo Running program in PRODUCTION mode with extended debug information + java -jar linkchecker.jar --spring.profiles.active=production,debug + ) else goto :KEY_NOT_FOUND %2 + ) + ) else ( + goto :PROPERTIES_FILE_NOT_FOUND + ) + ) else goto :KEY_NOT_FOUND %1 + ) +) +exit /B + +:Help +echo Usage: linkchecker_win [KEY] +echo. +echo Script without key is run program in DEMO mode. Also vailable next switches: +echo. +echo --debug - running program in DEMO mode with extended debug information. +echo. +echo --production - running program in PRODUCTION mode. For running in this mode needed additional file application-production.properties with PostgreSQL dataset information. Also for this mode available addition key --debug for runnning program with extended debug information. +echo. +echo --help - display this is message +echo. +echo Examples: +echo. +echo linkchecker_win - run program in DEMO mode +echo. +echo linkchecker_win --debug - run program in DEMO mode with extended debug information. +echo. +echo linkchecker_win --production - run program in PRODUCTION mode. +echo. +echo linkchecker_win --production --debug - run program in PRODUCTION mode with extended debug information. +echo. +echo For more information see https://gitlab.com/Aleksandrov/linkchecker/wikis/ +exit /b + +:KEY_NOT_FOUND +echo linkchecker_win: unknown option %~1 +echo Try 'linkchecker_win --help' for more information. +exit /b + +:PROPERTIES_FILE_NOT_FOUND +setlocal +echo WARNING! +echo. +echo You try run program in PRODUCTION mode. For this mode need PostgreSQL but file %PROPERTIES_FILE% with dataset information is not found. Please fill next information and run program again! +echo. +set /p PRMT="PostgreSQL database host name or IP address (default localhost): " +if ""=="%PRMT%" ( + set LINKCHECKER_PGSQL_DB_HOST=jdbc:postgresql://localhost +) else ( + set LINKCHECKER_PGSQL_DB_HOST=jdbc:postgresql://%PRMT% +) +set /p PRMT1="PostgreSQL database port (default 5432): " +if ""=="%PRMT1%" set LINKCHECKER_PGSQL_DB_PORT=5432 +set /p PRMT2="PostgreSQL database name (default linkchecker): " +if ""=="%PRMT2%" set LINKCHECKER_PGSQL_DB_NAME=linkchecker +set /p LINKCHECKER_PGSQL_DB_USER="PostgreSQL database user name: " +call :getPassword LINKCHECKER_PGSQL_DB_PASSWORD "PostgreSQL database password: " +echo. +echo LINKCHECKER_PGSQL_DB_HOST=%LINKCHECKER_PGSQL_DB_HOST%>%PROPERTIES_FILE% +echo LINKCHECKER_PGSQL_DB_PORT=%LINKCHECKER_PGSQL_DB_PORT%>>%PROPERTIES_FILE% +echo LINKCHECKER_PGSQL_DB_NAME=%LINKCHECKER_PGSQL_DB_NAME%>>%PROPERTIES_FILE% +echo LINKCHECKER_PGSQL_DB_USER=%LINKCHECKER_PGSQL_DB_USER%>>%PROPERTIES_FILE% +echo LINKCHECKER_PGSQL_DB_PASSWORD=%LINKCHECKER_PGSQL_DB_PASSWORD%>>%PROPERTIES_FILE% +endlocal +exit /b + +::------------------------------------------------------------------------------ +:: Masks user input and returns the input as a variable. +:: Password-masking code based on http://www.dostips.com/forum/viewtopic.php?p=33538#p33538 +:: +:: Arguments: %1 - the variable to store the password in +:: %2 - the prompt to display when receiving input +::------------------------------------------------------------------------------ +:getPassword +set "_password=" + +:: We need a backspace to handle character removal +for /f %%a in ('"prompt;$H&for %%b in (0) do rem"') do set "BS=%%a" + +:: Prompt the user +set /p "=%~2" nul') do if not defined key set "key=%%a" +set "key=%key:~-1%" + +:: If No keypress (enter), then exit +:: If backspace, remove character from password and console +:: Otherwise, add a character to password and go ask for next one +if defined key ( +if "%key%"=="%BS%" ( +if defined _password ( +set "_password=%_password:~0,-1%" +set /p "=!BS! !BS!" \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..fef5a8f --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7b1ee19 --- /dev/null +++ b/pom.xml @@ -0,0 +1,180 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.0.RELEASE + + + ru.resprojects + linkchecker + 0.3.0 + linkchecker + Linkchecker (test project) + + + 1.8 + 1.3.1 + 4.3.1 + 2.8.6 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-json + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + com.google.code.gson + gson + ${gson.version} + + + + + com.h2database + h2 + runtime + + + org.postgresql + postgresql + runtime + + + + + org.webjars + bootstrap + ${bootstrap.version} + + + + + org.jgrapht + jgrapht-core + ${jgrapht.version} + + + org.jgrapht + jgrapht-ext + ${jgrapht.version} + + + org.jgrapht + jgrapht-io + ${jgrapht.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.powermock + powermock-api-mockito2 + 2.0.2 + test + + + org.powermock + powermock-module-junit4 + 2.0.2 + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + **/*Tests.java + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.8 + + + generate-docs + prepare-package + + process-asciidoc + + + html + book + + ${project.build.directory}/generated-snippets + + + + + + + maven-resources-plugin + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/static/docs + + + + ${project.build.directory}/generated-docs + + + + + + + + + + diff --git a/src/main/asciidoc/api-guide-v1.adoc b/src/main/asciidoc/api-guide-v1.adoc new file mode 100644 index 0000000..c353a2c --- /dev/null +++ b/src/main/asciidoc/api-guide-v1.adoc @@ -0,0 +1,152 @@ += Linkchecker API Guide v1 +Test project; +:doctype: book +:toc: +:sectanchors: +:sectlinks: +:toclevels: 5 +:source-highlighter: highlightjs + +[[overview]] += Обзор + +*Linkchecker* - REST-сервис проверяющий работоспособность любой последовательности узлов, хранящихся в неориентированном ациклическом графе. + +API REST-сервиса разбит на следующие категории: + +* *GRAPH API* - набор команд позволяющий работать с графом в целом. +* *NODES API* - набор команд позволящий работать отдельно с вершинами графа. +* *EDGES API* - набор команд позволящий работать отдельно с рёбрами графа. + +[[overview-common]] +== Общая информация + +[[overview-http-verbs]] +=== HTTP методы + +API Linkchecker следует стандартным соглашениям HTTP REST и поддерживает HTTP методы, перечисленные ниже. + +|=== +| Вид запроса | Назначание + +| `GET` +| Используется для запроса содержимого указанного ресурса. + +| `POST` +| Применяется для передачи пользовательских данных заданному ресурсу (создание нового ресурса или передача данных для обработки на стороне сервера). + +| `DELETE` +| Удаляет указанный ресурс. + +| `OPTIONS` +| Предоставляет информацию клиенту о доступных для работы HTTP методов. +|=== + +[[overview-http-status-codes]] +=== Коды состояния HTTP + +В API Linkchecker используются следующие коды состояния HTTP. + +|=== +| Код состояния | Назначение + +| `200 OK` +| успешный запрос. Если клиентом были запрошены какие-либо данные, то они находятся в заголовке и/или теле сообщения. + +| `201 Created` +| В результате успешного выполнения запроса был создан новый ресурс. + +| `204 No Content` +| сервер успешно обработал запрос, но в ответе были переданы только заголовки без тела сообщения. + +| `422 Unprocessable Entity` +| сервер успешно принял запрос, может работать с указанным видом данных (например, в теле запроса находится XML-документ, имеющий верный синтаксис), однако имеется какая-то логическая ошибка, из-за которой невозможно произвести операцию над ресурсом + +| `500 Internal Server Error` +| любая внутренняя ошибка сервера, которая не входит в рамки остальных ошибок класса. +|=== + +[[overview-http-errors]] +=== Ошибки + +В случае возникновения ошибок во время работы REST-сервиса, клиенту будет возвращен структурированный набор данных в формате JSON + +[source,json] +---- +{ + "url": "request_link", + "type": "APP_ERROR[,DATA_NOT_FOUND,DATA_ERROR,VALIDATION_ERROR, WRONG_REQUEST]", + "place": "APP[,GRAPH,NODE,EDGE]", + "messages": [ + "Any message 1", + "Any message 2" + ] +} +---- + +, где + +* *url* - REST-запрос, при котором возникла ошибка +* *type* - тип ошибки +** _APP_ERROR_ - общие ошибки сервиса. +** _DATA_NOT_FOUND_ - ошибки, возникающие при поиске и извлечении данных. +** _DATA_ERROR_ - ошибки, возникающие при обработки данных. +** _VALIDATION_ERROR_ - ошибки, связанные с проверкой данных. +** _WRONG_REQUEST_ - ошибки, возникающие при некорректном запросе. +* *place* - места возникновения ошибок +** _APP_ - сервис +** _GRAPH_ - граф +** _NODE_ - вершины графа +** _EDGE_ - рёбра графа +* *messages* - сообщения об ошибках + +Виды сообщения об ошибках: + +*Обобщенные сообщения об ошибках* + +* Argument must not be null - на вход вместо объекта был подан null +* Collection must not be empty - на вход поступила пустая коллекция данных +* Collection must not contain a null item - на вход поступила коллекция, в которой содержится null элемент +* Collection must have more than one element - в определённых случаях требуется что бы коллекция состояла как минимум из двух элементов (например при работе с рёбрами графа, где требуется указать две вершины графа, которые нужно связать) +* %s with ID = %d is not found - данные с указанным ID не обнаружены, где %s - может быть или NODE или EDGE, %d - номер id'шника. + +*Сообщения об ошибках, возникающие при работе с рёбрами графа* + +* Edge for nodes [%s, %s] is not found - ребро для указанных вершин (нод) не найдено, [%s, %s] - уникальные имена пары вершин. +* Edge for nodes ([%s, %s], [%s, %s]) already present in the graph - данное сообщение возникает при попытки добавить в граф уже существующее ребро. Где ([%s, %s], [%s, %s]) - подставляются уникальные имена вершин графа. Так как граф неориентированный, то например v1 и v2 <=> v2 и v1. +* Edges for node %s is not found - данное сообщение возникает при попытки извлечь информацию по рёбрам графа для заданной вершины, %s - уникальное имя вершины графа. + +*Сообщения об ошибках, возникающие при работе с вершинами (нодами) графа* + +* Node %s already present in the graph - данное сообщение возникает, при попытки добавить вершину, которая уже присутствует в графе. +* Error while update node with id = - сообщение возникает при неудачной попытки обновить данные по вершине графа, где id - номер идентификатора вершины. +* Node with NAME = %s is not found - сообщение возникает, при неудачном поиске вершины графа по её уникальному имени. +* Node %s is not found - сообщение возникает, при неудачном поиске вершины графа в режиме поиска по заданному объекту. +* Nodes %s and %s are not reachable to each other - сообщение возникает если один из узлов (вершин графа) не достижим до другого узла (т.е. имеются промежуточные узлы). +* Node %s is fault - сообщение возникает в случае генерации сбоя в узле (в вершине графа), т.е. узел оказался недоступным во время обхода по узлам. + +[[rest-api-usage]] += REST API + +Далее - описание отдельных частей API REST-сервиса. + +[[rest-api-usage-graph]] +== GRAPH API + +Набор запросов при работе с графом в целом. + +include::graph.adoc[] + +[[rest-api-usage-nodes]] +== NODES API + +Набор запросов при работе с вершинами графа (нодами). + +include::nodes.adoc[] + +[[rest-api-usage-edges]] +== EDGES API + +Набор запросов при работе с рёбрами графа. + +include::edges.adoc[] \ No newline at end of file diff --git a/src/main/asciidoc/edges.adoc b/src/main/asciidoc/edges.adoc new file mode 100644 index 0000000..da659e6 --- /dev/null +++ b/src/main/asciidoc/edges.adoc @@ -0,0 +1,468 @@ +=== HTTP запросы для поиска рёбер графа +==== Получить список всех рёбер графа. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edges/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edges/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edges/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edges/http-response.adoc[] + +*Response fields* +include::{snippets}/get-edges/response-fields.adoc[] + +==== HTTP запросы для поиска рёбер графа. +===== Поиск ребра графа по его идентификатору. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edge-by-id/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edge-by-id/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edge-by-id/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edge-by-id/http-response.adoc[] + +*Response fields* +include::{snippets}/get-edge-by-id/response-fields.adoc[] + +===== Поиск рёбер графа в которых встречается уникальное имя вершины графа заданное в качестве параметра поиска + +В поиске используется уникальное имя вершины графа. В отбор попадают все ребра графа в которых встречается +уникальное имя вершины графа, заданное в качестве параметра поиска. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edges-by-node-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edges-by-node-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edges-by-node-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edges-by-node-name/http-response.adoc[] + +*Response fields* +include::{snippets}/get-edges-by-node-name/response-fields.adoc[] + +===== Поиск ребра графа по паре уникальных имен вершин графа, которые связывает искомое ребро + +Поиск ребра ведётся по конкретному набору вершин, которые связывает искомое ребро. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edge-by-nodes-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edge-by-nodes-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edge-by-nodes-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edge-by-nodes-name/http-response.adoc[] + +*Response fields* +include::{snippets}/get-edge-by-nodes-name/response-fields.adoc[] + +==== Примеры ошибок при поиске рёбер графа +===== Ошибка поиска ребра графа по идентификатору +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edge-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edge-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edge-exception-1/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edge-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/get-edge-exception-1/response-fields.adoc[] + +===== Ошибка поиска рёбер графа по уникальному имени вершины графа +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-edge-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-edge-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-edge-exception-2/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-edge-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/get-edge-exception-2/response-fields.adoc[] + +=== HTTP запросы добавления новых рёбер в граф +==== Создать и добавить новое ребро в граф + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge/httpie-request.adoc[] + +*Request body* +include::{snippets}/create-edge/request-body.adoc[] +*Request fields* +include::{snippets}/create-edge/request-fields.adoc[] + +*Пример ответа* + +*HTTP response* +include::{snippets}/create-edge/http-response.adoc[] +*Response body* +include::{snippets}/create-edge/response-body.adoc[] +*Response fields* +include::{snippets}/create-edge/response-fields.adoc[] + +==== Создать и добавить новые ребра в граф + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edges/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edges/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edges/httpie-request.adoc[] + +*Request body* +include::{snippets}/create-edges/request-body.adoc[] +*Request fields* +include::{snippets}/create-edges/request-fields.adoc[] + +*Пример ответа* + +*HTTP response* +include::{snippets}/create-edges/http-response.adoc[] +*Response body* +include::{snippets}/create-edges/response-body.adoc[] +*Response fields* +include::{snippets}/create-edges/response-fields.adoc[] + +==== Примеры ошибок возникающие при добавлении рёбер в граф +===== Ошибка валидации при добавлении ребра в граф + +Попытка создать новый объект ребра графа не указав имена связываемых ребром вершин графа. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge-exception-1/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-edge-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/create-edge-exception-1/response-fields.adoc[] + +===== Ошибка при создания ребра, который уже присутствует в графе + +Попытка создать новый объект ребра, который уже присутствует в графе. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge-exception-2/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-edge-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/create-edge-exception-2/response-fields.adoc[] + +===== Ошибка создания набора рёбер графа используя пустой список + +Попытка создать и добавить в граф набор рёбер графа используя пустой список. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge-exception-3/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge-exception-3/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge-exception-3/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-edge-exception-3/http-response.adoc[] +*Response fields* +include::{snippets}/create-edge-exception-3/response-fields.adoc[] + +===== Ошибка создания набора вершин рёбер используя список содержащий null-элемент + +Попытка создать и добавить в граф набор рёбер графа используя список содержащий null-элемент. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge-exception-4/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge-exception-4/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge-exception-4/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-edge-exception-4/http-response.adoc[] +*Response fields* +include::{snippets}/create-edge-exception-4/response-fields.adoc[] + +===== Ошибка создания набора рёбер графа используя список содержащий существующее ребро в графе + +Попытка создать и добавить в граф набор рёбер графа используя список содержащий существующее ребро в графе. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-edge-exception-5/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-edge-exception-5/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-edge-exception-5/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-edge-exception-5/http-response.adoc[] +*Response fields* +include::{snippets}/create-edge-exception-5/response-fields.adoc[] + +=== HTTP запросы для удаления рёбер графа +==== Удалить все рёбра графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-all-edges/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-all-edges/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-all-edges/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-all-edges/http-response.adoc[] + +==== HTTP запросы для удаления ребра графа +===== Поиск и удаление ребра графа по идентификатору + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edge-by-id/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edge-by-id/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edge-by-id/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edge-by-id/http-response.adoc[] + +===== Поиск и удаление всех рёбер графа которые содержат указанное в качестве параметра поиска имя вершины графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edges-by-node-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edges-by-node-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edges-by-node-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edges-by-node-name/http-response.adoc[] + +===== Поиск и удаление ребра графа по именам вершин графа, которые искомое ребро соединяет + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edge-by-nodes-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edge-by-nodes-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edge-by-nodes-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edge-by-nodes-name/http-response.adoc[] + +==== Примеры ошибок возникающие при удалении рёбер графа +===== Ошибка при поиске и удалении ребра графа по идентификатору + +Попытка найти и удалить ребро графа используя идентификатор + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edge-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edge-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edge-exception-1/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edge-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/delete-edge-exception-1/response-fields.adoc[] + +===== Ошибка при поиске и удалении рёбр графа которые не содержат указанное в качестве параметра поиска имя вершины графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edge-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edge-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edge-exception-2/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edge-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/delete-edge-exception-2/response-fields.adoc[] + +===== Ошибка при поиске и удалении ребра графа по именам вершин графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-edge-exception-3/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-edge-exception-3/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-edge-exception-3/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-edge-exception-3/http-response.adoc[] +*Response fields* +include::{snippets}/delete-edge-exception-3/response-fields.adoc[] diff --git a/src/main/asciidoc/graph.adoc b/src/main/asciidoc/graph.adoc new file mode 100644 index 0000000..de9c93a --- /dev/null +++ b/src/main/asciidoc/graph.adoc @@ -0,0 +1,215 @@ +=== Получить объект графа. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-graph/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-graph/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-graph/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-graph/http-response.adoc[] + +*HTTP response fields* +include::{snippets}/get-graph/response-fields.adoc[] + +=== Экспорт графа в формат https://www.graphviz.org/about/[graphviz]. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/export-graph/curl-request.adoc[] +*HTTP request* +include::{snippets}/export-graph/http-request.adoc[] +*HTTPie request* +include::{snippets}/export-graph/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/export-graph/http-response.adoc[] + +=== Создание нового графа + +При этом прежний граф полностью удаляется из БД. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-graph/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-graph/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-graph/httpie-request.adoc[] + +*Request body* +include::{snippets}/create-graph/request-body.adoc[] +*Request fields* +include::{snippets}/create-graph/request-fields.adoc[] + +*Пример ответа* + +*HTTP response* +include::{snippets}/create-graph/http-response.adoc[] +*Response body* +include::{snippets}/create-graph/response-body.adoc[] +*Response fields* +include::{snippets}/create-graph/response-fields.adoc[] + +==== Примеры ошибок +===== Ошибка при создании нового графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-graph-exception/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-graph-exception/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-graph-exception/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-graph-exception/http-response.adoc[] +*Response body* +include::{snippets}/create-graph-exception/response-body.adoc[] +*Response fields* +include::{snippets}/create-graph-exception/response-fields.adoc[] + +=== Удалить графа из БД + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-graph/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-graph/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-graph/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-graph/http-response.adoc[] + +=== Проверка работоспособности заданной последовательности узлов. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/checkroute-graph/curl-request.adoc[] +*HTTP request* +include::{snippets}/checkroute-graph/http-request.adoc[] +*HTTPie request* +include::{snippets}/checkroute-graph/httpie-request.adoc[] + +*Request body* +include::{snippets}/checkroute-graph/request-body.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/checkroute-graph/http-response.adoc[] +*Response body* +include::{snippets}/checkroute-graph/response-body.adoc[] + +==== Примеры ошибок +===== Пустая входная коллекция + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/checkroute-graph-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/checkroute-graph-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/checkroute-graph-exception-1/httpie-request.adoc[] + +*Request body* +include::{snippets}/checkroute-graph-exception-1/request-body.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/checkroute-graph-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/checkroute-graph-exception-1/response-fields.adoc[] + +===== Входная коллекция состоящая из одного элемента + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/checkroute-graph-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/checkroute-graph-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/checkroute-graph-exception-2/httpie-request.adoc[] + +*Request body* +include::{snippets}/checkroute-graph-exception-2/request-body.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/checkroute-graph-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/checkroute-graph-exception-2/response-fields.adoc[] + +===== Путь между входными узлами не найден + +*Пример запроса* + +*CURL request* +include::{snippets}/checkroute-graph-exception-3/curl-request.adoc[] +*HTTP request* +include::{snippets}/checkroute-graph-exception-3/http-request.adoc[] +*HTTPie request* +include::{snippets}/checkroute-graph-exception-3/httpie-request.adoc[] + +*Request body* +include::{snippets}/checkroute-graph-exception-3/request-body.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/checkroute-graph-exception-3/http-response.adoc[] +*Response fields* +include::{snippets}/checkroute-graph-exception-3/response-fields.adoc[] \ No newline at end of file diff --git a/src/main/asciidoc/nodes.adoc b/src/main/asciidoc/nodes.adoc new file mode 100644 index 0000000..e794a01 --- /dev/null +++ b/src/main/asciidoc/nodes.adoc @@ -0,0 +1,473 @@ +=== HTTP запросы для поиска вершин графа +==== Получить список всех вершин графа. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-nodes/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-nodes/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-nodes/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-nodes/http-response.adoc[] + +*Response fields* +include::{snippets}/get-nodes/response-fields.adoc[] + +==== HTTP запросы поиска вершины графа. +===== Найти вершину графа по её идентификатору. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-node-by-id/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-node-by-id/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-node-by-id/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-node-by-id/http-response.adoc[] + +*Response fields* +include::{snippets}/get-node-by-id/response-fields.adoc[] + +===== Найти вершину графа по её уникальному имени. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-node-by-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-node-by-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-node-by-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-node-by-name/http-response.adoc[] + +*Response fields* +include::{snippets}/get-node-by-name/response-fields.adoc[] + +==== Примеры ошибок при поиске вершин(ы) графа +===== Ошибка поиска вершины графа по уникальному имени +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/get-node-by-name-exception/curl-request.adoc[] +*HTTP request* +include::{snippets}/get-node-by-name-exception/http-request.adoc[] +*HTTPie request* +include::{snippets}/get-node-by-name-exception/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/get-node-by-name-exception/http-response.adoc[] +*Response fields* +include::{snippets}/get-node-by-name-exception/response-fields.adoc[] + +=== HTTP запросы добавления новых вершин в граф +==== Создать и добавить новую вершину в граф + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node/httpie-request.adoc[] + +*Request body* +include::{snippets}/create-node/request-body.adoc[] +*Request fields* +include::{snippets}/create-node/request-fields.adoc[] + +*Пример ответа* + +*HTTP response* +include::{snippets}/create-node/http-response.adoc[] +*Response body* +include::{snippets}/create-node/response-body.adoc[] +*Response fields* +include::{snippets}/create-node/response-fields.adoc[] + +==== Создать и добавить новые вершины в граф + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-nodes/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-nodes/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-nodes/httpie-request.adoc[] + +*Request body* +include::{snippets}/create-nodes/request-body.adoc[] +*Request fields* +include::{snippets}/create-nodes/request-fields.adoc[] + +*Пример ответа* + +*HTTP response* +include::{snippets}/create-nodes/http-response.adoc[] +*Response body* +include::{snippets}/create-nodes/response-body.adoc[] +*Response fields* +include::{snippets}/create-nodes/response-fields.adoc[] + +==== Примеры ошибок возникающие при добавлении вершин в граф +===== Ошибка валидации при добавлении вершины в граф + +Попытка создать новый объект вершины графа с пустым уникальным именем. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node-exception-1/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-node-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/create-node-exception-1/response-fields.adoc[] + +===== Ошибка создания уже существующей вершины + +Попытка создать новый объект вершины графа с уникальным именем, который уже существует в графе. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node-exception-2/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-node-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/create-node-exception-2/response-fields.adoc[] + +===== Ошибка создания набора вершин графа используя пустой список + +Попытка создать и добавить в граф набор вершин графа используя пустой список. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node-exception-3/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node-exception-3/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node-exception-3/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-node-exception-3/http-response.adoc[] +*Response fields* +include::{snippets}/create-node-exception-3/response-fields.adoc[] + +===== Ошибка создания набора вершин графа используя список содержащий null-элемент + +Попытка создать и добавить в граф набор вершин графа используя список содержащий null-элемент. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node-exception-4/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node-exception-4/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node-exception-4/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-node-exception-4/http-response.adoc[] +*Response fields* +include::{snippets}/create-node-exception-4/response-fields.adoc[] + +===== Ошибка создания набора вершин графа используя список содержащий существующую вершину в графе + +Попытка создать и добавить в граф набор вершин графа используя список содержащий существующую вершину в графе. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/create-node-exception-5/curl-request.adoc[] +*HTTP request* +include::{snippets}/create-node-exception-5/http-request.adoc[] +*HTTPie request* +include::{snippets}/create-node-exception-5/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/create-node-exception-5/http-response.adoc[] +*Response fields* +include::{snippets}/create-node-exception-5/response-fields.adoc[] + +=== HTTP запросы для удаления вершин графа +==== Удалить все вершины графа + +При удалении вершин графа происходит автоматическое удаление рёбер графа. Данный запрос эквивалентен запросу удаления всего графа. + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-all-nodes/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-all-nodes/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-all-nodes/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-all-nodes/http-response.adoc[] + +==== HTTP запросы для удаления вершины графа +===== Поиск и удаление вершины графа по идентификатору + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-by-id/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-by-id/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-by-id/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-by-id/http-response.adoc[] + +===== Поиск и удаление вершины графа по имени + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-by-name/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-by-name/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-by-name/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-by-name/http-response.adoc[] + +===== Поиск и удаление вершины графа по объекту + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-by-obj/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-by-obj/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-by-obj/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-by-obj/http-response.adoc[] + +==== Примеры ошибок возникающие при удалении вершин графа +===== Ошибка при поиске и удалении вершины графа по идентификатору + +Попытка найти и удалить вершину графа используя идентификатор + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-exception-1/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-exception-1/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-exception-1/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-exception-1/http-response.adoc[] +*Response fields* +include::{snippets}/delete-node-exception-1/response-fields.adoc[] + +===== Ошибка при поиске и удалении вершины графа по имени + +Попытка найти и удалить вершину графа используя уникальное имя + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-exception-2/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-exception-2/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-exception-2/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-exception-2/http-response.adoc[] +*Response fields* +include::{snippets}/delete-node-exception-2/response-fields.adoc[] + +===== Ошибка при поиске и удалении вершины графа по объекту вершины графа у которого id = null + +Попытка найти и удалить вершину графа используя объект вершины графа у которого id = null + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-exception-3/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-exception-3/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-exception-3/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-exception-3/http-response.adoc[] +*Response fields* +include::{snippets}/delete-node-exception-3/response-fields.adoc[] + +===== Ошибка при поиске и удалении вершины графа по объекту несуществующей вершины графа + +Попытка найти и удалить вершину графа используя объект несуществующей вершины графа + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-exception-4/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-exception-4/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-exception-4/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-exception-4/http-response.adoc[] +*Response fields* +include::{snippets}/delete-node-exception-4/response-fields.adoc[] + +===== Ошибка при поиске и удалении вершины графа по объекту вершины графа у которого указан некорректный id + +Попытка найти и удалить вершину графа используя объект вершины графа у которого указан некорректный id + +*Пример запроса* + +''' + +*CURL request* +include::{snippets}/delete-node-exception-5/curl-request.adoc[] +*HTTP request* +include::{snippets}/delete-node-exception-5/http-request.adoc[] +*HTTPie request* +include::{snippets}/delete-node-exception-5/httpie-request.adoc[] + +*Пример ответа* + +''' + +*HTTP response* +include::{snippets}/delete-node-exception-5/http-response.adoc[] +*Response fields* +include::{snippets}/delete-node-exception-5/response-fields.adoc[] diff --git a/src/main/java/ru/resprojects/linkchecker/AppProperties.java b/src/main/java/ru/resprojects/linkchecker/AppProperties.java new file mode 100644 index 0000000..d047dfd --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/AppProperties.java @@ -0,0 +1,52 @@ +package ru.resprojects.linkchecker; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Data class for application error string messages. These messages is loaded + * from "appmsg" block in the application.yml file + * + * Messages is divided on three type: + * + * AppMsg - error messages is related with application as whole. + * NodeMsg - error messages is related with nodes in the graph. + * EdgeMsg - error messages is related with edges int the graph. + */ +@Component +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "appmsg") +public class AppProperties { + + private Map appMsg = new HashMap<>(); + private Map nodeMsg = new HashMap<>(); + private Map edgeMsg = new HashMap<>(); + + public Map getAppMsg() { + return appMsg; + } + + public Map getNodeMsg() { + return nodeMsg; + } + + public Map getEdgeMsg() { + return edgeMsg; + } + + public void setAppMsg(Map appMsg) { + this.appMsg = appMsg; + } + + public void setNodeMsg(Map nodeMsg) { + this.nodeMsg = nodeMsg; + } + + public void setEdgeMsg(Map edgeMsg) { + this.edgeMsg = edgeMsg; + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/HasId.java b/src/main/java/ru/resprojects/linkchecker/HasId.java new file mode 100644 index 0000000..715aba4 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/HasId.java @@ -0,0 +1,20 @@ +package ru.resprojects.linkchecker; + +/** + * The interface indicates that the object has an identifier. + */ +public interface HasId { + + /** + * Get entity id. + * @return entity id. + */ + Integer getId(); + + /** + * Set entity id. + * @param id of entity. + */ + void setId(Integer id); + +} diff --git a/src/main/java/ru/resprojects/linkchecker/LinkcheckerApplication.java b/src/main/java/ru/resprojects/linkchecker/LinkcheckerApplication.java new file mode 100644 index 0000000..67c183b --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/LinkcheckerApplication.java @@ -0,0 +1,13 @@ +package ru.resprojects.linkchecker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LinkcheckerApplication { + + public static void main(String[] args) { + SpringApplication.run(LinkcheckerApplication.class, args); + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/dto/BaseDto.java b/src/main/java/ru/resprojects/linkchecker/dto/BaseDto.java new file mode 100644 index 0000000..c545a6d --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/dto/BaseDto.java @@ -0,0 +1,39 @@ +package ru.resprojects.linkchecker.dto; + +import ru.resprojects.linkchecker.HasId; + +/** + * Abstract base class for data transfer object. + */ +abstract public class BaseDto implements HasId { + + /** + * ID of transport object. + */ + protected Integer id; + + /** + * Default ctor. + */ + BaseDto() { + } + + /** + * Ctor. + * @param id of transport object. + */ + BaseDto(final Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public void setId(final Integer id) { + this.id = id; + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/dto/GraphDto.java b/src/main/java/ru/resprojects/linkchecker/dto/GraphDto.java new file mode 100644 index 0000000..363d2eb --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/dto/GraphDto.java @@ -0,0 +1,281 @@ +package ru.resprojects.linkchecker.dto; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static ru.resprojects.linkchecker.util.ValidationUtil.*; + +/** + * Class for transfer object that implements graph. + */ +public class GraphDto { + + /** + * Class for transfer object that implements graph node. + */ + public static class NodeGraph extends BaseDto { + + /** + * Unique graph node name. + */ + @NotBlank(message = VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE) + @Size(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE, + message = VALIDATOR_NODE_NAME_RANGE_MESSAGE) + private String name; + + /** + * The number of passes through the graph node. + */ + private int counter; + + /** + * Default ctor. + */ + public NodeGraph() { + } + + /** + * Ctor. + * @param name - unique graph node name. + */ + public NodeGraph(final String name) { + this(null, name, NODE_COUNTER_DEFAULT); + } + + /** + * Ctor. + * @param id - identity number of graph node. + * @param name - unique graph node name. + * @param counter - the number of passes through the current node. + */ + public NodeGraph(final Integer id, final String name, final int counter) { + super(id); + this.name = name; + this.counter = counter; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public int getCounter() { + return counter; + } + + public void setCounter(final int counter) { + this.counter = counter; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NodeGraph nodeGraph = (NodeGraph) o; + return Objects.equals(id, nodeGraph.id) + && counter == nodeGraph.counter + && name.equals(nodeGraph.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, counter); + } + + @Override + public String toString() { + return "{" + + "\"id\": \"" + id + '"' + + ", \"name\": \"" + name + '"' + + ", \"counter\":" + counter + + '}'; + } + } + + /** + * Class for transport object that implements undirected graph edge. + */ + public static class EdgeGraph extends BaseDto { + + /** + * Unique name of first graph node. + */ + @NotBlank(message = "nodeOne: " + VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE) + @Size(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE) + private String nodeOne; + + /** + * Unique name of second graph node. + */ + @NotBlank(message = "nodeTwo: " + VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE) + @Size(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE) + private String nodeTwo; + + /** + * Default ctor. + */ + public EdgeGraph() { + } + + /** + * Ctor. + * @param edge - object of undirected graph edge. + */ + public EdgeGraph(final EdgeGraph edge) { + this(edge.getId(), edge.getNodeOne(), edge.getNodeTwo()); + } + + + /** + * Ctor. + * @param nodeOne - unique name of first graph node. + * @param nodeTwo - unique name of second graph node. + */ + public EdgeGraph(final String nodeOne, final String nodeTwo) { + this(null, nodeOne, nodeTwo); + } + + /** + * Ctor. + * @param nodeOne - object of first graph node. + * @param nodeTwo - object of second graph node. + */ + public EdgeGraph(final NodeGraph nodeOne, final NodeGraph nodeTwo) { + this(null, nodeOne.getName(), nodeTwo.getName()); + } + + /** + * Ctor. + * @param id - identity number of graph edge. + * @param nodeOne - unique name of first graph node. + * @param nodeTwo - unique name of second graph node. + */ + public EdgeGraph(final Integer id, final String nodeOne, final String nodeTwo) { + super(id); + this.nodeOne = nodeOne; + this.nodeTwo = nodeTwo; + } + + /** + * Ctor. + * @param id - identity number of graph edge. + * @param nodeOne - object of first graph node. + * @param nodeTwo - object of second graph node. + */ + public EdgeGraph(final Integer id, final NodeGraph nodeOne, final NodeGraph nodeTwo) { + super(id); + this.nodeOne = nodeOne.getName(); + this.nodeTwo = nodeTwo.getName(); + } + + + public String getNodeOne() { + return nodeOne; + } + + + public void setNodeOne(final String nodeOne) { + this.nodeOne = nodeOne; + } + + public String getNodeTwo() { + return nodeTwo; + } + + public void setNodeTwo(final String nodeTwo) { + this.nodeTwo = nodeTwo; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EdgeGraph edgeGraph = (EdgeGraph) o; + return nodeOne.equals(edgeGraph.nodeOne) + && nodeTwo.equals(edgeGraph.nodeTwo) + && Objects.equals(id, edgeGraph.id); + } + + @Override + public int hashCode() { + return Objects.hash(id, nodeOne, nodeTwo); + } + + @Override + public String toString() { + return "{" + + "\"id\": \"" + id + '"' + + ", \"nodeOne\": \"" + + nodeOne + '"' + + ", \"nodeTwo\": \"" + + nodeTwo + '"' + + '}'; + } + } + + /** + * Collection of graph nodes. + */ + private Set<@Valid NodeGraph> nodes = new HashSet<>(); + + /** + * Collection of graph edges. + */ + private Set<@Valid EdgeGraph> edges = new HashSet<>(); + + /** + * Default ctor. + */ + public GraphDto() { + } + + /** + * Ctor. + * @param nodes - collection of graph nodes {@link NodeGraph}. + * @param edges - collection of graph edges {@link EdgeGraph}. + */ + public GraphDto(final Set nodes, final Set edges) { + this.nodes = nodes; + this.edges = edges; + } + + public Set getNodes() { + return nodes; + } + + public void setNodes(final Set nodes) { + this.nodes = nodes; + } + + + public Set getEdges() { + return edges; + } + + public void setEdges(final Set edges) { + this.edges = edges; + } + + @Override + public final String toString() { + return "{" + + "\"nodes\": " + + nodes + + ", \"edges\": " + + edges + + '}'; + } + + +} diff --git a/src/main/java/ru/resprojects/linkchecker/model/AbstractBaseEntity.java b/src/main/java/ru/resprojects/linkchecker/model/AbstractBaseEntity.java new file mode 100644 index 0000000..c51fdb1 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/model/AbstractBaseEntity.java @@ -0,0 +1,92 @@ +package ru.resprojects.linkchecker.model; + +import org.hibernate.Hibernate; +import ru.resprojects.linkchecker.HasId; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.SequenceGenerator; +import java.io.Serializable; + +/** + * Abstract base class for database entity. + */ +@MappedSuperclass +public abstract class AbstractBaseEntity implements HasId, Serializable { + + /** + * + */ + private static final long serialVersionUID = -5006383911943128194L; + + /** + * Initial number for database id sequential. + */ + private static final int START_SEQ = 5000; + + /** + * Unique identity for database entity. + */ + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", + allocationSize = 1, initialValue = START_SEQ) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") + protected Integer id; + + /** + * Default ctor. + */ + AbstractBaseEntity() { + } + + /** + * Ctor. + * @param id - unique database entity identity. + */ + AbstractBaseEntity(final Integer id) { + this.id = id; + } + + /** + * Get unique database entity identity. + * @return unique database entity identity. + */ + @Override + public Integer getId() { + return id; + } + + /** + * Set unique database entity identity. + * @param id of database entity. + */ + @Override + public void setId(final Integer id) { + this.id = id; + } + + @Override + public String toString() { + return String.format("Entity %s (%s)", getClass().getName(), id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(Hibernate.getClass(o))) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : id; + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/model/AbstractNamedEntity.java b/src/main/java/ru/resprojects/linkchecker/model/AbstractNamedEntity.java new file mode 100644 index 0000000..f8b9441 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/model/AbstractNamedEntity.java @@ -0,0 +1,67 @@ +package ru.resprojects.linkchecker.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import static ru.resprojects.linkchecker.util.ValidationUtil.*; + +/** + * Abstract base class for named database entity. + */ +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + /** + * + */ + private static final long serialVersionUID = 6679903940869239340L; + /** + * Unique name of database entity. + */ + @Column(name = "name", nullable = false, unique = true) + @NotBlank(message = VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE) + @Size(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE, + message = VALIDATOR_NODE_NAME_RANGE_MESSAGE) + private String name; + + /** + * Default ctor. + */ + AbstractNamedEntity() { + } + + /** + * Ctor. + * @param id of database entity. + * @param name unique name of database entity. + */ + AbstractNamedEntity(final Integer id, final String name) { + super(id); + this.name = name; + } + + /** + * Set unique name of database entity. + * @param name unique name of database entity. + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Get unique name of database entity. + * @return unique name of database entity. + */ + public String getName() { + return this.name; + } + + @Override + public String toString() { + return String.format("Entity %s (%s, '%s')", + getClass().getName(), getId(), name); + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/model/Edge.java b/src/main/java/ru/resprojects/linkchecker/model/Edge.java new file mode 100644 index 0000000..ba95513 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/model/Edge.java @@ -0,0 +1,108 @@ +package ru.resprojects.linkchecker.model; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import ru.resprojects.linkchecker.util.ValidationUtil; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +@Entity +@Table(name = "edges", uniqueConstraints = { + @UniqueConstraint( + columnNames = {"nodeone", "nodetwo"}, + name = "unique_edge" + ) +}) +public class Edge extends AbstractBaseEntity { + + /** + * + */ + private static final long serialVersionUID = -5267484684381196999L; + + /** + * First object of graph node. + */ + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "nodeone", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull(message = ValidationUtil.VALIDATOR_NOT_NULL_MESSAGE) + private Node nodeOne; + + /** + * Second object of graph node. + */ + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "nodetwo", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull(message = ValidationUtil.VALIDATOR_NOT_NULL_MESSAGE) + private Node nodeTwo; + + /** + * Default ctor. + */ + public Edge() { + } + + /** + * Ctor. + * @param edge - object of undirected graph edge. + */ + public Edge(final Edge edge) { + this(edge.getId(), edge.getNodeOne(), edge.getNodeTwo()); + } + + /** + * Ctor. + * @param nodeOne - first object of graph node. + * @param nodeTwo - second object of graph node. + */ + public Edge(final Node nodeOne, final Node nodeTwo) { + this(null, nodeOne, nodeTwo); + } + + /** + * Ctor. + * @param id - unique identity for database graph edge entity. + * @param nodeOne - first object of graph node. + * @param nodeTwo - second object of graph node. + */ + public Edge(final Integer id, final Node nodeOne, final Node nodeTwo) { + this.id = id; + this.nodeOne = nodeOne; + this.nodeTwo = nodeTwo; + } + + public Node getNodeOne() { + return nodeOne; + } + + public void setNodeOne(final Node nodeOne) { + this.nodeOne = nodeOne; + } + + public Node getNodeTwo() { + return nodeTwo; + } + + public void setNodeTwo(final Node nodeTwo) { + this.nodeTwo = nodeTwo; + } + + @Override + public String toString() { + return "Edge{" + + "id=" + getId() + + ", nodeOne=" + + nodeOne + + ", nodeTwo=" + + nodeTwo + + '}'; + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/model/Node.java b/src/main/java/ru/resprojects/linkchecker/model/Node.java new file mode 100644 index 0000000..5964c0b --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/model/Node.java @@ -0,0 +1,82 @@ +package ru.resprojects.linkchecker.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import static ru.resprojects.linkchecker.util.ValidationUtil.*; + +@Entity +@Table(name = "nodes", uniqueConstraints = { + @UniqueConstraint(columnNames = "name", name = "nodes_unique_name_idx") +}) +public class Node extends AbstractNamedEntity { + + /** + * + */ + private static final long serialVersionUID = 5592849608806842551L; + + private static final String DEFAULT_COUNTER_VALUE = "int default " + + NODE_COUNTER_DEFAULT; + + /** + * The number of passes through the node of the graph. + */ + @Column(name = "counter", columnDefinition = DEFAULT_COUNTER_VALUE) + private int counter; + + /** + * Default ctor. + */ + public Node() { + } + + /** + * Ctor. + * @param node - node of the graph database entity. + */ + public Node(final Node node) { + this(node.getId(), node.getName(), node.getCounter()); + } + + /** + * Ctor. + * @param name - unique name of database entity that implement graph node. + */ + public Node(final String name) { + super(null, name); + this.counter = NODE_COUNTER_DEFAULT; + } + + /** + * Ctor. + * @param id - unique identity for database graph node entity. + * @param name - unique name of database entity that implement graph node. + * @param counter - number of passes through the node of the graph. + */ + public Node(final Integer id, final String name, final int counter) { + super(id, name); + this.counter = counter; + } + + public int getCounter() { + return counter; + } + + public void setCounter(final int counter) { + this.counter = counter; + } + + @Override + public String toString() { + return "Node{" + + "id=" + getId() + + ", name='" + getName() + '\'' + + ", counter=" + counter + + '}'; + } + + +} diff --git a/src/main/java/ru/resprojects/linkchecker/repositories/EdgeRepository.java b/src/main/java/ru/resprojects/linkchecker/repositories/EdgeRepository.java new file mode 100644 index 0000000..2082314 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/repositories/EdgeRepository.java @@ -0,0 +1,36 @@ +package ru.resprojects.linkchecker.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; + +import java.util.List; +import java.util.Optional; + +@Transactional(readOnly = true) +public interface EdgeRepository extends JpaRepository { + + @Transactional + S save(S edge); + + @Transactional + List saveAll(Iterable entities); + + @Transactional + void deleteById(int id); + + @Transactional + void deleteAllInBatch(); + + @Transactional + void deleteInBatch(Iterable entities); + + Optional findEdgeByNodeOneAndNodeTwo(Node nodeOne, Node nodeTwo); + + List findEdgesByNodeOneOrNodeTwo(Node nodeOne, Node nodeTwo); + + boolean existsById(int id); + +} + diff --git a/src/main/java/ru/resprojects/linkchecker/repositories/NodeRepository.java b/src/main/java/ru/resprojects/linkchecker/repositories/NodeRepository.java new file mode 100644 index 0000000..175716d --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/repositories/NodeRepository.java @@ -0,0 +1,30 @@ +package ru.resprojects.linkchecker.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; +import ru.resprojects.linkchecker.model.Node; + +import java.util.List; + +@Transactional(readOnly = true) +public interface NodeRepository extends JpaRepository { + + @Transactional + S save(S node); + + @Transactional + List saveAll(Iterable entities); + + @Transactional + void deleteById(Integer id); + + @Transactional + void deleteByName(String name); + + @Transactional + void deleteAllInBatch(); + + Node getByName(String name); + + boolean existsByName(String name); +} diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeService.java b/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeService.java new file mode 100644 index 0000000..4e61561 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeService.java @@ -0,0 +1,98 @@ +package ru.resprojects.linkchecker.services; + +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.Set; + +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + +/** + * GraphEdgeService - the interface for work with graph edges. + */ +public interface GraphEdgeService { + + /** + * Create edge of the graph. Nodes that linked by the edge, must be + * exist in graph, else throw exception. + * @param edgeGraph edge {@link EdgeGraph} of the graph. + * @return added graph edge. + * @throws NotFoundException while creating edge + */ + EdgeGraph create(final EdgeGraph edgeGraph) throws NotFoundException; + + /** + * Batch creation edges of the graph. Nodes that linked by the edge, must be + * exist in graph, else throw exception. + * @param edgeGraphs set of graph edges {@link EdgeGraph} + * @return added graph edges. + * @throws NotFoundException while creating edges + */ + Set create(final Set edgeGraphs) throws NotFoundException; + + /** + * Search edge of the graph by id and delete from graph. + * @param id of edge of the graph. + * @throws NotFoundException if edge is not found in the graph. + */ + void delete(final Integer id) throws NotFoundException; + + /** + * Search edges by unique name of the graph node and delete them from graph. + * Since the edge describes the connection of two nodes, the search occurs + * on the node one or node two. + * @param nodeName unique name of the graph node. + * @throws NotFoundException if edge is not found in the graph. + */ + void delete(final String nodeName) throws NotFoundException; + + /** + * Search edge of the graph by node one and node two and if edge contain + * both these nodes then delete edge from graph. + * @param nodeNameOne unique name of the first graph node. + * @param nodeNameTwo unique name of the second graph node. + * @throws NotFoundException if edge is not found in the graph. + */ + void delete(final String nodeNameOne, final String nodeNameTwo) throws NotFoundException; + + void delete(final Set edges) throws NotFoundException; + + /** + * Removing all edges from the graph. + */ + void deleteAll(); + + /** + * Search edge of the graph by node one and node two and if edge contain + * both these nodes then return edge else throw exception. + * @param nodeNameOne unique name of the first graph node. + * @param nodeNameTwo unique name of the second graph node. + * @return graph edge {@link EdgeGraph}. + * @throws NotFoundException if edge is not found in the graph. + */ + EdgeGraph get(final String nodeNameOne, final String nodeNameTwo) throws NotFoundException; + + /** + * Search edges by unique name of the graph node and return it. + * Since the edge describes the connection of two nodes, the search occurs + * on the node one or node two. + * @param nodeName unique name of the graph node. + * @return set of edges of the graph + */ + Set get(final String nodeName); + + /** + * Get edge of the graph by id. + * @param id of edge ot the graph. + * @return edge {@link EdgeGraph} of the graph. + * @throws NotFoundException if edge is not found in the graph. + */ + EdgeGraph getById(final Integer id) throws NotFoundException; + + /** + * Get all edges from graph. + * @return set of edges ot the graph. + */ + Set getAll(); +} + diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeServiceImpl.java b/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeServiceImpl.java new file mode 100644 index 0000000..cc57276 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphEdgeServiceImpl.java @@ -0,0 +1,232 @@ +package ru.resprojects.linkchecker.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; +import ru.resprojects.linkchecker.repositories.EdgeRepository; +import ru.resprojects.linkchecker.repositories.NodeRepository; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; +import static ru.resprojects.linkchecker.util.ValidationUtil.checkNotFound; + +@Service +public class GraphEdgeServiceImpl implements GraphEdgeService { + + private final EdgeRepository edgeRepository; + private final NodeRepository nodeRepository; + private final AppProperties properties; + + @Autowired + public GraphEdgeServiceImpl(final EdgeRepository edgeRepository, + final NodeRepository nodeRepository, final AppProperties properties) { + this.edgeRepository = edgeRepository; + this.nodeRepository = nodeRepository; + this.properties = properties; + } + + private boolean isPresent(final Node nodeOne, final Node nodeTwo) { + try { + get(nodeOne.getName(), nodeTwo.getName()); + return true; + } catch (NotFoundException e) { + return false; + } + } + + @Override + public EdgeGraph create(final EdgeGraph edgeGraph) throws NotFoundException { + if (Objects.isNull(edgeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + Node nodeOne = checkNotFound( + nodeRepository.getByName(edgeGraph.getNodeOne()), + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), edgeGraph.getNodeOne()), + ErrorPlaceType.EDGE + ); + Node nodeTwo = checkNotFound( + nodeRepository.getByName(edgeGraph.getNodeTwo()), + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), edgeGraph.getNodeTwo()), + ErrorPlaceType.EDGE + ); + if (isPresent(nodeOne, nodeTwo)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), + nodeOne.getName(), + nodeTwo.getName(), + nodeTwo.getName(), + nodeOne.getName() + ) + ); + } + Edge edge = new Edge(nodeOne, nodeTwo); + return GraphUtil.edgeToEdgeGraph(edgeRepository.save(edge)); + } + + @Override + public Set create(final Set edgeGraphs) throws NotFoundException { + if (Objects.isNull(edgeGraphs)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + if (edgeGraphs.isEmpty()) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_EMPTY") + ); + } + Map> nodes = new HashMap<>(); + for (EdgeGraph edgeGraph : edgeGraphs) { + if (Objects.isNull(edgeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL") + ); + } + Node nodeOne = checkNotFound( + nodeRepository.getByName(edgeGraph.getNodeOne()), + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), edgeGraph.getNodeOne()), + ErrorPlaceType.EDGE + ); + Node nodeTwo = checkNotFound( + nodeRepository.getByName(edgeGraph.getNodeTwo()), + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), edgeGraph.getNodeTwo()), + ErrorPlaceType.EDGE + ); + if (isPresent(nodeOne, nodeTwo)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.EDGE, + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), + nodeOne.getName(), + nodeTwo.getName(), + nodeTwo.getName(), + nodeOne.getName() + ) + ); + } + Map nodeMap = new HashMap<>(); + nodeMap.put(edgeGraph.getNodeOne(), nodeOne); + nodeMap.put(edgeGraph.getNodeTwo(), nodeTwo); + nodes.put(edgeGraph, nodeMap); + } + List edges = edgeGraphs.stream() + .map(eg -> new Edge( + nodes.get(eg).get(eg.getNodeOne()), + nodes.get(eg).get(eg.getNodeTwo())) + ).collect(Collectors.toList()); + return GraphUtil.edgesToEdgeGraphs(edgeRepository.saveAll(edges)); + } + + @Override + public void delete(final Integer id) throws NotFoundException { + if (edgeRepository.existsById(id)) { + edgeRepository.deleteById(id); + } else { + throw new NotFoundException(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), ErrorPlaceType.EDGE, id), ErrorPlaceType.EDGE); + } + } + + @Override + public void delete(final String nodeName) throws NotFoundException { + List edges = getEdges(nodeName); + if (edges.isEmpty()) { + throw new NotFoundException(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), nodeName), ErrorPlaceType.EDGE); + } + edgeRepository.deleteInBatch(edges); + } + + @Override + public void delete(final String nodeNameOne, final String nodeNameTwo) throws NotFoundException { + Edge edge = checkNotFound(getEdge(nodeNameOne, nodeNameTwo), + String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), nodeNameOne, nodeNameTwo), + ErrorPlaceType.EDGE); + edgeRepository.delete(edge); + } + + @Override + public void delete(Set edges) throws NotFoundException { + edgeRepository.deleteInBatch(edges); + } + + @Override + public void deleteAll() { + edgeRepository.deleteAllInBatch(); + } + + @Override + public EdgeGraph get(final String nodeNameOne, final String nodeNameTwo) throws NotFoundException { + EdgeGraph edgeGraph = GraphUtil.edgeToEdgeGraph(getEdge(nodeNameOne, nodeNameTwo)); + return checkNotFound(edgeGraph, + String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), nodeNameOne, nodeNameTwo), + ErrorPlaceType.EDGE); + } + + private Edge getEdge(final String nodeNameOne, final String nodeNameTwo) { + Node nodeOne = nodeRepository.getByName(nodeNameOne); + Node nodeTwo = nodeRepository.getByName(nodeNameTwo); + return edgeRepository.findEdgeByNodeOneAndNodeTwo(nodeOne, nodeTwo) + .orElse(edgeRepository.findEdgeByNodeOneAndNodeTwo(nodeTwo, nodeOne).orElse(null)); + } + + @Override + public Set get(final String nodeName) { + List result = getEdges(nodeName); + if (result.isEmpty()) { + throw new NotFoundException(String.format(properties.getEdgeMsg() + .get("EDGE_MSG_GET_BY_NAME_ERROR"), nodeName), ErrorPlaceType.EDGE); + } + return GraphUtil.edgesToEdgeGraphs(result); + } + + private List getEdges(final String nodeName) { + Node node = nodeRepository.getByName(nodeName); + return edgeRepository.findEdgesByNodeOneOrNodeTwo(node, node); + } + + @Override + public EdgeGraph getById(final Integer id) throws NotFoundException { + EdgeGraph edgeGraph = GraphUtil.edgeToEdgeGraph(edgeRepository.findById(id) + .orElse(null)); + return checkNotFound(edgeGraph, String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), + ErrorPlaceType.EDGE, id), ErrorPlaceType.EDGE); + } + + @Override + public Set getAll() { + return GraphUtil.edgesToEdgeGraphs(edgeRepository.findAll()); + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphNodeService.java b/src/main/java/ru/resprojects/linkchecker/services/GraphNodeService.java new file mode 100644 index 0000000..6de53da --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphNodeService.java @@ -0,0 +1,85 @@ +package ru.resprojects.linkchecker.services; + +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.Set; + +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; + +/** + * GraphNodeService - the interface for work with graph nodes. + */ +public interface GraphNodeService { + + /** + * Create node of the graph. + * @param nodeGraph graph node {@link NodeGraph} + * @return added graph node. + */ + NodeGraph create(final NodeGraph nodeGraph); + + /** + * Batch creation nodes of the graph + * @param nodeGraphs set of graph nodes {@link NodeGraph} + * @return added graph nodes. + */ + Set create(final Set nodeGraphs); + + /** + * Update node of the graph. + * @param nodeGraph graph node {@link NodeGraph} + * @throws NotFoundException if node not updated + */ + void update(final NodeGraph nodeGraph) throws NotFoundException; + + /** + * Search graph node by id and delete from graph. + * @param id of node of the graph. + * @throws NotFoundException if node not found in the graph. + */ + void delete(final Integer id) throws NotFoundException; + + /** + * Search graph node by unique name and delete from graph. + * @param name unique name of graph node. + * @throws NotFoundException if node not found in the graph. + */ + void delete(final String name) throws NotFoundException; + + /** + * Search graph node by object {@link NodeGraph} and delete from graph. + * @param nodeGraph object {@link NodeGraph}. + * @throws NotFoundException if node not found in the graph. + */ + void delete(final NodeGraph nodeGraph) throws NotFoundException; + + /** + * Will be deleted all nodes and edges associated with + * these nodes in the graph. + */ + void deleteAll(); + + /** + * Search and return graph node by unique name. + * @param name unique name of graph node. + * @return node of the graph. + * @throws NotFoundException if node not found in the graph. + */ + NodeGraph get(final String name) throws NotFoundException; + + /** + * Search and return graph node by id. + * @param id of node of the graph. + * @return node of the graph. + * @throws NotFoundException if node not found in the graph. + */ + NodeGraph getById(final Integer id) throws NotFoundException; + + /** + * Get all graph nodes. + * @return set of graph nodes. + */ + Set getAll(); + +} + diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphNodeServiceImpl.java b/src/main/java/ru/resprojects/linkchecker/services/GraphNodeServiceImpl.java new file mode 100644 index 0000000..0d83baf --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphNodeServiceImpl.java @@ -0,0 +1,207 @@ +package ru.resprojects.linkchecker.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.repositories.NodeRepository; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.Objects; +import java.util.Set; + +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.util.ValidationUtil.checkNotFound; + +@Service +public class GraphNodeServiceImpl implements GraphNodeService { + + private final NodeRepository nodeRepository; + private final AppProperties properties; + + @Autowired + public GraphNodeServiceImpl(final NodeRepository nodeRepository, final AppProperties properties) { + this.nodeRepository = nodeRepository; + this.properties = properties; + } + + private boolean isPresent(final NodeGraph nodeGraph) { + try { + get(nodeGraph.getName()); + return true; + } catch (NotFoundException e) { + return false; + } + } + + @Override + public NodeGraph create(final NodeGraph nodeGraph) { + if (Objects.isNull(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + if (isPresent(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"), + nodeGraph.getName() + ) + ); + } + return GraphUtil.nodeToNodeGraph(nodeRepository.save( + GraphUtil.nodeGraphToNode(nodeGraph))); + } + + @Override + public Set create(Set nodeGraphs) { + if (Objects.isNull(nodeGraphs)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + if (nodeGraphs.isEmpty()) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_EMPTY") + ); + } + nodeGraphs.forEach(nodeGraph -> { + if (Objects.isNull(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL") + ); + } + if (isPresent(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"), + nodeGraph.getName() + ) + ); + } + }); + return GraphUtil.nodesToNodeGraphs(nodeRepository.saveAll( + GraphUtil.nodeGraphsToNodes(nodeGraphs))); + } + + @Override + public void update(final NodeGraph nodeGraph) throws NotFoundException { + if (Objects.isNull(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + checkNotFound(nodeRepository.save( + GraphUtil.nodeGraphToNode(nodeGraph)), + properties.getNodeMsg().get("NODE_MSG_UPDATE_ERROR") + nodeGraph.getId(), + ErrorPlaceType.NODE + ); + } + + @Override + public void delete(final Integer id) throws NotFoundException { + if (nodeRepository.existsById(id)) { + nodeRepository.deleteById(id); + } else { + throw new NotFoundException(String.format( + properties.getAppMsg().get("MSG_BY_ID_ERROR"), + ErrorPlaceType.NODE, id + ), ErrorPlaceType.NODE); + } + } + + @Override + public void delete(final String name) throws NotFoundException { + if (nodeRepository.existsByName(name)) { + nodeRepository.deleteByName(name); + } else { + throw new NotFoundException(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), + name + ), ErrorPlaceType.NODE); + } + } + + @Override + public void delete(final NodeGraph nodeGraph) throws NotFoundException { + if (Objects.isNull(nodeGraph)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.NODE, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + try { + NodeGraph nodeFromRepo = GraphUtil.nodeToNodeGraph(nodeRepository + .findById(nodeGraph.getId()).orElse(null)); + if (nodeGraph.equals(nodeFromRepo)) { + nodeRepository.deleteById(nodeGraph.getId()); + } else { + throw new Exception(); + } + } catch (Exception e) { + throw new NotFoundException(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), + nodeGraph.toString() + ), ErrorPlaceType.NODE); + } + } + + @Override + public void deleteAll() { + nodeRepository.deleteAllInBatch(); + } + + @Override + public Set getAll() { + return GraphUtil.nodesToNodeGraphs(nodeRepository.findAll()); + } + + @Override + public NodeGraph get(final String name) throws NotFoundException { + NodeGraph nodeGraph = GraphUtil.nodeToNodeGraph(nodeRepository.getByName(name)); + return checkNotFound(nodeGraph, + String.format( + properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), + name + ), ErrorPlaceType.NODE); + } + + @Override + public NodeGraph getById(final Integer id) throws NotFoundException { + NodeGraph nodeGraph = GraphUtil.nodeToNodeGraph(nodeRepository + .findById(id).orElse(null)); + return checkNotFound(nodeGraph, + String.format( + properties.getAppMsg().get("MSG_BY_ID_ERROR"), + ErrorPlaceType.NODE, id + ), ErrorPlaceType.NODE); + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphService.java b/src/main/java/ru/resprojects/linkchecker/services/GraphService.java new file mode 100644 index 0000000..c68212e --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphService.java @@ -0,0 +1,60 @@ +package ru.resprojects.linkchecker.services; + +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.Set; + +/** + * GraphService - the interface for work with undirected graph. + */ +public interface GraphService { + + /** + * Checking input graph data, removing cycles from input graph and saving it + * to the DB. + * @param graphTo graph {@link GraphDto} + * @return graph. + * @throws ApplicationException if found errors. + */ + GraphDto create(final GraphDto graphTo) throws ApplicationException; + + /** + * Get graph. + * @return graph {@link GraphDto} + */ + GraphDto get(); + + /** + * Remove graph. + */ + void clear(); + + /** + * Checking route between nodes. + * @param nodeNameSet collection of the unique nodes name. + * @return check result in JSON format. + * @throws NotFoundException if route is not found + */ + String checkRoute(final Set nodeNameSet) throws NotFoundException; + + /** + * Exporting graph to graphviz format. + * @return string data in graphviz format. + */ + String exportToGraphViz(); + + /** + * Get access to nodes of the graph. + * @return {@link GraphNodeService} + */ + GraphNodeService getNodes(); + + /** + * Get access to edges of the graph. + * @return {@link GraphEdgeService} + */ + GraphEdgeService getEdges(); +} + diff --git a/src/main/java/ru/resprojects/linkchecker/services/GraphServiceImpl.java b/src/main/java/ru/resprojects/linkchecker/services/GraphServiceImpl.java new file mode 100644 index 0000000..1e91327 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/services/GraphServiceImpl.java @@ -0,0 +1,215 @@ +package ru.resprojects.linkchecker.services; + +import org.jgrapht.Graph; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultEdge; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.model.AbstractNamedEntity; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; +import static ru.resprojects.linkchecker.util.GraphUtil.*; + +@Service +public class GraphServiceImpl implements GraphService { + + private final GraphEdgeService edges; + private final GraphNodeService nodes; + private final AppProperties properties; + + @Autowired + public GraphServiceImpl(final GraphEdgeService edges, final GraphNodeService nodes, final AppProperties properties) { + this.edges = edges; + this.nodes = nodes; + this.properties = properties; + } + + @Override + public GraphDto create(final GraphDto graphTo) throws ApplicationException { + if (Objects.isNull(graphTo)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.GRAPH, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + if (graphTo.getNodes().isEmpty() && !graphTo.getEdges().isEmpty()) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.GRAPH, + HttpStatus.UNPROCESSABLE_ENTITY, + "NODES: " + properties.getAppMsg().get("MSG_COLLECTION_EMPTY") + ); + } + clear(); + GraphDto graph = graphToGraphDto( + removeCyclesFromGraph(graphBuilder(graphTo.getNodes(), graphTo.getEdges()))); + Set nodeGraphSet = nodes.create(graph.getNodes()); + Set edgeGraphSet = edges.create(graph.getEdges()); + return new GraphDto(nodeGraphSet, edgeGraphSet); + } + + @Override + public GraphDto get() { + return removeGraphCycles(new GraphDto(nodes.getAll(), edges.getAll())); + } + + @Override + public String exportToGraphViz() { + return GraphUtil.exportToGraphViz(get()); + } + + @Override + public void clear() { + nodes.deleteAll(); + } + + @Override + public String checkRoute(final Set nodeNameSet) throws NotFoundException { + if (Objects.isNull(nodeNameSet)) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.GRAPH, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_ARGUMENT_NULL") + ); + } + if (nodeNameSet.isEmpty()) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.GRAPH, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_EMPTY") + ); + } + if (nodeNameSet.size() == 1) { + throw new ApplicationException( + ErrorType.DATA_ERROR, + ErrorPlaceType.GRAPH, + HttpStatus.UNPROCESSABLE_ENTITY, + properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_ONE_ELEMENT") + ); + } + GraphDto graphDto = removeGraphCycles(new GraphDto(nodes.getAll(), + edges.getAll())); + Map faultNodes = getRandomNodeFault(graphDto.getNodes()); + List nodeNameList = new ArrayList<>(nodeNameSet); + Graph graph = graphBuilder(graphDto.getNodes(), + graphDto.getEdges()); + DijkstraShortestPath dAlg = new DijkstraShortestPath<>(graph); + Node firstNode = nodeGraphToNode(graphDto.getNodes().stream() + .filter(ng -> ng.getName().equalsIgnoreCase(nodeNameList.get(0))) + .findFirst() + .orElse(null)); + if (Objects.isNull(firstNode)) { + throw new NotFoundException( + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), nodeNameList.get(0)), + ErrorPlaceType.GRAPH + ); + } + ShortestPathAlgorithm.SingleSourcePaths paths = dAlg.getPaths(firstNode); + if (faultNodes.getOrDefault(firstNode.getName(), false)) { + throw new NotFoundException( + String.format(properties.getNodeMsg().get("NODE_MSG_IS_FAULT"), firstNode.getName()), + ErrorPlaceType.GRAPH + ); + } + nodeNameList.stream().skip(1).forEach(name -> { + Node nextNode = nodeGraphToNode(graphDto.getNodes().stream() + .filter(ng -> ng.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null)); + if (Objects.isNull(nextNode)) { + throw new NotFoundException( + String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), name), + ErrorPlaceType.GRAPH + ); + } + if (faultNodes.getOrDefault(nextNode.getName(), false)) { + throw new NotFoundException( + String.format(properties.getNodeMsg().get("NODE_MSG_IS_FAULT"), name), + ErrorPlaceType.GRAPH + ); + } + List findNodes = paths.getPath(nextNode).getVertexList(); + List findNodesName = findNodes.stream() + .map(AbstractNamedEntity::getName) + .collect(Collectors.toList()); + if (!nodeNameList.containsAll(findNodesName)) { + throw new NotFoundException( + String.format(properties.getNodeMsg().get("NODE_MSG_NOT_REACHABLE"), nodeNameList.get(0), name), + ErrorPlaceType.GRAPH + ); + } + }); + nodeNameList.forEach(name -> + graphDto.getNodes().stream() + .filter(ng -> ng.getName().equalsIgnoreCase(name)) + .findFirst() + .ifPresent(ng -> { + ng.setCounter(ng.getCounter() + 1); + nodes.update(ng); + })); + return String.format("Route for nodes %s is found", nodeNameList.toString()); + } + + private GraphDto removeGraphCycles(final GraphDto graph) { + if (Objects.isNull(graph) || graph.getEdges().isEmpty()) { + return graph; + } + GraphDto optimizedGraph = graphToGraphDto( + removeCyclesFromGraph(graphBuilder(graph.getNodes(), graph.getEdges()))); + //Checking, was removed edges or not from graph after optimizing + if (graph.getEdges().size() == optimizedGraph.getEdges().size()) { + return graph; + } + //Because ID's lost's in optimized graph, we need recover them. + Set optimizedEdges = graph.getEdges().stream() + .filter(e -> optimizedGraph.getEdges().stream() + .anyMatch(eg -> eg.getNodeOne().equalsIgnoreCase(e.getNodeOne()) + && eg.getNodeTwo().equalsIgnoreCase(e.getNodeTwo()))) + .collect(Collectors.toSet()); + //Rewrite edge collection in optimized graph. + optimizedGraph.setEdges(optimizedEdges); + //Search all edges that was removed from graph and remove them from DB. + Set removedEdgesGraph = graph.getEdges().stream() + .filter(eg -> !optimizedGraph.getEdges().contains(eg)) + .collect(Collectors.toSet()); + Set removedEdges = getEdgesFromGraphDto(new GraphDto( + optimizedGraph.getNodes(), + removedEdgesGraph)); + if (!removedEdges.isEmpty()) { + edges.delete(removedEdges); + return optimizedGraph; + } + return graph; + } + + public GraphEdgeService getEdges() { + return edges; + } + + public GraphNodeService getNodes() { + return nodes; + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/GraphUtil.java b/src/main/java/ru/resprojects/linkchecker/util/GraphUtil.java new file mode 100644 index 0000000..d808015 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/GraphUtil.java @@ -0,0 +1,348 @@ +package ru.resprojects.linkchecker.util; + +import org.jgrapht.Graph; +import org.jgrapht.GraphPath; +import org.jgrapht.Graphs; +import org.jgrapht.alg.cycle.PatonCycleBase; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.SimpleGraph; +import org.jgrapht.io.ComponentNameProvider; +import org.jgrapht.io.DOTExporter; +import org.jgrapht.io.GraphExporter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; + +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + + +/** + * Helper class for work with graph. + */ +public class GraphUtil { + + private static final Logger LOG = LoggerFactory.getLogger(GraphUtil.class); + + /** + * Ctor. + */ + private GraphUtil() { + } + + /** + * Building graph in JGraphT format + * from collections of the nodes and edges. + * @param nodesGraph collection of nodes {@link NodeGraph}. + * @param edgesGraph collection of edges {@link EdgeGraph}. + * @return graph in JGraphT format. + */ + public static Graph graphBuilder(final Collection nodesGraph, + final Collection edgesGraph) { + Graph graph = new SimpleGraph<>(DefaultEdge.class); + if (Objects.isNull(nodesGraph) || Objects.isNull(edgesGraph)) { + LOG.debug("graphBuilder: Return empty graph because one of the input collection is null"); + return graph; + } + Set nodes = nodeGraphsToNodes(nodesGraph); + Set edges = edgesGraph.stream() + .filter(Objects::nonNull) + .map(eg -> { + Node nodeOne = nodes.stream() + .filter(n -> n.getName().equalsIgnoreCase(eg.getNodeOne())) + .findFirst() + .orElse(null); + Node nodeTwo = nodes.stream() + .filter(n -> n.getName().equalsIgnoreCase(eg.getNodeTwo())) + .findFirst() + .orElse(null); + if (Objects.nonNull(nodeOne) && Objects.nonNull(nodeTwo)) { + return new Edge(eg.getId(), nodeOne, nodeTwo); + } else { + return null; + } }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + nodes.forEach(graph::addVertex); + edges.forEach(edge -> graph.addEdge( + edge.getNodeOne(), edge.getNodeTwo() + )); + return graph; + } + + /** + * Convert graph from {@see JGraphT} format + * to graph DTO {@link GraphDto} format. + * ATTENTION: GraphDto will return edges without IDs, + * because graph in JGraphT format does not store IDs for edges! + * @param graph graph in JGraphT format. + * @return {@link GraphDto}. + */ + public static GraphDto graphToGraphDto(final Graph graph) { + if (Objects.isNull(graph)) { + LOG.debug("graphToGraphDto: returned empty DTO graph because input graph is null."); + return new GraphDto(); + } + return new GraphDto(getNodesDtoFromGraph(graph), getEdgesDtoFromGraph(graph)); + } + + /** + * Converting JGraphT nodes to the GraphDto nodes {@link NodeGraph} + * @param graph graph in the JGraphT format. + * @return collection of GraphDto nodes. + */ + private static Set getNodesDtoFromGraph(final Graph graph) { + if (Objects.isNull(graph)) { + LOG.debug("getNodesDtoFromGraph: returned empty collection, because input graph is null"); + return new HashSet<>(); + } + return graph.vertexSet().stream() + .map(n -> new NodeGraph(n.getId(), n.getName(), n.getCounter())) + .collect(Collectors.toSet()); + } + + /** + * Converting JGraphT edges to the GraphDto edges {@link NodeGraph} + * @param graph graph in the JGraphT format. + * @return collection of GraphDto edges without IDs. + */ + private static Set getEdgesDtoFromGraph(final Graph graph) { + if (Objects.isNull(graph)) { + LOG.debug("getEdgesDtoFromGraph: returned empty collection, because input graph is null"); + return new HashSet<>(); + } + return graph.edgeSet().stream() + .map(e -> new EdgeGraph(graph.getEdgeSource(e).getName(), graph.getEdgeTarget(e).getName())) + .collect(Collectors.toSet()); + } + + /** + * Find a cycle basis of an undirected graph using a variant of Paton's + * algorithm from {@see JGraphT} library. + * NOTE: while removing cycles algorithm each time returns a random set of + * graph edges for the same graph. + * @param graph graph in JGraphT format. + * @return graph in JGraphT format without cycles. + */ + public static Graph removeCyclesFromGraph( + final Graph graph) { + LOG.debug("removeCyclesFromGraph: Detect cycles in graph by Paton algorithm"); + if (!isGraphContainCycles(graph)) { + LOG.debug("removeCyclesFromGraph: Cycles not found!"); + return graph; + } + SimpleGraph result = new SimpleGraph<>(DefaultEdge.class); + Graphs.addGraph(result, graph); + while (true) { + LOG.debug("removeCyclesFromGraph: Try detect cycles"); + PatonCycleBase patonCycleBase = new PatonCycleBase<>(result); + Set> paths = patonCycleBase.getCycleBasis().getCyclesAsGraphPaths(); + Set edgeSet = new HashSet<>(); + if (paths.size() != 0) { + LOG.debug("removeCyclesFromGraph: Cycles found! Count of cycles in present graph = {}", paths.size()); + for (GraphPath graphPath : paths) { + edgeSet.addAll(graphPath.getEdgeList()); + } + LOG.debug("removeCyclesFromGraph: Remove edge from cycle"); + if (edgeSet.iterator().hasNext()) { + DefaultEdge edge = edgeSet.iterator().next(); + result.removeEdge(edge); + } + } else { + LOG.debug("removeCyclesFromGraph: Cycles not found!"); + break; + } + } + return result; + } + + private static boolean isGraphContainCycles(final Graph graph) { + SimpleGraph result = new SimpleGraph<>(DefaultEdge.class); + Graphs.addGraph(result, graph); + PatonCycleBase patonCycleBase = new PatonCycleBase<>(result); + Set> paths = patonCycleBase.getCycleBasis() + .getCyclesAsGraphPaths(); + return !paths.isEmpty(); + } + + /** + * Export graph to {@see GraphViz.dot} + * format. + * @param graphTo graph in DTO format, see {@link GraphDto}. + * @return graph in GraphViz.dot format. + */ + public static String exportToGraphViz(final GraphDto graphTo) { + if (Objects.isNull(graphTo)) return ""; + Graph graph = graphBuilder(graphTo.getNodes(), graphTo.getEdges()); + ComponentNameProvider vertexIdProvider = component -> + component.getName() + "_" + component.getId(); + ComponentNameProvider vertexNameProvider = Node::getName; + GraphExporter exporter = new DOTExporter<>( + vertexIdProvider, vertexNameProvider, null + ); + try (Writer writer = new StringWriter()) { + exporter.exportGraph(graph, writer); + LOG.debug(writer.toString()); + return writer.toString(); + } catch (Exception e) { + LOG.warn("Fail export graph to GraphViz format.", e); + } + return ""; + } + + /** + * Converting collection of {@link Node} into set of {@link NodeGraph} + * @param nodes node model objects collection. + * @return collection of graph node DTO or empty collection if nodes is null. + */ + public static Set nodesToNodeGraphs(final Collection nodes) { + if (Objects.isNull(nodes)) { + LOG.debug("nodesToNodeGraphs: return empty collection because input collection of nodes is null"); + return new HashSet<>(); + } + return nodes.stream() + .filter(Objects::nonNull) + .map(GraphUtil::nodeToNodeGraph) + .collect(Collectors.toSet()); + } + + /** + * Converting collection of {@link NodeGraph} to collection of {@link Node} + * @param nodeGraphs collection of graph node DTO + * @return collection of node model objects or empty collection if + * nodeGraphs is null. + */ + public static Set nodeGraphsToNodes(final Collection nodeGraphs) { + if (Objects.isNull(nodeGraphs)) { + LOG.debug("nodeGraphsToNodes: return empty collection because input collection of DTO nodes is null"); + return new HashSet<>(); + } + return nodeGraphs.stream() + .filter(Objects::nonNull) + .map(GraphUtil::nodeGraphToNode) + .collect(Collectors.toSet()); + } + + /** + * Converting {@link Node} to {@link NodeGraph} + * @param node model object. + * @return graph node DTO or null + */ + public static NodeGraph nodeToNodeGraph(final Node node) { + if (node != null) { + return new NodeGraph( + node.getId(), + node.getName(), + node.getCounter() + ); + } else { + return null; + } + } + + /** + * Converting {@link NodeGraph} to {@link Node} + * @param nodeGraph graph node DTO. + * @return model object or null + */ + public static Node nodeGraphToNode(final NodeGraph nodeGraph) { + if (nodeGraph != null) { + return new Node( + nodeGraph.getId(), + nodeGraph.getName(), + nodeGraph.getCounter() + ); + } else { + return null; + } + } + + /** + * Converting collection of {@link Edge} to collection of {@link EdgeGraph} + * @param edges model objects collection. + * @return collection of graph edge DTO or empty collection if param is null. + */ + public static Set edgesToEdgeGraphs(final Collection edges) { + if (Objects.isNull(edges)) { + LOG.debug("edgesToEdgeGraphs: return empty collection because input collection of edges is null"); + return new HashSet<>(); + } + return edges.stream() + .filter(Objects::nonNull) + .map(GraphUtil::edgeToEdgeGraph) + .collect(Collectors.toSet()); + } + + /** + * Converting {@link Edge} to {@link EdgeGraph} + * @param edge model object. + * @return graph edge DTO or null + */ + public static EdgeGraph edgeToEdgeGraph(final Edge edge) { + if (edge != null) { + return new EdgeGraph(edge.getId(), edge.getNodeOne().getName(), + edge.getNodeTwo().getName()); + } else { + return null; + } + } + + /** + * Getting graph edges {@link Edge} from graph data transfer object {@link GraphDto} + * @param graph graph data transfer object + * @return collection of graph edges. + */ + public static Set getEdgesFromGraphDto(final GraphDto graph) { + return graph.getEdges().stream() + .map(eg -> { + Node nodeOne = nodeGraphToNode(graph.getNodes().stream() + .filter(ng -> ng.getName().equalsIgnoreCase(eg.getNodeOne())) + .findFirst() + .orElse(null)); + Node nodeTwo = nodeGraphToNode(graph.getNodes().stream() + .filter(ng -> ng.getName().equalsIgnoreCase(eg.getNodeTwo())) + .findFirst() + .orElse(null)); + if (Objects.nonNull(nodeOne) && Objects.nonNull(nodeTwo)) { + return new Edge(eg.getId(), nodeOne, nodeTwo); + } else { + return null; + } }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + /** + * Node random failure generator. + * @param nodes node graph collection + * @return map with key as node graph name and value as fault of node graph + * state or null if node graph collection is empty or null. + */ + public static Map getRandomNodeFault(Collection nodes) { + LOG.debug("getRandomNodeFault: Generating random node fault"); + Map result = new HashMap<>(); + if (Objects.isNull(nodes) || nodes.isEmpty()) { + return result; + } + nodes.forEach(nodeGraph -> { + boolean isFault = ThreadLocalRandom.current().nextInt(100) >= 90; + result.put(nodeGraph.getName(), isFault); + }); + LOG.debug("getRandomNodeFault: result = " + result.toString()); + return result; + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/ValidationUtil.java b/src/main/java/ru/resprojects/linkchecker/util/ValidationUtil.java new file mode 100644 index 0000000..19f4a6e --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/ValidationUtil.java @@ -0,0 +1,63 @@ +package ru.resprojects.linkchecker.util; + +import org.slf4j.Logger; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import javax.servlet.http.HttpServletRequest; + +/** + * Helper class for work with validation. + */ +public final class ValidationUtil { + + public static final int NODE_COUNTER_DEFAULT = 0; + public static final int MIN_NAME_SIZE = 1; + public static final int MAX_NAME_SIZE = 20; + public static final String VALIDATOR_NOT_NULL_MESSAGE = "Object is not be null"; + public static final String VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE = "Node name is not be empty"; + public static final String VALIDATOR_NODE_NAME_RANGE_MESSAGE = "Node name must be at range from " + + MIN_NAME_SIZE + " to " + MAX_NAME_SIZE; + + private ValidationUtil() { + } + + public static T checkNotFound(T object, String msg, ErrorPlaceType place) { + checkNotFound(object != null, msg, place); + return object; + } + + public static void checkNotFound(boolean found, String arg, ErrorPlaceType place) { + if (!found) { + throw new NotFoundException(arg, place); + } + } + + // http://stackoverflow.com/a/28565320/548473 + private static Throwable getRootCause(Throwable t) { + Throwable result = t; + Throwable cause; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } + + public static String getMessage(Throwable e) { + return e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName(); + } + + public static Throwable logAndGetRootCause(Logger log, HttpServletRequest req, + Exception e, boolean logException, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return rootCause; + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/exeptions/ApplicationException.java b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ApplicationException.java new file mode 100644 index 0000000..db6b8b4 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ApplicationException.java @@ -0,0 +1,45 @@ +package ru.resprojects.linkchecker.util.exeptions; + +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +public class ApplicationException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -6458332116628903136L; + private final ErrorType type; + private final ErrorPlaceType place; + private final HttpStatus httpStatus; + private final String[] messages; + + public ApplicationException(String messages, HttpStatus httpStatus) { + this(ErrorType.APP_ERROR, ErrorPlaceType.GRAPH, httpStatus, messages); + } + + public ApplicationException(ErrorType type, ErrorPlaceType place, HttpStatus httpStatus, String... messages) { + super(String.format("type=%s, place=%s, msg=%s", type, place, Arrays.toString(messages))); + this.type = type; + this.place = place; + this.messages = messages; + this.httpStatus = httpStatus; + } + + public ErrorType getType() { + return type; + } + + public ErrorPlaceType getPlace() { + return place; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String[] getMessages() { + return messages; + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorInfo.java b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorInfo.java new file mode 100644 index 0000000..81e8bea --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorInfo.java @@ -0,0 +1,74 @@ +package ru.resprojects.linkchecker.util.exeptions; + +import java.util.Arrays; +import java.util.StringJoiner; + +/** + * Data class for exception handler. + */ +public class ErrorInfo { + + private String url; + private ErrorType type; + private ErrorPlaceType place; + private String[] messages; + + public ErrorInfo() { + } + + /** + * Ctor. + * @param url REST request where an error has occurred + * @param type of error + * @param place where an error has occurred + * @param messages program error messages + */ + public ErrorInfo(String url, ErrorType type, ErrorPlaceType place, String... messages) { + this.url = url; + this.type = type; + this.place = place; + this.messages = messages; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ErrorType getType() { + return type; + } + + public void setType(ErrorType type) { + this.type = type; + } + + public ErrorPlaceType getPlace() { + return place; + } + + public void setPlace(ErrorPlaceType place) { + this.place = place; + } + + public String[] getMessages() { + return messages; + } + + public void setMessages(String[] messages) { + this.messages = messages; + } + + @Override + public String toString() { + return new StringJoiner(", ", ErrorInfo.class.getSimpleName() + "[", "]") + .add("url='" + url + "'") + .add("type=" + type) + .add("place=" + place) + .add("messages=" + Arrays.toString(messages)) + .toString(); + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorPlaceType.java b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorPlaceType.java new file mode 100644 index 0000000..6698db1 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorPlaceType.java @@ -0,0 +1,12 @@ +package ru.resprojects.linkchecker.util.exeptions; + +/** + * Places where errors are an occurred + * EDGE - errors in edges of the graph. + * NODE - error in nodes of the graph. + * GRAPH - error in graph. + * APP - error in application. + */ +public enum ErrorPlaceType { + EDGE, NODE, GRAPH, APP +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorType.java b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorType.java new file mode 100644 index 0000000..ab4df7e --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/exeptions/ErrorType.java @@ -0,0 +1,12 @@ +package ru.resprojects.linkchecker.util.exeptions; + +/** + * Types of errors what my be occurred in program + */ +public enum ErrorType { + APP_ERROR, //Types of errors that are associated with errors in the program as a whole + DATA_NOT_FOUND, //Types of errors that are associated with search and extract data + DATA_ERROR, //Types of errors that are associated with data handling + VALIDATION_ERROR, //Types of errors are associated with validating data in REST-controllers + WRONG_REQUEST //Types of errors that are associated with http requests +} diff --git a/src/main/java/ru/resprojects/linkchecker/util/exeptions/NotFoundException.java b/src/main/java/ru/resprojects/linkchecker/util/exeptions/NotFoundException.java new file mode 100644 index 0000000..0c46efe --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/util/exeptions/NotFoundException.java @@ -0,0 +1,16 @@ +package ru.resprojects.linkchecker.util.exeptions; + +import org.springframework.http.HttpStatus; + +public class NotFoundException extends ApplicationException { + + /** + * + */ + private static final long serialVersionUID = 7379365858788450248L; + + public NotFoundException(final String message, ErrorPlaceType place) { + super(ErrorType.DATA_NOT_FOUND, place, HttpStatus.UNPROCESSABLE_ENTITY, message); + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/CustomErrorController.java b/src/main/java/ru/resprojects/linkchecker/web/CustomErrorController.java new file mode 100644 index 0000000..ab66933 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/CustomErrorController.java @@ -0,0 +1,61 @@ +package ru.resprojects.linkchecker.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import ru.resprojects.linkchecker.util.exeptions.ErrorInfo; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Map; + +@Controller +public class CustomErrorController extends AbstractErrorController { + + private static final Logger LOG = LoggerFactory.getLogger(CustomErrorController.class); + + public CustomErrorController(ErrorAttributes errorAttributes) { + super(errorAttributes); + } + + @RequestMapping(value = "/error", produces = MediaType.TEXT_HTML_VALUE) + @ResponseBody + public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + HttpStatus status = getStatus(request); + response.setStatus(status.value()); + Map model = getModel(request); + ModelAndView modelAndView = resolveErrorView(request, response, status, model); + LOG.debug("errorHtml: " + model); + return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); + } + + @RequestMapping(value = "/error", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public ErrorInfo error(HttpServletRequest request) { + String errorRequestUrl = request.getScheme() + "://"+ request.getServerName() + + ":" + request.getServerPort() + getModel(request).get("path").toString(); + ErrorInfo errorInfo = new ErrorInfo(errorRequestUrl, + ErrorType.WRONG_REQUEST, ErrorPlaceType.APP, "Bad request"); + LOG.debug("error: " + errorInfo); + return errorInfo; + } + + private Map getModel(HttpServletRequest request) { + return Collections.unmodifiableMap(getErrorAttributes(request, true)); + } + + @Override + public String getErrorPath() { + return "/error"; + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/EntryPointController.java b/src/main/java/ru/resprojects/linkchecker/web/EntryPointController.java new file mode 100644 index 0000000..ded876f --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/EntryPointController.java @@ -0,0 +1,15 @@ +package ru.resprojects.linkchecker.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class EntryPointController { + + @GetMapping("/") + public String index(Model model) { + return "index"; + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/rest/ExceptionInfoHandler.java b/src/main/java/ru/resprojects/linkchecker/web/rest/ExceptionInfoHandler.java new file mode 100644 index 0000000..4fb5a2d --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/rest/ExceptionInfoHandler.java @@ -0,0 +1,89 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.resprojects.linkchecker.util.ValidationUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.ErrorInfo; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; + +/** + * Exception handler for REST-controllers + */ +@RestControllerAdvice +public class ExceptionInfoHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + private final MessageSource messageSource; + + @Autowired + public ExceptionInfoHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + @ExceptionHandler(value = {ApplicationException.class, NotFoundException.class}) + public ResponseEntity appError(HttpServletRequest req, ApplicationException appEx) { + return ResponseEntity.status(appEx.getHttpStatus()) + .body(logAndGetErrorInfo(req,appEx, false, + appEx.getType(), appEx.getPlace(), appEx.getMessages())); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req,e, true, + ErrorType.APP_ERROR, ErrorPlaceType.APP); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class, + ConstraintViolationException.class}) + public ErrorInfo validationsError(HttpServletRequest req, Exception e) { + String[] details; + if (e instanceof ConstraintViolationException) { + details = ((ConstraintViolationException) e).getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .toArray(String[]::new); + } else { + BindingResult result = e instanceof BindException ? + ((BindException) e).getBindingResult() : ((MethodArgumentNotValidException) e).getBindingResult(); + details = result.getFieldErrors().stream() + .map(this::getMessage) + .toArray(String[]::new); + } + return logAndGetErrorInfo(req, e, false, + ErrorType.VALIDATION_ERROR, ErrorPlaceType.APP, details); + } + + private ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, + boolean logException, ErrorType errorType, ErrorPlaceType placeType, String... msg) { + Throwable rootCause = ValidationUtil.logAndGetRootCause(LOG, req, e, + logException, errorType); + return new ErrorInfo(req.getRequestURL().toString(),errorType, placeType, + msg.length != 0 ? msg : new String[]{ValidationUtil.getMessage(rootCause)}); + } + + private String getMessage(MessageSourceResolvable resolvable) { + return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale()); + } + +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestController.java b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestController.java new file mode 100644 index 0000000..f819326 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestController.java @@ -0,0 +1,126 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import ru.resprojects.linkchecker.services.GraphService; + +import javax.validation.Valid; +import java.net.URI; +import java.util.Collection; +import java.util.Set; + +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; +import static ru.resprojects.linkchecker.web.rest.GraphRestController.REST_URL; + +/** + * REST controller for work with edges of the data graph + */ +@Validated +@RestController +@RequestMapping(value = GraphEdgeRestController.EDGE_REST_URL, + produces = MediaType.APPLICATION_JSON_VALUE) +public class GraphEdgeRestController { + + private static final Logger LOG = LoggerFactory.getLogger(GraphEdgeRestController.class); + + public static final String EDGE_REST_URL = REST_URL + "/edges"; + + private GraphService graphService; + + @Autowired + public GraphEdgeRestController(final GraphService graphService) { + this.graphService = graphService; + } + + @PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create(@RequestBody @Valid EdgeGraph edge) { + LOG.info("Creating new graph edge"); + EdgeGraph created = this.graphService.getEdges().create(edge); + URI uri = MvcUriComponentsBuilder.fromController(getClass()) + .path(EDGE_REST_URL) + .buildAndExpand() + .toUri(); + return ResponseEntity.created(uri).body(created); + } + + //How to validate data in collections https://stackoverflow.com/a/54394177 + @PostMapping(value = "create/byBatch", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> createByBatch(@RequestBody @Valid Set edges) { + LOG.info("Creating new graph edges"); + Set created = this.graphService.getEdges().create(edges); + URI uri = MvcUriComponentsBuilder.fromController(getClass()) + .path(EDGE_REST_URL) + .buildAndExpand() + .toUri(); + return ResponseEntity.created(uri).body(created); + } + + @GetMapping + public ResponseEntity> get() { + LOG.info("Getting all graph edges"); + return ResponseEntity.ok(this.graphService.getEdges().getAll()); + } + + @GetMapping(value = "/byId/{id}") + public ResponseEntity getById(@PathVariable Integer id) { + LOG.info("Getting graph edge by id = " + id); + return ResponseEntity.ok(graphService.getEdges().getById(id)); + } + + @GetMapping(value = "/byName/{name}") + public ResponseEntity> getByName(@PathVariable String name) { + LOG.info("Getting graph edge by name = " + name); + return ResponseEntity.ok(graphService.getEdges().get(name)); + } + + @GetMapping(value = "/byName") + @ResponseBody + public ResponseEntity getByNames(@RequestParam("nodeOne") String nameNodeOne, @RequestParam("nodeTwo") String nameNodeTwo) { + LOG.info(String.format("Getting graph edge by node one {%s} and node two {%s} names", nameNodeOne, nameNodeTwo)); + return ResponseEntity.ok(graphService.getEdges().get(nameNodeOne, nameNodeTwo)); + } + + @DeleteMapping + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete() { + LOG.info("Removing all graph edges"); + graphService.getEdges().deleteAll(); + } + + @DeleteMapping(value = "/byId/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteById(@PathVariable Integer id) { + LOG.info("Removing graph edge by id = " + id); + graphService.getEdges().delete(id); + } + + @DeleteMapping(value = "/byName/{name}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteByName(@PathVariable String name) { + LOG.info("Removing graph edge by name = " + name); + graphService.getEdges().delete(name); + } + + @DeleteMapping(value = "/byName") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteByNames(@RequestParam("nodeOne") String nodeOne, @RequestParam("nodeTwo") String nodeTwo) { + LOG.info(String.format("Removing graph edge by node one {%s} and node two {%s} names", nodeOne, nodeTwo)); + graphService.getEdges().delete(nodeOne, nodeTwo); + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestController.java b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestController.java new file mode 100644 index 0000000..b414507 --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestController.java @@ -0,0 +1,117 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import ru.resprojects.linkchecker.services.GraphService; + +import javax.validation.Valid; +import java.net.URI; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.web.rest.GraphRestController.REST_URL; + +/** + * REST controller for work with nodes of the data graph + */ +@Validated +@RestController +@RequestMapping(value = GraphNodeRestController.NODES_REST_URL, + produces = MediaType.APPLICATION_JSON_VALUE) +public class GraphNodeRestController { + + private static final Logger LOG = LoggerFactory.getLogger(GraphNodeRestController.class); + + public static final String NODES_REST_URL = REST_URL + "/nodes"; + + private GraphService graphService; + + @Autowired + public GraphNodeRestController(final GraphService graphService) { + this.graphService = graphService; + } + + @PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create(@Valid @RequestBody NodeGraph node) { + LOG.info("Creating new graph node"); + NodeGraph created = this.graphService.getNodes().create(node); + URI uri = MvcUriComponentsBuilder.fromController(getClass()) + .path(NODES_REST_URL) + .buildAndExpand() + .toUri(); + return ResponseEntity.created(uri).body(created); + } + + @PostMapping(value = "/create/byBatch", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> createByBatch(@RequestBody @Valid Set nodes) { + LOG.info("Creating new graph nodes"); + Set created = this.graphService.getNodes().create(nodes); + URI uri = MvcUriComponentsBuilder.fromController(getClass()) + .path(NODES_REST_URL) + .buildAndExpand() + .toUri(); + return ResponseEntity.created(uri).body(created); + } + + @GetMapping + public ResponseEntity> get() { + LOG.info("Getting all graph nodes"); + return ResponseEntity.ok(this.graphService.getNodes().getAll()); + } + + @GetMapping(value = "/byId/{id}") + public ResponseEntity getById(@PathVariable Integer id) { + LOG.info("Getting graph node by id = " + id); + return ResponseEntity.ok(graphService.getNodes().getById(id)); + } + + @GetMapping(value = "/byName/{name}") + public ResponseEntity getByName(@PathVariable String name) { + LOG.info("Getting graph node by name = " + name); + return ResponseEntity.ok(graphService.getNodes().get(name)); + } + + @DeleteMapping(value = "/byId/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteById(@PathVariable Integer id) { + LOG.info("Removing graph node by id = " + id); + graphService.getNodes().delete(id); + } + + @DeleteMapping(value = "/byName/{name}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteByName(@PathVariable String name) { + LOG.info("Removing graph node by name = " + name); + graphService.getNodes().delete(name); + } + + @DeleteMapping(value = "/byObj") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteByObject(@RequestBody NodeGraph obj) { + LOG.info("Removing graph node by object = " + Optional.ofNullable(obj)); + graphService.getNodes().delete(obj); + } + + @DeleteMapping + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete() { + LOG.info("Removing all graph nodes and graph edges that are linked with these nodes"); + graphService.getNodes().deleteAll(); + } +} diff --git a/src/main/java/ru/resprojects/linkchecker/web/rest/GraphRestController.java b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphRestController.java new file mode 100644 index 0000000..598814c --- /dev/null +++ b/src/main/java/ru/resprojects/linkchecker/web/rest/GraphRestController.java @@ -0,0 +1,90 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.services.GraphService; + +import javax.validation.Valid; +import java.net.URI; +import java.util.Set; + +/** + * REST controller for work with data graph in generally + */ +@Validated +@RestController +@RequestMapping(value = GraphRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class GraphRestController { + + private static final Logger LOG = LoggerFactory.getLogger(GraphRestController.class); + + public static final String REST_URL = "/rest/v1/graph"; + + private GraphService graphService; + + @Autowired + public GraphRestController(final GraphService graphService) { + this.graphService = graphService; + } + + @RequestMapping(method = RequestMethod.OPTIONS) + public ResponseEntity options() { + return ResponseEntity + .ok() + .allow(HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE, HttpMethod.OPTIONS) + .build(); + } + + @GetMapping + public ResponseEntity get() { + LOG.info("Getting graph"); + return ResponseEntity.ok(this.graphService.get()); + } + + @GetMapping(value = "/export", produces = MediaType.TEXT_HTML_VALUE) + public ResponseEntity exportToGraphViz() { + LOG.info("Export graph to GraphViz format"); + return ResponseEntity.ok(this.graphService.exportToGraphViz()); + } + + @PostMapping(value = "/checkroute", produces = MediaType.TEXT_HTML_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity checkRoute(@RequestBody Set nodeNames) { + LOG.info("Checking route for nodes " + nodeNames.toString()); + return ResponseEntity.ok(this.graphService.checkRoute(nodeNames)); + } + + @PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create(@RequestBody @Valid GraphDto graph) { + LOG.info("Creating new graph"); + GraphDto created = graphService.create(graph); + URI uri = MvcUriComponentsBuilder.fromController(getClass()) + .path(REST_URL) + .buildAndExpand() + .toUri(); + return ResponseEntity.created(uri).body(created); + } + + @DeleteMapping + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete() { + LOG.info("Removing graph"); + graphService.clear(); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e55e24d --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,109 @@ +spring: + profiles: + active: demo +logging: + level: + ru.resprojects: info +--- +spring: + profiles: production + jpa: + database: postgresql + generate-ddl: true + properties: + hibernate: + jdbc: + lob: + non_contextual_creation: true + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + open-in-view: false + datasource: + url: ${LINKCHECKER_PGSQL_DB_HOST}:${LINKCHECKER_PGSQL_DB_PORT}/${LINKCHECKER_PGSQL_DB_NAME} + username: ${LINKCHECKER_PGSQL_DB_USER} + password: ${LINKCHECKER_PGSQL_DB_PASSWORD} + platform: postgresql + initialization-mode: never +--- +spring: + profiles: demo, test + jpa: + database: h2 + open-in-view: false + hibernate: + ddl-auto: none + datasource: + initialization-mode: always + platform: h2 +--- +spring: + profiles: test + datasource: + url: jdbc:h2:mem:linkchecker;DB_CLOSE_ON_EXIT=FALSE + thymeleaf: + cache: false +--- +spring: + profiles: demo + datasource: + url: jdbc:h2:mem:linkchecker;DB_CLOSE_ON_EXIT=TRUE +--- +spring: + profiles: debug + jpa: + show-sql: true + properties: + hibernate: + generate_statistics: true +logging: + level: + ru.resprojects: debug +--- +spring: + profiles: moc_test +--- +spring: + profiles: demo, production, test + jpa: + properties: + hibernate: + jdbc: + batch_size: 10 + order_inserts: true + order_updates: true +--- +spring: + http: + converters: + preferred-json-mapper: gson + output: + ansi: + enabled: detect + +logging: + level: + org.springframework: error + pattern: + file: "%d %p %c{1.} [%t] %m%n" + console: "%clr(%d{HH:mm:ss.SSS}){yellow} %clr(%-5p) %clr(---){faint} %clr([%t]){cyan} %clr(%logger{36}){blue} %clr(:){red} %clr(%msg){faint}%n" + file: + name: /log/linkchecker.log + max-size: 5MB + +appmsg: + app-msg: + MSG_ARGUMENT_NULL: "Argument must not be null" + MSG_COLLECTION_EMPTY: "Collection must not be empty" + MSG_COLLECTION_CONTAIN_NULL: "Collection must not contain a null item" + MSG_COLLECTION_CONTAIN_ONE_ELEMENT: "Collection must have more than one element" + MSG_BY_ID_ERROR: "%s with ID = %d is not found" + edge-msg: + EDGE_MSG_GET_ERROR: "Edge for nodes [%s, %s] is not found" + EDGE_MSG_ALREADY_PRESENT_ERROR: "Edge for nodes ([%s, %s], [%s, %s]) already present in the graph" + EDGE_MSG_GET_BY_NAME_ERROR: "Edges for node %s is not found" + node-msg: + NODE_MSG_ALREADY_PRESENT_ERROR: "Node %s already present in the graph" + NODE_MSG_UPDATE_ERROR: "Error while update node with id = " + NODE_MSG_BY_NAME_ERROR: "Node with NAME = %s is not found" + NODE_MSG_BY_OBJECT_ERROR: "Node %s is not found" + NODE_MSG_NOT_REACHABLE: "Nodes %s and %s are not reachable to each other" + NODE_MSG_IS_FAULT: "Node %s is fault" \ No newline at end of file diff --git a/src/main/resources/data-h2.sql b/src/main/resources/data-h2.sql new file mode 100644 index 0000000..0f39323 --- /dev/null +++ b/src/main/resources/data-h2.sql @@ -0,0 +1,17 @@ +DELETE FROM edges; +DELETE FROM nodes; + +ALTER SEQUENCE global_seq RESTART WITH 5000; + +INSERT INTO nodes (name) VALUES +('v1'), +('v2'), +('v3'), +('v4'), +('v5'); + +INSERT INTO edges (nodeOne, nodeTwo) VALUES +(5000, 5001), +(5000, 5002), +(5000, 5004), +(5002, 5003); \ No newline at end of file diff --git a/src/main/resources/data-postgresql.sql b/src/main/resources/data-postgresql.sql new file mode 100644 index 0000000..0f39323 --- /dev/null +++ b/src/main/resources/data-postgresql.sql @@ -0,0 +1,17 @@ +DELETE FROM edges; +DELETE FROM nodes; + +ALTER SEQUENCE global_seq RESTART WITH 5000; + +INSERT INTO nodes (name) VALUES +('v1'), +('v2'), +('v3'), +('v4'), +('v5'); + +INSERT INTO edges (nodeOne, nodeTwo) VALUES +(5000, 5001), +(5000, 5002), +(5000, 5004), +(5002, 5003); \ No newline at end of file diff --git a/src/main/resources/schema-h2.sql b/src/main/resources/schema-h2.sql new file mode 100644 index 0000000..c6004a0 --- /dev/null +++ b/src/main/resources/schema-h2.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS nodes CASCADE; +DROP TABLE IF EXISTS edges; +DROP SEQUENCE IF EXISTS global_seq; + +CREATE SEQUENCE global_seq MINVALUE 5000; + +CREATE TABLE nodes ( + id INT DEFAULT global_seq.nextval PRIMARY KEY, + name VARCHAR NOT NULL, + counter INT DEFAULT 0 NOT NULL +); +CREATE UNIQUE INDEX nodes_unique_name_idx ON nodes(name); + +CREATE TABLE edges ( + id INT DEFAULT global_seq.nextval PRIMARY KEY, + nodeone INT NOT NULL, + nodetwo INT NOT NULL, + FOREIGN KEY (nodeone) REFERENCES nodes(id) ON DELETE CASCADE, + FOREIGN KEY (nodetwo) REFERENCES nodes(id) ON DELETE CASCADE, + CHECK (nodeone <> nodetwo), + CONSTRAINT unique_edge UNIQUE (nodeone, nodetwo) +); \ No newline at end of file diff --git a/src/main/resources/schema-postgresql.sql b/src/main/resources/schema-postgresql.sql new file mode 100644 index 0000000..69422da --- /dev/null +++ b/src/main/resources/schema-postgresql.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS nodes CASCADE; +DROP TABLE IF EXISTS edges; +DROP SEQUENCE IF EXISTS global_seq CASCADE; + +CREATE SEQUENCE global_seq START 5000; + +CREATE TABLE nodes ( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + name VARCHAR NOT NULL, + counter INTEGER DEFAULT 0 NOT NULL +); +CREATE UNIQUE INDEX nodes_unique_name_idx ON nodes(name); + +CREATE TABLE edges ( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + nodeone INT NOT NULL, + nodetwo INT NOT NULL, + FOREIGN KEY (nodeone) REFERENCES nodes(id) ON DELETE CASCADE, + FOREIGN KEY (nodetwo) REFERENCES nodes(id) ON DELETE CASCADE, + CHECK (nodeone <> nodetwo), + CONSTRAINT unique_edge UNIQUE (nodeone, nodetwo) +); \ No newline at end of file diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 0000000..8ac5ab2 --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,16 @@ +.starter-template { + padding: 3rem 1.5rem; + text-align: center; +} + +.p-0 { + padding: 0% +} + +h1 { + color:#0000FF; +} + +h2 { + color:#FF0000; +} \ No newline at end of file diff --git a/src/main/resources/static/pics/404.jpg b/src/main/resources/static/pics/404.jpg new file mode 100644 index 0000000..d585ecc Binary files /dev/null and b/src/main/resources/static/pics/404.jpg differ diff --git a/src/main/resources/static/pics/logo.png b/src/main/resources/static/pics/logo.png new file mode 100644 index 0000000..9bc4ef5 Binary files /dev/null and b/src/main/resources/static/pics/logo.png differ diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..e21f2d8 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,34 @@ + + + + + + + Linkchecker (тестовое задание) + + + + + + + + + + +
+
+
+ +
+ Go Back +
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..2ccd96e --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,82 @@ + + + + + + + Linkchecker (тестовое задание) + + + + + + + + + + +
+
+ +

Linkchecker (тестовое задание)

+ +

Оригинальный текст задания

+ +

Разработать REST-сервис, проверяющий работоспособность любой последовательности узлов. + Каждый узел имеет уникальное имя, вероятность, с которой откажет при обращении к нему, и счетчик успешно выполненных запросов.

+ +

Сервис должен реализовывать два POST-метода:

+ +
    +
  1. setNodes - устанавливает граф из узлов, описанных выше. Формат входных данных - JSON. Программа должна исключать циклические связи узлов.
  2. +
  3. checkRoute - принимает набор вершин (или их идентификаторов) в формате JSON и проходит по этим вершинам, проверяя на каждом пройденном узле, не отказал ли он. Если путь существует в графе и ни один из узлов пути не отказал, следует увеличить счетчик в каждом из узлов пути. В противном случае отображать ошибку в ответе POST-метода (произвольный формат).
  4. +
  5. Узлы и связи должны храниться в базе данных.
  6. +
+ +

Изменённый вариант задания

+ +

После введённых корректировок, итоговый вид тестового задания выглядит следующим образом:

+ +

Разработать REST-сервис, проверяющий работоспособность любой последовательности узлов.

+ +
    +
  1. Каждый узел имеет уникальное имя и счетчик успешно выполненных запросов, так же для хранения в базе данных есть уникальный идентификатор, который присваивается автоматически при записи в БД. Был исключен элемент вероятность отказа узла. Этот параметр перестал быть нужным, так как вероятность отказа узла стала случайным фактором, возникающая автоматически, во время проверки последовательности узлов.
  2. +
  3. Граф в программе неориентированный, т.е. вершины графа связаны друг с другом рёбрами, не имеющими направления. В базе данных, хранение графа осуществляется в двух таблицах. В одной таблице осуществляется хранение набора узлов графа, в другой набор рёбер графа. Более подробно смотрите запись в wiki План работы
  4. +
  5. Программа позволяет делать следующее: +
      +
    1. Работать с графом обобщённо: +
        +
      1. Создавать новый граф (при этом информация о прежнем графе будет удалена с БД)
      2. +
      3. Извлекать информацию о графе в заданном формате
      4. +
      5. Удалять целиком весь граф
      6. +
      7. Проверять работоспособность заданной последовательности узлов (по условию задачи) выполнив соответствующий запрос.
      8. +
      +
    2. +
    3. Работать с узлами и ребрами по отдельности, т.е. добавлять, удалять, искать информацию по заданным параметрам. Ручное изменение какой-либо информации о узле и ребре не предусмотрена, т.е. возможно либо добавления узла или ребра в БД или удаление из БД)
    4. +
    +
  6. +
+ +

Так как по условию задания, граф должен исключить все виды циклов (т.е. граф должен быть ациклическим), то при любом запросе информации о графе целиком или же при проверки набора заданных узлов будет происходить автоматический поиск и удаление циклов из графа. Удаление циклов происходит при помощи удаления набора рёбер, создающие циклы. Поэтому в случае обнаружения циклов в графе, исходный набор рёбер будет отличаться от набора рёбер хранящихся в БД.

+ +

Автоматический поиск и удаление циклов не срабатывает, если происходит работа только с набором данных рёбер графа в отдельности, т.е. можно добавлять в базу рёбра, образующие циклы.

+ +

Используемый стек : Spring Boot, Spring Data, ORM (Hibernate), JGraphT : (для работы с графом), GSON (используется вместо используемого по умолчанию Jackson для работы с json), + Thymeleaf и Bootstrap (используется для формирования стартовой информационной страницы), Mockito (идёт вместе с Spring Boot), + Powermock (подключается отдельной библиотекой, используется в дополнении к mockito для тестов)

+ +

Хранилище данных : PostgeSQL (для production), H2 (для тестов)

+
+
+ + + + \ No newline at end of file diff --git a/src/test/java/ru/resprojects/linkchecker/TestUtils.java b/src/test/java/ru/resprojects/linkchecker/TestUtils.java new file mode 100644 index 0000000..9c6e970 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/TestUtils.java @@ -0,0 +1,48 @@ +package ru.resprojects.linkchecker; + +import com.google.gson.Gson; +import ru.resprojects.linkchecker.dto.GraphDto; + +import java.lang.reflect.Type; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Helper class for unit testing. + */ +public class TestUtils { + + public static final GraphDto.NodeGraph nodeGraph = new GraphDto.NodeGraph(5000, "v1", 0); + public static final GraphDto.EdgeGraph edgeGraph = new GraphDto.EdgeGraph(5005, "v1", "v2"); + public static final Set nodesGraph = Stream.of( + nodeGraph, + new GraphDto.NodeGraph(5001, "v2", 0), + new GraphDto.NodeGraph(5002, "v3", 0), + new GraphDto.NodeGraph(5003, "v4", 0), + new GraphDto.NodeGraph(5004, "v5", 0) + ).collect(Collectors.toSet()); + public static final Set edgesGraph = Stream.of( + edgeGraph, + new GraphDto.EdgeGraph(5006, "v1", "v3"), + new GraphDto.EdgeGraph(5007, "v1", "v5"), + new GraphDto.EdgeGraph(5008, "v3", "v4") + ).collect(Collectors.toSet()); + public static final GraphDto graph = new GraphDto(nodesGraph, edgesGraph); + + private TestUtils() { + } + + public static String mapToJson(Object obj) { + return new Gson().toJson(obj); + } + + public static T mapFromJson(String json, Type type) { + return new Gson().fromJson(json, type); + } + + public static T mapFromJson(String json, Class clazz) { + return new Gson().fromJson(json, clazz); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/model/ValidationModelTests.java b/src/test/java/ru/resprojects/linkchecker/model/ValidationModelTests.java new file mode 100644 index 0000000..f42536f --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/model/ValidationModelTests.java @@ -0,0 +1,86 @@ +package ru.resprojects.linkchecker.model; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.util.ValidationUtil; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = "moc_test") +public class ValidationModelTests { + + private static final Logger LOG = LoggerFactory.getLogger(ValidationModelTests.class); + private static ValidatorFactory validatorFactory; + private static Validator validator; + + @BeforeClass + public static void createValidator() { + validatorFactory = Validation.buildDefaultValidatorFactory(); + validator = validatorFactory.getValidator(); + } + + @AfterClass + public static void close() { + validatorFactory.close(); + } + + @Test + public void tryCreateNodeWithBlankName() { + Node node = new Node(""); + Set> violations = validator.validate(node); + Assert.assertEquals(2, violations.size()); + ConstraintViolation violation = violations.stream() + .filter(v -> ValidationUtil.VALIDATOR_NODE_NOT_BLANK_NAME_MESSAGE.equals(v.getMessage())) + .findFirst() + .orElse(null); + Assert.assertNotNull(violation); + violation = violations.stream() + .filter(v -> ValidationUtil.VALIDATOR_NODE_NAME_RANGE_MESSAGE.equals(v.getMessage())) + .findFirst() + .orElse(null); + Assert.assertNotNull(violation); + printViolationMessage(violations); + } + + @Test + public void tryCreateNodeWithTooLongNodeName() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 51; i++) { + sb.append('a'); + } + Node node = new Node(sb.toString()); + Set> violations = validator.validate(node); + Assert.assertEquals(1, violations.size()); + ConstraintViolation violation = violations.stream() + .filter(v -> ValidationUtil.VALIDATOR_NODE_NAME_RANGE_MESSAGE.equals(v.getMessage())) + .findFirst() + .orElse(null); + Assert.assertNotNull(violation); + printViolationMessage(violations); + } + + private static void printViolationMessage(Set> violations) { + violations.forEach(v -> { + LOG.info("VIOLATION INFO"); + LOG.info("--> MESSAGE: " + v.getMessage()); + LOG.info("--> PROPERTY PATH: " + v.getPropertyPath().toString()); + LOG.info("--> INVALID VALUE: " + v.getInvalidValue()); + }); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/repositories/EdgeRepositoryH2DBTests.java b/src/test/java/ru/resprojects/linkchecker/repositories/EdgeRepositoryH2DBTests.java new file mode 100644 index 0000000..ff7f34d --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/repositories/EdgeRepositoryH2DBTests.java @@ -0,0 +1,144 @@ +package ru.resprojects.linkchecker.repositories; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class EdgeRepositoryH2DBTests { + + private static final Logger LOG = LoggerFactory.getLogger(EdgeRepositoryH2DBTests.class); + + @Autowired + EdgeRepository edgeRepository; + + @Autowired + NodeRepository nodeRepository; + + @Test + public void persistNewEdge() { + Node nodeOne = nodeRepository.save(new Node("v6")); + Node nodeTwo = nodeRepository.getByName("v5"); + Edge edge = edgeRepository.save(new Edge(nodeOne, nodeTwo)); + Assert.assertNotNull(edge); + LOG.info("New Edge: " + edge); + List edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + Assert.assertEquals(5, edges.size()); + LOG.info("EDGES count = " + edges.size()); + } + + @Test + public void persistEdgeList() { + List nodes = nodeRepository.findAll(); + Node nodeOne = nodeRepository.save(new Node("v6")); + List edges = new ArrayList<>(); + nodes.forEach(node -> edges.add(new Edge(nodeOne, node))); + edgeRepository.saveAll(edges); + List newEdgeList = edgeRepository.findAll(); + Assert.assertNotNull(newEdgeList); + Assert.assertEquals(9, newEdgeList.size()); + LOG.info("Size: " + newEdgeList.size()); + newEdgeList.forEach(edge -> LOG.info(edge.toString())); + } + + @Test + public void getAllEdges() { + List edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + Assert.assertEquals(4, edges.size()); + LOG.info("LIST count = " + edges.size()); + edges.forEach(edge -> LOG.info(edge.toString())); + } + + @Test + public void getEdgeById() { + Edge edge = edgeRepository.findById(5005).orElse(null); + Assert.assertNotNull(edge); + Assert.assertEquals("v1", edge.getNodeOne().getName()); + LOG.info("EDGE INFO WITH ID = 5005: " + edge); + } + + @Test + public void getEdgeByNodeOneAndNodeTwo() { + Node nodeOne = nodeRepository.getByName("v1"); + Node nodeTwo = nodeRepository.getByName("v2"); + Edge edge = edgeRepository.findEdgeByNodeOneAndNodeTwo(nodeOne, nodeTwo).orElse(null); + Assert.assertNotNull(edge); + Assert.assertEquals(new Integer(5005), edge.getId()); + LOG.info("EDGE FOR NODES v1 and v2: " + edge); + } + + @Test + public void getEdgeByNodeOneOrNodeTwo() { + Node nodeOne = nodeRepository.getByName("v1"); + Node nodeTwo = nodeRepository.getByName("v2"); + List edges = edgeRepository.findEdgesByNodeOneOrNodeTwo(nodeOne, null); + Assert.assertNotNull(edges); + Assert.assertNotEquals(0, edges.size()); + edges.forEach(edge -> LOG.info(edge.toString())); + LOG.info("-------------"); + edges = edgeRepository.findEdgesByNodeOneOrNodeTwo(null, nodeTwo); + Assert.assertNotNull(edges); + Assert.assertNotEquals(0, edges.size()); + edges.forEach(edge -> LOG.info(edge.toString())); + LOG.info("-------------"); + edges = edgeRepository.findEdgesByNodeOneOrNodeTwo(nodeOne, nodeTwo); + Assert.assertNotNull(edges); + Assert.assertNotEquals(0, edges.size()); + edges.forEach(edge -> LOG.info(edge.toString())); + LOG.info("-------------"); + edges = edgeRepository.findEdgesByNodeOneOrNodeTwo(nodeTwo, nodeTwo); + Assert.assertNotNull(edges); + Assert.assertNotEquals(0, edges.size()); + edges.forEach(edge -> LOG.info(edge.toString())); + } + + @Test + public void deleteEdgesInBatch() { + Node node = nodeRepository.getByName("v1"); + List edges = edgeRepository.findEdgesByNodeOneOrNodeTwo(node, node); + Assert.assertNotNull(edges); + edgeRepository.deleteInBatch(edges); + edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + edges.forEach(edge -> LOG.info(edge.toString())); + } + + @Test + public void deleteById() { + edgeRepository.deleteById(5005); + List edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + Assert.assertEquals(3, edges.size()); + LOG.info("EDGES count = " + edges.size()); + } + + @Test + public void deleteAllEdges() { + edgeRepository.deleteAllInBatch(); + List edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + Assert.assertEquals(0, edges.size()); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/repositories/NodeRepositoryH2DBTests.java b/src/test/java/ru/resprojects/linkchecker/repositories/NodeRepositoryH2DBTests.java new file mode 100644 index 0000000..22e6f24 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/repositories/NodeRepositoryH2DBTests.java @@ -0,0 +1,139 @@ +package ru.resprojects.linkchecker.repositories; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class NodeRepositoryH2DBTests { + + private static final Logger LOG = LoggerFactory.getLogger(NodeRepositoryH2DBTests.class); + + @Autowired + NodeRepository nodeRepository; + + @Autowired + EdgeRepository edgeRepository; + + @Test + public void persistNewNode() { + Node node = new Node("v11"); + Node savedNode = nodeRepository.save(node); + Assert.assertNotNull(savedNode.getId()); + LOG.info(nodeRepository.getByName("v11").toString()); + int count = nodeRepository.findAll().size(); + Assert.assertEquals(6, count); + LOG.info("LIST count = " + count); + } + + @Test + public void persistNodeList() { + List nodes = new ArrayList<>(); + IntStream.range(1, 60).forEach(i -> nodes.add(new Node("w" + i))); + nodeRepository.saveAll(nodes); + List savedNodes = nodeRepository.findAll(); + Assert.assertNotNull(savedNodes); + Assert.assertEquals(64, savedNodes.size()); + LOG.info("LIST count = " + savedNodes.size()); + savedNodes.forEach(node -> LOG.info(node.toString())); + } + + @Test + public void getAllNodes() { + List nodes = nodeRepository.findAll(); + Assert.assertNotNull(nodes); + Assert.assertEquals(5, nodes.size()); + LOG.info("LIST count = " + nodes.size()); + nodes.forEach(node -> LOG.info(node.toString())); + } + + @Test + public void getNodeById() { + Node node = nodeRepository.findById(5000).orElse(null); + Assert.assertNotNull(node); + Assert.assertEquals("v1", node.getName()); + LOG.info("NODE INFO WITH ID = 5000: " + node); + } + + @Test + public void nodeNotFoundById() { + Node node = nodeRepository.findById(5010).orElse(null); + Assert.assertNull(node); + } + + @Test + public void getNodeByName() { + Node node = nodeRepository.getByName("v1"); + Assert.assertNotNull(node); + Assert.assertEquals("v1", node.getName()); + LOG.info("NODE INFO WITH ID = 5000: " + node); + } + + @Test + public void nodeNotFoundIfNameIsNull() { + Node node = nodeRepository.getByName(null); + Assert.assertNull(node); + } + + @Test + public void nodeNotFoundByName() { + Node node = nodeRepository.getByName("v11"); + Assert.assertNull(node); + } + + @Test + public void deleteById() { + nodeRepository.deleteById(5000); + List nodes = nodeRepository.findAll(); + Assert.assertNotNull(nodes); + Assert.assertEquals(4, nodes.size()); + LOG.info("LIST count = " + nodes.size()); + nodes.forEach(node -> LOG.info(node.toString())); + } + + @Test + public void deleteByName() { + nodeRepository.deleteByName("v1"); + List nodes = nodeRepository.findAll(); + Assert.assertNotNull(nodes); + Assert.assertEquals(4, nodes.size()); + LOG.info("LIST count = " + nodes.size()); + nodes.forEach(node -> LOG.info(node.toString())); + } + + @Test + public void cascadeDeleteAllNodesAndEdges() { + nodeRepository.deleteAllInBatch(); + List nodes = nodeRepository.findAll(); + Assert.assertNotNull(nodes); + Assert.assertEquals(0, nodes.size()); + List edges = edgeRepository.findAll(); + Assert.assertNotNull(edges); + Assert.assertEquals(0, edges.size()); + } + + @Test + public void existNodeByName() { + Assert.assertTrue(nodeRepository.existsByName("v1")); + } +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceH2DBTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceH2DBTests.java new file mode 100644 index 0000000..7cf93ff --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceH2DBTests.java @@ -0,0 +1,259 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class GraphEdgeServiceH2DBTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphEdgeServiceH2DBTests.class); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Autowired + private GraphEdgeService edgeService; + + @Autowired + private AppProperties properties; + + @Test + public void createEdge() { + EdgeGraph edgeGraph = new EdgeGraph("v1", "v4"); + EdgeGraph actual = edgeService.create(edgeGraph); + Assert.assertNotNull(actual); + Assert.assertEquals(new Integer(5009), actual.getId()); + Set egList = edgeService.getAll(); + Assert.assertEquals(5, egList.size()); + egList.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void createEdgeNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + edgeService.create((EdgeGraph) null); + } + + @Test + public void createEdgeNodeNotFoundException() { + EdgeGraph edgeGraph = new EdgeGraph("v10", "v4"); + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v10")); + edgeService.create(edgeGraph); + } + + @Test + public void createEdgeAlreadyPresentException() { + EdgeGraph edgeGraph = new EdgeGraph("v1", "v2"); + thrown.expect(ApplicationException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), "v1", "v2", "v2", "v1")); + edgeService.create(edgeGraph); + } + + @Test + public void createEdgeAlreadyPresentVariantTwoException() { + EdgeGraph edgeGraph = new EdgeGraph("v2", "v1"); + thrown.expect(ApplicationException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), "v2", "v1", "v1", "v2")); + edgeService.create(edgeGraph); + } + + @Test + public void createEdges() { + Set edgeGraphs = Stream.of( + new EdgeGraph("v2", "v3"), + new EdgeGraph("v2", "v5"), + new EdgeGraph("v3", "v5") + ).collect(Collectors.toSet()); + Set actual = edgeService.create(edgeGraphs); + Assert.assertFalse(actual.isEmpty()); + Assert.assertNotNull(actual.iterator().next().getId()); + actual.forEach(eg -> LOG.info("---- RETURNED EDGE: " + eg)); + Set egList = edgeService.getAll(); + Assert.assertEquals(7, egList.size()); + egList.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void createEdgesEmptyCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_EMPTY")); + edgeService.create(new HashSet<>()); + } + + @Test + public void createEdgesNodeNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v21")); + Set edgeGraphs = Stream.of( + new EdgeGraph("v21", "v3"), + new EdgeGraph("v2", "v5"), + new EdgeGraph("v3", "v5") + ).collect(Collectors.toSet()); + edgeService.create(edgeGraphs); + } + + @Test + public void createEdgesCollectionContainNullException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL")); + Set edgeGraphs = Stream.of( + null, + new EdgeGraph("v2", "v5"), + new EdgeGraph("v3", "v5") + ).collect(Collectors.toSet()); + edgeService.create(edgeGraphs); + } + + @Test + public void createEdgesEdgeAlreadyPresentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), "v1", "v2", "v2", "v1")); + Set edgeGraphs = Stream.of( + new EdgeGraph("v1", "v2"), + new EdgeGraph("v2", "v5") + ).collect(Collectors.toSet()); + edgeService.create(edgeGraphs); + } + + @Test + public void deleteEdgeById() { + edgeService.delete(5005); + Set egList = edgeService.getAll(); + Assert.assertEquals(3, egList.size()); + egList.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void deleteEdgeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "EDGE", 5022)); + edgeService.delete(5022); + } + + @Test + public void deleteEdgeByNodeOneAndNodeTwoNames() { + edgeService.delete("v1", "v2"); + Set actual = edgeService.getAll(); + actual.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void deleteEdgeByNodeOneAndNodeTwoNamesNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), "v15", "v2")); + edgeService.delete("v15", "v2"); + } + + @Test + public void deleteEdgeByNodeName() { + edgeService.delete("v1"); + Set actual = edgeService.getAll(); + actual.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void deleteEdgeByNodeNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), "v15")); + edgeService.delete("v15"); + } + + @Test + public void deleteAllEdges() { + edgeService.deleteAll(); + Set actual = edgeService.getAll(); + Assert.assertTrue(actual.isEmpty()); + } + + @Test + public void getAllEdges() { + Set actual = edgeService.getAll(); + Assert.assertEquals(4, actual.size()); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5007)) + .findFirst() + .get().getNodeOne()).isEqualTo("v1"); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5007)) + .findFirst() + .get().getNodeTwo()).isEqualTo("v5"); + actual.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void getEdgeById() { + EdgeGraph actual = edgeService.getById(5005); + Assert.assertNotNull(actual); + Assert.assertEquals(TestUtils.edgeGraph, actual); + LOG.info(actual.toString()); + } + + @Test + public void getEdgeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "EDGE", 7000)); + edgeService.getById(7000); + } + + @Test + public void getEdgesByNodeName() { + Set actual = edgeService.get("v1"); + Set expected = TestUtils.edgesGraph.stream() + .filter(eg -> eg.getId() != 5008) + .collect(Collectors.toSet()); + Assert.assertFalse(actual.isEmpty()); + Assert.assertEquals(expected.size(), actual.size()); + assertThat(actual).containsAnyOf(expected.iterator().next()); + actual.forEach(eg -> LOG.info("---- EDGE: " + eg)); + } + + @Test + public void getEdgesByNodeNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), "v100")); + edgeService.get("v100"); + } + + @Test + public void getEdgeByNodeNameOneAndNodeNameTwo() { + EdgeGraph actual = edgeService.get("v1", "v2"); + Assert.assertNotNull(actual); + Assert.assertEquals(TestUtils.edgeGraph, actual); + Integer actualId = actual.getId(); + LOG.info(actual.toString()); + actual = edgeService.get("v2", "v1"); + Assert.assertEquals(actualId, actual.getId()); //must equal because graph is undirected + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceMockTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceMockTests.java new file mode 100644 index 0000000..96a5090 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphEdgeServiceMockTests.java @@ -0,0 +1,314 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; +import ru.resprojects.linkchecker.repositories.EdgeRepository; +import ru.resprojects.linkchecker.repositories.NodeRepository; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = "moc_test") +public class GraphEdgeServiceMockTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @MockBean + private EdgeRepository edgeRepository; + + @MockBean + private NodeRepository nodeRepository; + + @Autowired + private AppProperties properties; + + private GraphEdgeService edgeService; + + private List nodes; + + @Before + public void init() { + edgeService = new GraphEdgeServiceImpl(edgeRepository, nodeRepository, properties); + nodes = Stream.of( + new Node(5000, "v1", 0), + new Node(5001, "v2", 0), + new Node(5002, "v3", 0), + new Node(5003, "v4", 0), + new Node(5004, "v5", 0) + ).collect(Collectors.toList()); + } + + @Test + public void createEdge() { + Node nodeOne = nodes.get(0); + Node nodeTwo = nodes.get(1); + Edge edge = new Edge(5005, nodeOne, nodeTwo); + EdgeGraph edgeGraph = new EdgeGraph("v1", "v2"); + given(nodeRepository.getByName("v1")).willReturn(nodeOne); + given(nodeRepository.getByName("v2")).willReturn(nodeTwo); + when(edgeRepository.save(any(Edge.class))).thenReturn(edge); + EdgeGraph actual = edgeService.create(edgeGraph); + Assert.assertNotNull(actual); + Assert.assertEquals(new Integer(5005), actual.getId()); + } + + @Test + public void createEdgeNodeNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v1")); + EdgeGraph edgeGraph = new EdgeGraph("v1", "v2"); + given(nodeRepository.getByName("v1")).willReturn(null); + edgeService.create(edgeGraph); + } + + @Test + public void createEdgeNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + edgeService.create((EdgeGraph) null); + } + + @Test + public void createEdges() { + List edges = new ArrayList<>(); + edges.add(new Edge(5005,nodes.get(0), nodes.get(1))); + edges.add(new Edge(5006,nodes.get(0), nodes.get(2))); + edges.add(new Edge(5007,nodes.get(0), nodes.get(4))); + edges.add(new Edge(5008,nodes.get(2), nodes.get(3))); + Set edgeGraphs = edges.stream() + .map(e -> new EdgeGraph(e.getNodeOne().getName(), e.getNodeTwo().getName())) + .collect(Collectors.toSet()); + given(nodeRepository.getByName("v1")).willReturn(nodes.get(0)); + given(nodeRepository.getByName("v2")).willReturn(nodes.get(1)); + given(nodeRepository.getByName("v3")).willReturn(nodes.get(2)); + given(nodeRepository.getByName("v4")).willReturn(nodes.get(3)); + given(nodeRepository.getByName("v5")).willReturn(nodes.get(4)); + when(edgeRepository.saveAll(anyIterable())).thenReturn(edges); + Set actual = edgeService.create(edgeGraphs); + Assert.assertNotNull(actual); + Assert.assertEquals(4, actual.size()); + } + + @Test + public void createEdgesNodeNotFoundException() { + List edges = Stream.of( + new Edge(5005,nodes.get(0), nodes.get(1)), + new Edge(5006,nodes.get(0), nodes.get(2)), + new Edge(5007,nodes.get(0), nodes.get(4)), + new Edge(5008,nodes.get(2), nodes.get(3)) + ).collect(Collectors.toList()); + Set edgeGraphs = edges.stream() + .map(e -> new EdgeGraph(e.getNodeOne().getName(), e.getNodeTwo().getName())) + .collect(Collectors.toSet()); + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v1")); + given(nodeRepository.getByName("v1")).willReturn(null); + edgeService.create(edgeGraphs); + } + + @Test + public void createEdgesNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + edgeService.create((Set) null); + } + + @Test + public void createEdgesEmptyCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_EMPTY")); + edgeService.create(new HashSet<>()); + } + + @Test + public void createEdgesCollectionContainNullException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL")); + Set edgeGraphs = new HashSet<>(); + edgeGraphs.add(new EdgeGraph("v1", "v2")); + edgeGraphs.add(null); + edgeService.create(edgeGraphs); + } + + @Test + public void deleteEdgeById() { + List edges = Stream.of( + new Edge(5008, nodes.get(2), nodes.get(3)) + ).collect(Collectors.toList()); + given(edgeRepository.existsById(new Integer(5005))).willReturn(true); + given(edgeRepository.findAll()).willReturn(edges); + edgeService.delete(5005); + Set actual = edgeService.getAll(); + Assert.assertEquals(1, actual.size()); + } + + @Test + public void deleteEdgeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "EDGE", 5050)); + when(edgeRepository.existsById(5050)).thenReturn(false); + edgeService.delete(5050); + } + + @Test + public void deleteEdgesByNodeName() { + List edges = Stream.of( + new Edge(5005,nodes.get(0), nodes.get(1)), + new Edge(5006,nodes.get(0), nodes.get(2)), + new Edge(5007,nodes.get(0), nodes.get(4)) + ).collect(Collectors.toList()); + List edgesAfterDelete = Stream.of( + new Edge(5008, nodes.get(2), nodes.get(3)) + ).collect(Collectors.toList()); + given(nodeRepository.getByName("v1")).willReturn(nodes.get(0)); + given(edgeRepository.findEdgesByNodeOneOrNodeTwo(any(Node.class), any(Node.class))).willReturn(edges); + given(edgeRepository.findAll()).willReturn(edgesAfterDelete); + edgeService.delete("v1"); + Set actual = edgeService.getAll(); + Assert.assertEquals(1, actual.size()); + } + + @Test + public void deleteEdgesByNodeNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), "v1")); + List emptyList = new ArrayList<>(); + given(edgeRepository.findEdgesByNodeOneOrNodeTwo(any(Node.class), any(Node.class))). + willReturn(emptyList); + edgeService.delete("v1"); + } + + @Test + public void deleteEdgeByNodeNameOneAndNodeNameTwo() { + Edge edge = new Edge(5005,nodes.get(0), nodes.get(1)); + given(nodeRepository.getByName("v1")).willReturn(nodes.get(0)); + given(nodeRepository.getByName("v2")).willReturn(nodes.get(1)); + given(edgeRepository.findEdgeByNodeOneAndNodeTwo(any(Node.class), any(Node.class))). + willReturn(Optional.of(edge)); + edgeService.delete("v1", "v2"); + } + + @Test + public void deleteEdgeByNodeNameOneAndNodeNameTwoNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), "v1", "v2")); + edgeService.delete("v1", "v2"); + } + + @Test + public void getAllEdges() { + List edges = Stream.of( + new Edge(5005, nodes.get(0), nodes.get(1)), + new Edge(5006, nodes.get(0), nodes.get(2)), + new Edge(5007, nodes.get(0), nodes.get(4)), + new Edge(5008, nodes.get(2), nodes.get(3)) + ).collect(Collectors.toList()); + given(edgeRepository.findAll()).willReturn(edges); + Set actual = edgeService.getAll(); + EdgeGraph edgeGraph = GraphUtil.edgeToEdgeGraph(edges.get(2)); + Assert.assertEquals(4, actual.size()); + assertThat(actual).contains(edgeGraph); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5007)) + .findFirst() + .get().getNodeOne()).isEqualTo("v1"); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5007)) + .findFirst() + .get().getNodeTwo()).isEqualTo("v5"); + } + + @Test + public void getEdgeById() { + Node nodeOne = nodes.get(0); + Node nodeTwo = nodes.get(1); + Edge edge = new Edge(5005, nodeOne, nodeTwo); + given(edgeRepository.findById(5005)).willReturn(Optional.of(edge)); + EdgeGraph actual = edgeService.getById(5005); + Assert.assertEquals("v1", actual.getNodeOne()); + Assert.assertEquals("v2", actual.getNodeTwo()); + } + + @Test + public void getEdgeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "EDGE", 7000)); + edgeService.getById(7000); + } + + @Test + public void getEdgesByNodeName() { + List edges = Stream.of( + new Edge(5005, nodes.get(0), nodes.get(1)), + new Edge(5006, nodes.get(0), nodes.get(2)), + new Edge(5007, nodes.get(0), nodes.get(4)) + ).collect(Collectors.toList()); + given(nodeRepository.getByName("v1")).willReturn(nodes.get(0)); + given(edgeRepository.findEdgesByNodeOneOrNodeTwo(nodes.get(0),nodes.get(0))) + .willReturn(edges); + Set actual = edgeService.get("v1"); + EdgeGraph edgeGraph = GraphUtil.edgeToEdgeGraph(edges.get(0)); + assertThat(actual).contains(edgeGraph); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5005)) + .findFirst() + .get().getNodeOne()).isEqualTo("v1"); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5005)) + .findFirst() + .get().getNodeTwo()).isEqualTo("v2"); + } + + @Test + public void getEdgeByNodeOneAndNodeTwoNames() { + Node nodeOne = nodes.get(0); + Node nodeTwo = nodes.get(1); + Edge edge = new Edge(5005, nodeOne, nodeTwo); + given(nodeRepository.getByName("v1")).willReturn(nodeOne); + given(nodeRepository.getByName("v2")).willReturn(nodeTwo); + given(edgeRepository.findEdgeByNodeOneAndNodeTwo(nodeOne, nodeTwo)).willReturn(Optional.of(edge)); + EdgeGraph actual = edgeService.get("v1", "v2"); + Assert.assertNotNull(actual); + Assert.assertEquals("v1", actual.getNodeOne()); + Assert.assertEquals("v2", actual.getNodeTwo()); + } + + @Test + public void exceptionWhileEdgeByNodeOneAndNodeTwoNames() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), "v1", "v2")); + edgeService.get("v1", "v2"); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceH2DBTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceH2DBTests.java new file mode 100644 index 0000000..4f72fa6 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceH2DBTests.java @@ -0,0 +1,199 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class GraphNodeServiceH2DBTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphNodeServiceH2DBTests.class); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Autowired + GraphNodeService nodeService; + + @Autowired + AppProperties properties; + + @Test + public void getNodeByName() { + NodeGraph nodeGraph = nodeService.get("v1"); + Assert.assertNotNull(nodeGraph); + Assert.assertEquals("v1", nodeGraph.getName()); + LOG.info("NODE DTO: " + nodeGraph); + } + + @Test + public void getNodeById() { + NodeGraph nodeGraph = nodeService.getById(5000); + Assert.assertNotNull(nodeGraph); + Assert.assertEquals("v1", nodeGraph.getName()); + LOG.info("NODE DTO: " + nodeGraph); + } + + @Test + public void getNodeByNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v11")); + nodeService.get("v11"); + } + + @Test + public void getNodeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "NODE", 5050)); + nodeService.getById(5050); + } + + @Test + public void getAllNodes() { + Set actual = nodeService.getAll(); + Assert.assertEquals(5, actual.size()); + assertThat(actual.stream() + .filter(eg -> eg.getId().equals(5000)) + .findFirst() + .get().getName()).isEqualTo("v1"); + actual.forEach(ng -> LOG.info("---- NODE: " + ng)); + } + + @Test + public void deleteNodeByNodeGraph() { + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 0); + nodeService.delete(nodeGraph); + Set actual = nodeService.getAll(); + Assert.assertEquals(4, actual.size()); + } + + @Test + public void deleteNodeByNodeGraphNotFoundException() { + NodeGraph nodeGraph = new NodeGraph(5020, "v1", 0); + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), nodeGraph.toString())); + nodeService.delete(nodeGraph); + } + + @Test + public void deleteNodeByNodeGraphAnotherNotFoundException() { + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 1); + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), nodeGraph.toString())); + nodeService.delete(nodeGraph); + } + + @Test + public void deleteNodeByName() { + nodeService.delete("v1"); + Set actual = nodeService.getAll(); + Assert.assertEquals(4, actual.size()); + } + + @Test + public void deleteNodeByNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v10")); + nodeService.delete("v10"); + } + + @Test + public void deleteNodeById() { + nodeService.delete(5000); + Set actual = nodeService.getAll(); + Assert.assertEquals(4, actual.size()); + } + + @Test + public void deleteNodeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "NODE", 5100)); + nodeService.delete(5100); + } + + @Test + public void deleteAllNodes() { + nodeService.deleteAll(); + Set nodeGraphs = nodeService.getAll(); + Assert.assertNotNull(nodeGraphs); + Assert.assertEquals(0, nodeGraphs.size()); + } + + @Test + public void createNode() { + NodeGraph nodeGraph = new NodeGraph("v6"); + nodeService.create(nodeGraph); + NodeGraph actual = nodeService.get("v6"); + Assert.assertNotNull(actual); + Set nodeGraphs = nodeService.getAll(); + nodeGraphs.forEach(ng -> LOG.info("---- NODE: " + ng)); + } + + @Test + public void createNodes() { + Set nodeGraphs = new HashSet<>(); + IntStream.range(1, 6).forEach(i -> { + nodeGraphs.add(new NodeGraph("w" + i)); + }); + nodeService.create(nodeGraphs); + Set actual = nodeService.getAll(); + Assert.assertNotNull(actual); + Assert.assertEquals(10, actual.size()); + Assert.assertEquals("w1", actual.stream() + .filter(ng -> "w1".equals(ng.getName())) + .findFirst().get().getName()); + actual.forEach(ng -> LOG.info("---- NODE: " + ng)); + } + + @Test + public void createNodeNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + nodeService.create((NodeGraph) null); + } + + @Test + public void nodeUpdate() { + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 1); + nodeService.update(nodeGraph); + NodeGraph actual = nodeService.get("v1"); + Assert.assertNotNull(actual); + Assert.assertEquals(1, actual.getCounter(), 0); + Set nodeGraphs = nodeService.getAll(); + nodeGraphs.forEach(ng -> LOG.info("---- NODE: " + ng)); + } + + @Test + public void nodeUpdateNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + nodeService.update(null); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceMockTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceMockTests.java new file mode 100644 index 0000000..08fc2da --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphNodeServiceMockTests.java @@ -0,0 +1,303 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.model.Node; +import ru.resprojects.linkchecker.repositories.NodeRepository; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.when; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = "moc_test") +public class GraphNodeServiceMockTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private GraphNodeService graphNodeService; + + @MockBean + private NodeRepository nodeRepository; + + @Autowired + private AppProperties properties; + + private List nodes = Stream.of( + new Node(5001, "v2", 0), + new Node(5002, "v3", 0), + new Node(5003, "v4", 0), + new Node(5004, "v5", 0) + ).collect(Collectors.toList()); + + @Before + public void init() { + graphNodeService = new GraphNodeServiceImpl(nodeRepository, properties); + } + + @Test + public void getNodeByName() { + given(nodeRepository.getByName("v1")).willReturn( + new Node(5000, "v1", 0) + ); + NodeGraph actual = graphNodeService.get("v1"); + assertThat(actual.getName()).isEqualTo("v1"); + assertThat(actual.getCounter()).isEqualTo(0); + } + + @Test + public void getNodeByNameNotFoundException() throws NotFoundException { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v1")); + graphNodeService.get("v1"); + } + + @Test + public void getNodeById() { + given(nodeRepository.findById(5000)).willReturn( + Optional.of(new Node(5000, "v1", 0)) + ); + NodeGraph actual = graphNodeService.getById(5000); + assertThat(actual.getName()).isEqualTo("v1"); + assertThat(actual.getCounter()).isEqualTo(0); + } + + @Test + public void getNodeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "NODE", 5000)); + graphNodeService.getById(5000); + } + + @Test + public void getAllNodes() { + List nodes = Stream.of( + new Node(5000, "v1", 0), + new Node(5001, "v2", 0), + new Node(5002, "v3", 0), + new Node(5003, "v4", 0), + new Node(5004, "v5", 0) + ).collect(Collectors.toList()); + given(nodeRepository.findAll()).willReturn(nodes); + Set actual = graphNodeService.getAll(); + NodeGraph nodeGraph = GraphUtil.nodeToNodeGraph(nodes.get(4)); + Assert.assertEquals(5, actual.size()); + assertThat(actual).contains(nodeGraph); + } + + @Test + public void deleteNodeByNodeGraph() { + given(nodeRepository.findById(5000)).willReturn( + Optional.of(new Node(5000, "v1", 0)) + ); + given(nodeRepository.findAll()).willReturn(nodes); + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 0); + graphNodeService.delete(nodeGraph); + Set actual = graphNodeService.getAll(); + Assert.assertEquals(4, actual.size()); + } + + @Test + public void deleteNodeByNodeGraphNotFoundException() { + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 0); + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), nodeGraph.toString())); + graphNodeService.delete(nodeGraph); + } + + @Test + public void deleteNodeByNodeGraphWithNullIdNotFoundException() { + NodeGraph nodeGraph = new NodeGraph(null, "v1", 0); + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), nodeGraph.toString())); + graphNodeService.delete(nodeGraph); + } + + @Test + public void deleteNodeByNodeGraphNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphNodeService.delete((NodeGraph) null); + } + + @Test + public void deleteNodeByName() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v1")); + when(nodeRepository.existsByName("v1")).thenReturn(true).thenReturn(false); + given(nodeRepository.findAll()).willReturn(nodes); + graphNodeService.delete("v1"); + Set actual = graphNodeService.getAll(); + Assert.assertEquals(4, actual.size()); + graphNodeService.delete("v1"); + } + + @Test + public void deleteNodeByNameNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "null")); + graphNodeService.delete((String) null); + } + + @Test + public void deleteNodeById() { + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "NODE", 5000)); + when(nodeRepository.existsById(5000)).thenReturn(true).thenReturn(false); + given(nodeRepository.findAll()).willReturn(nodes); + graphNodeService.delete(5000); + Set actual = graphNodeService.getAll(); + Assert.assertEquals(4, actual.size()); + graphNodeService.delete(5000); + } + + @Test + public void deleteNodeByIdNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage( String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), "NODE", null)); + graphNodeService.delete((Integer) null); + } + + @Test + public void createNode() { + NodeGraph nodeGraph = new NodeGraph("v1"); + Node node = new Node("v1"); + node.setId(5000); + when(nodeRepository.save(any(Node.class))).thenReturn(node); + NodeGraph actual = graphNodeService.create(nodeGraph); + Assert.assertNotNull(actual); + Assert.assertNotNull(actual.getId()); + Assert.assertEquals(5000, actual.getId().intValue()); + Assert.assertEquals("v1", actual.getName()); + } + + @Test + public void createNodeIsPresentException() { + thrown.expect(ApplicationException.class); + NodeGraph nodeGraph = new NodeGraph("v1"); + thrown.expectMessage(String.format( + properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"), + nodeGraph.getName() + )); + Node node = new Node("v1"); + node.setId(5000); + when(nodeRepository.getByName(any(String.class))).thenReturn(node); + graphNodeService.create(nodeGraph); + } + + @Test + public void createNodeNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphNodeService.create((NodeGraph) null); + } + + @Test + public void createNodes() { + Set nodeGraphs = new HashSet<>(); + List nodes = new ArrayList<>(); + IntStream.range(1, 6).forEach(i -> { + nodeGraphs.add(new NodeGraph("w" + i)); + nodes.add(new Node(5000 + i, "w" + i, 0)); + }); + when(nodeRepository.saveAll(anyIterable())).thenReturn(nodes); + Set actual = graphNodeService.create(nodeGraphs); + Assert.assertNotNull(actual); + Assert.assertEquals(5, actual.size()); + } + + @Test + public void createNodesIsPresentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(String.format( + properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"), + "w1" + )); + Set nodeGraphs = new HashSet<>(); + nodeGraphs.add(new NodeGraph("w1")); + Node node = new Node(5000, "w1", 0); + when(nodeRepository.getByName(any(String.class))).thenReturn(node); + graphNodeService.create(nodeGraphs); + } + + @Test + public void createNodesNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphNodeService.create((Set) null); + } + + @Test + public void createNodesEmptyCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_EMPTY")); + graphNodeService.create(new HashSet<>()); + } + + @Test + public void createNodesCollectionContainNullException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL")); + Set nodeGraphs = new HashSet<>(); + nodeGraphs.add(new NodeGraph("v1")); + nodeGraphs.add(null); + graphNodeService.create(nodeGraphs); + } + + @Test + public void updateNode() { + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 2); + Node node = new Node(5000, "v1", 2); + when(nodeRepository.save(any(Node.class))).thenReturn(node); + when(nodeRepository.findById(5000)).thenReturn(Optional.of(node)); + graphNodeService.update(nodeGraph); + NodeGraph actual = graphNodeService.getById(5000); + Assert.assertNotNull(actual); + Assert.assertEquals(nodeGraph, GraphUtil.nodeToNodeGraph(node)); + } + + @Test + public void updateNodeNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphNodeService.update(null); + } + + @Test + public void updateNodeWhileUpdateException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_UPDATE_ERROR"), 5000)); + NodeGraph nodeGraph = new NodeGraph(5000, "v1", 0); + when(nodeRepository.save(any(Node.class))).thenReturn(null); + graphNodeService.update(nodeGraph); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphServiceH2DBTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphServiceH2DBTests.java new file mode 100644 index 0000000..5f12473 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphServiceH2DBTests.java @@ -0,0 +1,194 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.util.exeptions.ApplicationException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class GraphServiceH2DBTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphServiceH2DBTests.class); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Autowired + private GraphService graphService; + + @Autowired + private AppProperties properties; + + private Set edgeGraphSet = Stream.of( + new EdgeGraph("v1", "v2"), + new EdgeGraph("v1", "v3"), + new EdgeGraph("v1", "v4"), + new EdgeGraph("v2", "v3"), + new EdgeGraph("v2", "v4"), + new EdgeGraph("v3", "v4") + ).collect(Collectors.toSet()); + + @Test + public void createGraph() { + GraphDto graphDto = new GraphDto(); + graphDto.setNodes(Stream.of( + new NodeGraph("v1"), + new NodeGraph("v2"), + new NodeGraph("v3"), + new NodeGraph("v4") + ).collect(Collectors.toSet())); + graphDto.setEdges(edgeGraphSet); + GraphDto actual = graphService.create(graphDto); + Assert.assertNotNull(actual); + LOG.info(actual.toString()); + } + + @Test + public void createGraphWithExtraEdges() { + GraphDto graphDto = new GraphDto(); + graphDto.setNodes(Stream.of( + new NodeGraph("v1"), + new NodeGraph("v2"), + new NodeGraph("v3") + ).collect(Collectors.toSet())); + graphDto.setEdges(edgeGraphSet); + GraphDto actual = graphService.create(graphDto); + Assert.assertNotNull(actual); + LOG.info(actual.toString()); + } + + @Test + public void createGraphNullArgumentException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphService.create(null); + } + + @Test + public void createGraphEmptyCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage("NODES: " + properties.getAppMsg().get("MSG_COLLECTION_EMPTY")); + GraphDto graphDto = new GraphDto(); + graphDto.setNodes(new HashSet<>()); + graphDto.setEdges(edgeGraphSet); + graphService.create(graphDto); + } + + @Test + public void getGraphWithRemovedEdges() { + graphService.getEdges().create(Stream.of( + new EdgeGraph("v2", "v3"), + new EdgeGraph("v3", "v5"), + new EdgeGraph("v2", "v4"), + new EdgeGraph("v5", "v4") + ).collect(Collectors.toSet())); + Set edgeGraphs = graphService.getEdges().getAll(); + LOG.info(edgeGraphs.toString()); + GraphDto actual = graphService.get(); + Assert.assertNotNull(actual); + Assert.assertNotEquals(edgeGraphs.size(), actual.getEdges().size()); + LOG.info(actual.toString()); + } + + @Test + public void getGraphWithoutRemovingEdges() { + GraphDto actual = graphService.get(); + Assert.assertNotNull(actual); + Assert.assertEquals(4, actual.getEdges().size()); + LOG.info(actual.toString()); + } + + @Test + public void deleteGraph() { + graphService.clear(); + GraphDto actual = graphService.get(); + Assert.assertNotNull(actual); + Assert.assertEquals(0, actual.getEdges().size()); + Assert.assertEquals(0, actual.getNodes().size()); + } + + @Test + public void getGraphAfterAddedNewNode() { + graphService.getNodes().create(new NodeGraph("v6")); + GraphDto actual = graphService.get(); + Assert.assertNotNull(actual); + Assert.assertEquals(6, actual.getNodes().size()); + Assert.assertEquals(4, actual.getEdges().size()); + } + + @Test + public void checkNodesRoute() { + Set nodeNames = Stream.of("v1", "v2", "v3", "v5").collect(Collectors.toSet()); + int faultCount = 0; + Map nodeErrorStat = new HashMap<>(); + for (int i = 0; i < 100; i++) { + try { + LOG.info(graphService.checkRoute(nodeNames)); + } catch (ApplicationException e) { + String node = e.getMessage().split(" ")[1]; + LOG.info(node); + if (!nodeErrorStat.containsKey(node)) { + nodeErrorStat.put(node, 1); + } else { + Integer val = nodeErrorStat.get(node); + nodeErrorStat.put(node, ++val); + } + faultCount++; + } + } + LOG.info(graphService.get().toString()); + LOG.info("FAULT COUNT for CHECK ROUTE = " + faultCount); + nodeErrorStat.forEach((key, value) -> LOG.info("NODE " + key + " error count = " + value)); + } + + @Test + public void checkRouteNullCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_ARGUMENT_NULL")); + graphService.checkRoute(null); + } + + @Test + public void checkRouteEmptyCollectionException() { + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_EMPTY")); + graphService.checkRoute(new HashSet<>()); + } + + @Test + public void checkRouteCollectionHaveOneElementException() { + Set nodeNames = Stream.of("v10").collect(Collectors.toSet()); + thrown.expect(ApplicationException.class); + thrown.expectMessage(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_ONE_ELEMENT")); + graphService.checkRoute(nodeNames); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/services/GraphServiceMockTests.java b/src/test/java/ru/resprojects/linkchecker/services/GraphServiceMockTests.java new file mode 100644 index 0000000..5f7d609 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/services/GraphServiceMockTests.java @@ -0,0 +1,110 @@ +package ru.resprojects.linkchecker.services; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.util.GraphUtil; +import ru.resprojects.linkchecker.util.exeptions.NotFoundException; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.when; +import static org.powermock.api.mockito.PowerMockito.spy; + +//How to use PowerMock https://www.baeldung.com/intro-to-powermock +//How to use PowerMock and SpringRunner https://stackoverflow.com/a/57780838 +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = "moc_test") +@PrepareForTest(GraphUtil.class) +@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", + "javax.xml.transform.*", "org.xml.*", "javax.management.*", + "javax.net.ssl.*", "com.sun.org.apache.xalan.internal.xsltc.trax.*"}) +public class GraphServiceMockTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private GraphService graphService; + + @MockBean + private GraphEdgeService edgeService; + + @MockBean + private GraphNodeService nodeService; + + @Autowired + private AppProperties properties; + + @Before + public void init() { + graphService = new GraphServiceImpl(edgeService, nodeService, properties); + } + + @Test + public void checkRouteNodeFaultException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_IS_FAULT"), "v1")); + spy(GraphUtil.class); + Map nodesFault = new HashMap<>(); + nodesFault.put("v1", true); + nodesFault.put("v2", false); + nodesFault.put("v3", false); + given(nodeService.getAll()).willReturn(TestUtils.nodesGraph); + given(edgeService.getAll()).willReturn(TestUtils.edgesGraph); + when(GraphUtil.getRandomNodeFault(anyCollection())).thenReturn(nodesFault); + graphService.checkRoute(Stream.of("v1", "v2", "v3").collect(Collectors.toSet())); + } + + @Test + public void checkRouteNodeNotReachableException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_NOT_REACHABLE"), "v1", "v4")); + spy(GraphUtil.class); + Map nodesFault = new HashMap<>(); + nodesFault.put("v1", false); + nodesFault.put("v2", false); + nodesFault.put("v3", false); + nodesFault.put("v4", false); + given(nodeService.getAll()).willReturn(TestUtils.nodesGraph); + given(edgeService.getAll()).willReturn(TestUtils.edgesGraph); + when(GraphUtil.getRandomNodeFault(anyCollection())).thenReturn(nodesFault); + graphService.checkRoute(Stream.of("v1", "v2", "v4").collect(Collectors.toSet())); + } + + @Test + public void checkRouteNodeNotFoundException() { + thrown.expect(NotFoundException.class); + thrown.expectMessage(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v7")); + spy(GraphUtil.class); + Map nodesFault = new HashMap<>(); + nodesFault.put("v1", false); + nodesFault.put("v2", false); + nodesFault.put("v3", false); + given(nodeService.getAll()).willReturn(TestUtils.nodesGraph); + given(edgeService.getAll()).willReturn(TestUtils.edgesGraph); + when(GraphUtil.getRandomNodeFault(anyCollection())).thenReturn(nodesFault); + graphService.checkRoute(Stream.of("v1", "v2", "v3", "v7").collect(Collectors.toSet())); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/util/GraphUtilTests.java b/src/test/java/ru/resprojects/linkchecker/util/GraphUtilTests.java new file mode 100644 index 0000000..459f1ef --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/util/GraphUtilTests.java @@ -0,0 +1,332 @@ +package ru.resprojects.linkchecker.util; + +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.model.Edge; +import ru.resprojects.linkchecker.model.Node; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; +import static ru.resprojects.linkchecker.util.GraphUtil.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = "moc_test") +public class GraphUtilTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphUtilTests.class); + + private GraphDto graphDto; + + @Before + public void init() { + Set nodeGraphSet = Stream.of( + new NodeGraph(5000, "v1", 0), + new NodeGraph(5001, "v2", 0), + new NodeGraph(5002, "v3", 0), + new NodeGraph(5003, "v4", 0), + new NodeGraph(5004, "v5", 0) + ).collect(Collectors.toSet()); + Set edgeGraphSet = Stream.of( + new EdgeGraph(5005, "v1", "v2"), + new EdgeGraph(5006, "v1", "v3"), + new EdgeGraph(5007, "v1", "v5"), + new EdgeGraph(5008, "v3", "v4") + ).collect(Collectors.toSet()); + graphDto = new GraphDto(nodeGraphSet, edgeGraphSet); + } + + @Test + public void generateRandomNodeFaultTest() { + Set nodeGraphSet = new HashSet<>(); + IntStream.range(0, 23).forEach(v -> nodeGraphSet.add(new NodeGraph(5000 + v, "v" + v, 0))); + Map result = getRandomNodeFault(nodeGraphSet); + Assert.assertNotNull(result); + result.forEach((key, value) -> LOG.debug("Node: " + key + " is fault = " + value)); + long countOfFault = result.entrySet().stream().filter(Map.Entry::getValue).count(); + LOG.debug("Count of fault elements = " + countOfFault); + } + + @Test + public void returnEmptyMapFromGenerateRandomNodeFault() { + Map result = getRandomNodeFault(null); + Assert.assertTrue(result.isEmpty()); + result = getRandomNodeFault(new HashSet<>()); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void getEdgesFromGraphDtoTest() { + Set actual = getEdgesFromGraphDto(graphDto); + Assert.assertNotNull(actual); + assertThat(actual).isNotEmpty(); + Assert.assertEquals(graphDto.getEdges().size(), actual.size()); + Assert.assertTrue(actual.stream() + .anyMatch(edge -> edge.getId().equals( + graphDto.getEdges().iterator().next().getId()) + ) + ); + actual.forEach(edge -> LOG.debug(edge.toString())); + } + + @Test + public void getEdgesFromGraphDtoSkipEdgeGraph() { + EdgeGraph eg = new EdgeGraph(5009, "v3", "v7"); + graphDto.getEdges().add(eg); + Set actual = getEdgesFromGraphDto(graphDto); + Assert.assertNotNull(actual); + Assert.assertTrue(actual.stream().noneMatch( + edge -> edge.getId().equals(eg.getId())) + ); + LOG.debug("EdgeGraph collection:"); + graphDto.getEdges().forEach(edgeGraph -> LOG.debug(edgeGraph.toString())); + LOG.debug("Edge collection:"); + actual.forEach(edge -> LOG.debug(edge.toString())); + } + + @Test + public void edgeToEdgeGraphTest() { + Edge edge = new Edge(3, new Node(1, "v1", 0), + new Node(2, "v2", 0)); + EdgeGraph actual = edgeToEdgeGraph(edge); + Assert.assertNotNull(actual); + Assert.assertEquals(edge.getId(), actual.getId()); + } + + @Test + public void edgeToEdgeGraphReturnNull() { + Assert.assertNull(edgeToEdgeGraph(null)); + } + + @Test + public void edgesToEdgeGraphsTest() { + Set edges = Stream.of( + new Edge(3, new Node(1, "v1", 0), + new Node(2, "v2", 0)), + new Edge(6, new Node(4, "v1", 0), + new Node(5, "v3", 0)) + ).collect(Collectors.toSet()); + Set actual = edgesToEdgeGraphs(edges); + Assert.assertNotNull(actual); + assertThat(actual).isNotEmpty(); + Assert.assertEquals(edges.size(), actual.size()); + for (EdgeGraph edgeGraph : actual) { + Assert.assertTrue(edges.stream() + .anyMatch(e -> e.getId().equals(edgeGraph.getId()))); + } + } + + @Test + public void edgesToEdgeGraphsReturnEmptyCollection() { + assertThat(edgesToEdgeGraphs(null)).isEmpty(); + } + + @Test + public void nodeGraphToNodeTest() { + NodeGraph nodeGraph = graphDto.getNodes().iterator().next(); + Node node = nodeGraphToNode(nodeGraph); + Assert.assertNotNull(node); + Assert.assertEquals(nodeGraph.getId(), node.getId()); + } + + @Test + public void nodeGraphToNodeReturnNull() { + Assert.assertNull(nodeGraphToNode(null)); + } + + @Test + public void nodeToNodeGraphTest() { + Node node = new Node(1, "v1", 0); + NodeGraph actual = nodeToNodeGraph(node); + Assert.assertNotNull(actual); + Assert.assertEquals(node.getId(), node.getId()); + } + + @Test + public void nodeToNodeGraphReturnNull() { + Assert.assertNull(nodeToNodeGraph(null)); + } + + @Test + public void nodeGraphsToNodesTest() { + Set actual = nodeGraphsToNodes(graphDto.getNodes()); + Assert.assertNotNull(actual); + assertThat(actual).isNotEmpty(); + Assert.assertEquals(graphDto.getNodes().size(), actual.size()); + for (Node node : actual) { + Assert.assertTrue(graphDto.getNodes().stream() + .anyMatch(n -> n.getId().equals(node.getId()))); + } + } + + @Test + public void nodeGraphsToNodesReturnEmptyCollection() { + assertThat(nodeGraphsToNodes(null)).isEmpty(); + } + + @Test + public void nodesToNodeGraphsTest() { + Set nodes = Stream.of( + new Node(1, "v1", 0), + new Node(2, "v2", 0) + ).collect(Collectors.toSet()); + Set actual = nodesToNodeGraphs(nodes); + Assert.assertNotNull(actual); + assertThat(actual).isNotEmpty(); + for (NodeGraph nodeGraph : actual) { + Assert.assertTrue(nodes.stream() + .anyMatch(n -> n.getId().equals(nodeGraph.getId()))); + } + } + + @Test + public void nodesToNodeGraphsReturnEmptyCollection() { + assertThat(nodesToNodeGraphs(null)).isEmpty(); + } + + @Test + public void exportToGraphVizTest() { + String actual = exportToGraphViz(graphDto); + assertThat(actual).isNotEmpty(); + assertThat(actual).contains("strict graph"); + } + + @Test + public void exportToGraphVizReturnNull() { + assertThat(exportToGraphViz(null)).isEmpty(); + } + + @Test + public void graphBuilderTest() { + Graph actual = graphBuilder(graphDto.getNodes(), + graphDto.getEdges()); + Assert.assertNotNull(actual); + Assert.assertEquals(actual.vertexSet().size(), graphDto.getNodes().size()); + Assert.assertEquals(actual.edgeSet().size(), graphDto.getEdges().size()); + LOG.debug(actual.toString()); + } + + @Test + public void graphBuilderSkipNullElementFromNodesAndEdges() { + graphDto.getNodes().add(null); + graphDto.getEdges().add(null); + Graph actual = graphBuilder(graphDto.getNodes(), + graphDto.getEdges()); + Assert.assertNotNull(actual); + Assert.assertNotEquals(actual.vertexSet().size(), graphDto.getNodes().size()); + Assert.assertNotEquals(actual.edgeSet().size(), graphDto.getEdges().size()); + LOG.debug(actual.toString()); + } + + @Test + public void graphBuilderSkipEdgeWithNonExistsNodes() { + graphDto.getEdges().add(new EdgeGraph(5009, "v10", "v12")); + Graph actual = graphBuilder(graphDto.getNodes(), + graphDto.getEdges()); + Assert.assertNotNull(actual); + Assert.assertNotEquals(actual.edgeSet().size(), graphDto.getEdges().size()); + LOG.debug(actual.toString()); + } + + @Test + public void graphBuilderReturnEmptyGraph() { + Graph actual = graphBuilder(null, + graphDto.getEdges()); + assertThat(actual.vertexSet()).isEmpty(); + assertThat(actual.edgeSet()).isEmpty(); + LOG.debug(actual.toString()); + actual = graphBuilder(graphDto.getNodes(), null); + assertThat(actual.vertexSet()).isEmpty(); + assertThat(actual.edgeSet()).isEmpty(); + LOG.debug(actual.toString()); + actual = graphBuilder(null, null); + assertThat(actual.vertexSet()).isEmpty(); + assertThat(actual.edgeSet()).isEmpty(); + LOG.debug(actual.toString()); + } + + @Test + public void graphToGraphDtoTest() { + GraphDto actual = graphToGraphDto(graphBuilder(graphDto.getNodes(), + graphDto.getEdges())); + assertThat(actual.getNodes()).isNotEmpty(); + assertThat(actual.getEdges()).isNotEmpty(); + Assert.assertEquals(graphDto.getNodes().size(), actual.getNodes().size()); + Assert.assertEquals(graphDto.getEdges().size(), actual.getEdges().size()); + for (NodeGraph nodeGraph : actual.getNodes()) { + Assert.assertTrue(graphDto.getNodes().stream() + .anyMatch(ng -> ng.equals(nodeGraph))); + } + // Because method graphToGraphDto is return edges without IDs , + // IDs is not checked. + for (EdgeGraph edgeGraph : actual.getEdges()) { + Assert.assertTrue(graphDto.getEdges().stream() + .anyMatch(eg -> eg.getNodeOne().equals(edgeGraph.getNodeOne()) + && eg.getNodeTwo().equals(edgeGraph.getNodeTwo())) + ); + } + LOG.debug(actual.toString()); + } + + @Test + public void graphToGraphDtoReturnEmptyGraphDto() { + GraphDto actual = graphToGraphDto(null); + assertThat(actual.getNodes()).isEmpty(); + assertThat(actual.getEdges()).isEmpty(); + } + + @Test + public void removeCyclesFromGraphTest() { + GraphDto cyclesGraph = new GraphDto(); + cyclesGraph.getNodes().addAll(graphDto.getNodes()); + cyclesGraph.getEdges().addAll(graphDto.getEdges()); + cyclesGraph.getEdges().add(new EdgeGraph(5009, "v2", "v4")); + cyclesGraph.getEdges().add(new EdgeGraph(5010, "v2", "v3")); + cyclesGraph.getEdges().add(new EdgeGraph(5011, "v3", "v5")); + cyclesGraph.getEdges().add(new EdgeGraph(5012, "v3", "v4")); + cyclesGraph.getEdges().add(new EdgeGraph(5013, "v5", "v4")); + GraphDto actual = graphToGraphDto(removeCyclesFromGraph(graphBuilder( + cyclesGraph.getNodes(), cyclesGraph.getEdges()))); + Assert.assertEquals(graphDto.getNodes().size(), actual.getNodes().size()); + Assert.assertEquals(graphDto.getEdges().size(), actual.getEdges().size()); + } + + @Test + public void removeCyclesFromGraphCyclesNotFound() { + GraphDto actual = graphToGraphDto(removeCyclesFromGraph(graphBuilder( + graphDto.getNodes(), graphDto.getEdges()))); + for (NodeGraph nodeGraph : actual.getNodes()) { + Assert.assertTrue(graphDto.getNodes().stream() + .anyMatch(ng -> ng.equals(nodeGraph))); + } + // Because method graphToGraphDto is return edges without IDs , + // IDs is not checked. + for (EdgeGraph edgeGraph : actual.getEdges()) { + Assert.assertTrue(graphDto.getEdges().stream() + .anyMatch(eg -> eg.getNodeOne().equals(edgeGraph.getNodeOne()) + && eg.getNodeTwo().equals(edgeGraph.getNodeTwo())) + ); + } + } + + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestControllerTests.java b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestControllerTests.java new file mode 100644 index 0000000..2167023 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphEdgeRestControllerTests.java @@ -0,0 +1,310 @@ +package ru.resprojects.linkchecker.web.rest; + +import com.google.gson.reflect.TypeToken; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.util.exeptions.ErrorInfo; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +public class GraphEdgeRestControllerTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphRestControllerTests.class); + + @Autowired + private MockMvc mvc; + + @Autowired + private AppProperties properties; + + @Test + public void addNewEdge() throws Exception { + EdgeGraph newEdge = new EdgeGraph("v1", "v4"); + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newEdge))).andReturn(); + Assert.assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus()); + EdgeGraph returnedEdge = TestUtils.mapFromJson(result.getResponse().getContentAsString(), EdgeGraph.class); + Assert.assertNotNull(returnedEdge); + Assert.assertEquals(newEdge.getNodeOne(), returnedEdge.getNodeOne()); + Assert.assertEquals(newEdge.getNodeTwo(), returnedEdge.getNodeTwo()); + Assert.assertNotNull(returnedEdge.getId()); + } + + @Test + public void addNewEdgeValidationException() throws Exception { + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(new EdgeGraph()))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.VALIDATION_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.APP, error.getPlace()); + LOG.info(Arrays.asList(error.getMessages()).toString()); + } + + @Test + public void addNewEdgeAlreadyPresentException() throws Exception { + EdgeGraph newEdge = new EdgeGraph("v1", "v2"); + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newEdge))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), + newEdge.getNodeOne(), newEdge.getNodeTwo(), + newEdge.getNodeTwo(), newEdge.getNodeOne()))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewEdges() throws Exception { + Set newEdges = Stream.of( + new EdgeGraph("v1", "v4"), + new EdgeGraph("v2", "v4") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newEdges))).andReturn(); + Assert.assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus()); + Type listType = new TypeToken>() {}.getType(); + Set returnedEdges = TestUtils.mapFromJson(result.getResponse().getContentAsString(), listType); + Assert.assertEquals(newEdges.size(), returnedEdges.size()); + Assert.assertTrue(returnedEdges.stream().anyMatch(ng -> ng.getNodeOne().equals("v1") && ng.getNodeTwo().equals("v4"))); + } + + @Test + public void addNewEdgesEmptyCollectionException() throws Exception { + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(Collections.emptySet()))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_EMPTY"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewEdgesCollectionContainNullObjectException() throws Exception { + Set newEdges = Stream.of( + null, + new EdgeGraph("v1", "v4"), + new EdgeGraph("v2", "v4") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newEdges))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewEdgesCollectionContainAlreadyPresentNodeException() throws Exception { + EdgeGraph newEdge = new EdgeGraph("v1", "v2"); + Set newEdges = Stream.of( + newEdge, + new EdgeGraph("v1", "v4"), + new EdgeGraph("v2", "v4") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newEdges))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getEdgeMsg().get("EDGE_MSG_ALREADY_PRESENT_ERROR"), + newEdge.getNodeOne(), newEdge.getNodeTwo(), + newEdge.getNodeTwo(), newEdge.getNodeOne()))); + LOG.info(errMsgs.toString()); + } + + @Test + public void getEdges() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.edgesGraph))); + } + + @Test + public void getEdgeById() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byId/5005").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.edgeGraph))); + } + + @Test + public void getEdgesByNodeName() throws Exception { + Set expected = TestUtils.edgesGraph.stream() + .filter(eg -> eg.getId() != 5008) + .collect(Collectors.toSet()); + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(expected))); + } + + @Test + public void getEdgeByNodeNames() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v1&nodeTwo=v2").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.edgeGraph))); + } + + @Test + public void getEdgeByIdNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), ErrorPlaceType.EDGE, 5050))); + } + + @Test + public void getEdgesByNameNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName/v100") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), "v100"))); + } + + @Test + public void deleteAllEdges() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(Collections.EMPTY_SET))); + } + + @Test + public void deleteEdgeById() throws Exception { + Set expected = TestUtils.edgesGraph.stream() + .filter(eg -> eg.getId() != 5005) + .collect(Collectors.toSet()); + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byId/5005").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(expected))); + } + + @Test + public void deleteEdgeByIdNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), ErrorPlaceType.EDGE, 5050))); + } + + @Test + public void deleteEdgesByNodeName() throws Exception { + Set expected = TestUtils.edgesGraph.stream() + .filter(eg -> eg.getId() != 5008) + .collect(Collectors.toSet()); + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName/v4").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(expected))); + } + + @Test + public void deleteEdgesByNodeNameNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName/v50") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_BY_NAME_ERROR"), "v50"))); + } + + @Test + public void deleteEdgeByNodeNames() throws Exception { + Set expected = TestUtils.edgesGraph.stream() + .filter(eg -> eg.getId() != 5005) + .collect(Collectors.toSet()); + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v1&nodeTwo=v2").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(expected))); + } + + @Test + public void deleteEdgeByNodeNamesNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v50&nodeTwo=v2") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.EDGE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getEdgeMsg().get("EDGE_MSG_GET_ERROR"), "v50", "v2"))); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestControllerTests.java b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestControllerTests.java new file mode 100644 index 0000000..cbaab1c --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphNodeRestControllerTests.java @@ -0,0 +1,333 @@ +package ru.resprojects.linkchecker.web.rest; + +import com.google.gson.reflect.TypeToken; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.util.exeptions.ErrorInfo; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +public class GraphNodeRestControllerTests { + + private static final Logger LOG = LoggerFactory.getLogger(GraphNodeRestControllerTests.class); + + @Autowired + private MockMvc mvc; + + @Autowired + private AppProperties properties; + + @Test + public void getNodes() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.nodesGraph))); + } + + @Test + public void getNodeById() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byId/5000").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.nodeGraph))); + } + + @Test + public void getNodeByName() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.nodeGraph))); + } + + @Test + public void getNodeByNameNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byName/v10") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v10"))); + } + + @Test + public void getNodeByIdNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getAppMsg().get("MSG_BY_ID_ERROR"), ErrorPlaceType.NODE, 5050))); + } + + @Test + public void addNewNodeToGraph() throws Exception { + NodeGraph newNode = new NodeGraph("v6"); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))).andReturn(); + Assert.assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus()); + NodeGraph returnedNode = TestUtils.mapFromJson(result.getResponse().getContentAsString(), NodeGraph.class); + Assert.assertNotNull(returnedNode); + Assert.assertEquals(newNode.getName(), returnedNode.getName()); + Assert.assertNotNull(returnedNode.getId()); + } + + @Test + public void addNewNodesToGraph() throws Exception { + Set newNodes = Stream.of( + new NodeGraph("v6"), + new NodeGraph("v7"), + new NodeGraph("v8") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNodes))).andReturn(); + Assert.assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus()); + Type listType = new TypeToken>() {}.getType(); + Set returnedNodes = TestUtils.mapFromJson(result.getResponse().getContentAsString(), listType); + Assert.assertEquals(newNodes.size(), returnedNodes.size()); + Assert.assertTrue(returnedNodes.stream().anyMatch(ng -> ng.getName().equals("v6"))); + } + + @Test + public void addNewNodeValidationException() throws Exception { + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(new NodeGraph()))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.VALIDATION_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.APP, error.getPlace()); + LOG.info(Arrays.asList(error.getMessages()).toString()); + } + + @Test + public void addNewNodeAlreadyPresentException() throws Exception { + NodeGraph newNode = new NodeGraph("v1"); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"), newNode.getName()))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewNodesEmptyCollectionException() throws Exception { + Set newNodes = Collections.emptySet(); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNodes))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_EMPTY"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewNodesCollectionContainNullObjectException() throws Exception { + Set newNodes = Stream.of( + null, + new NodeGraph("v7"), + new NodeGraph("v8") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNodes))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_NULL"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void addNewNodesCollectionContainAlreadyPresentNodeException() throws Exception { + Set newNodes = Stream.of( + new NodeGraph("v1"), + new NodeGraph("v7"), + new NodeGraph("v8") + ).collect(Collectors.toSet()); + MvcResult result = this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNodes))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getNodeMsg().get("NODE_MSG_ALREADY_PRESENT_ERROR"),"v1"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void deleteAllNodes() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(Collections.EMPTY_SET))); + } + + @Test + public void deleteNodeById() throws Exception { + Set nodes = TestUtils.nodesGraph.stream().filter(ng -> ng.getId() != 5000).collect(Collectors.toSet()); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byId/5000").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(nodes))); + } + + @Test + public void deleteNodeByName() throws Exception { + Set nodes = TestUtils.nodesGraph.stream().filter(ng -> !ng.getName().equals("v1")).collect(Collectors.toSet()); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(nodes))); + } + + @Test + public void deleteNodeByObject() throws Exception { + Set nodes = TestUtils.nodesGraph.stream().filter(ng -> !ng.getName().equals("v1")).collect(Collectors.toSet()); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(TestUtils.nodeGraph))) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(nodes))); + } + + @Test + public void deleteNodeByIdNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getAppMsg().get("MSG_BY_ID_ERROR"), ErrorPlaceType.NODE, 5050))); + LOG.info(errMsgs.toString()); + } + + @Test + public void deleteNodeByNameNotFoundException() throws Exception { + MvcResult result = this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byName/v10") + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v10"))); + LOG.info(errMsgs.toString()); + } + + @Test + public void deleteNodeByObjectWithNullIdNotFoundException() throws Exception { + NodeGraph newNode = new NodeGraph(null, "v10", 0); + MvcResult result = this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), newNode.toString()))); + LOG.info(errMsgs.toString()); + } + + @Test + public void deleteNodeByObjectNotFoundException() throws Exception { + NodeGraph newNode = new NodeGraph(5020, "v10", 0); + MvcResult result = this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), newNode.toString()))); + LOG.info(errMsgs.toString()); + } + + @Test + public void deleteNodeByObjectWithIncorrectIdException() throws Exception { + NodeGraph newNode = new NodeGraph(5000, "v10", 0); + MvcResult result = this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.NODE, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format( + properties.getNodeMsg().get("NODE_MSG_BY_OBJECT_ERROR"), newNode.toString()))); + LOG.info(errMsgs.toString()); + } + + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerMockTests.java b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerMockTests.java new file mode 100644 index 0000000..ce2a73d --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerMockTests.java @@ -0,0 +1,47 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.services.GraphService; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +@RunWith(SpringRunner.class) +@WebMvcTest(GraphRestController.class) +public class GraphRestControllerMockTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private GraphService graphService; + + @Test + public void checkRouteInGraph() throws Exception { + List route = Stream.of("v1", "v2", "v3").collect(Collectors.toList()); + String returnedResult = String.format("Route for nodes %s is found", route.toString()); + given(this.graphService.checkRoute(anySet())).willReturn(returnedResult); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))).andReturn(); + Assert.assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); + Assert.assertEquals(result.getResponse().getContentAsString(), returnedResult); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerTests.java b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerTests.java new file mode 100644 index 0000000..06c5762 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/GraphRestControllerTests.java @@ -0,0 +1,173 @@ +package ru.resprojects.linkchecker.web.rest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.resprojects.linkchecker.AppProperties; +import ru.resprojects.linkchecker.LinkcheckerApplication; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.util.exeptions.ErrorInfo; +import ru.resprojects.linkchecker.util.exeptions.ErrorPlaceType; +import ru.resprojects.linkchecker.util.exeptions.ErrorType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.resprojects.linkchecker.dto.GraphDto.NodeGraph; +import static ru.resprojects.linkchecker.dto.GraphDto.EdgeGraph; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LinkcheckerApplication.class) +@ActiveProfiles(profiles = {"test", "debug"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +public class GraphRestControllerTests { + + @Autowired + private MockMvc mvc; + + @Autowired + private AppProperties properties; + + @Test + public void getGraph() throws Exception { + this.mvc.perform(get(GraphRestController.REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(TestUtils.graph))); + } + + @Test + public void exportGraphToGraphVizFormat() throws Exception { + MvcResult result = this.mvc.perform(get(GraphRestController.REST_URL + "/export").accept(MediaType.TEXT_HTML_VALUE)) + .andReturn(); + Assert.assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); + String content = result.getResponse().getContentAsString(); + Assert.assertFalse(content.isEmpty()); + Assert.assertTrue(content.contains("strict graph G")); + } + + @Test + public void createGraph() throws Exception { + Set nodesGraph = TestUtils.nodesGraph.stream() + .map(ng -> new GraphDto.NodeGraph(ng.getName())) + .collect(Collectors.toSet()); + Set edgesGraph = TestUtils.edgesGraph.stream() + .map(eg -> new EdgeGraph(eg.getNodeOne(), eg.getNodeTwo())) + .collect(Collectors.toSet()); + GraphDto graph = new GraphDto(nodesGraph, edgesGraph); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(graph))).andReturn(); + Assert.assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus()); + GraphDto returnedGraph = TestUtils.mapFromJson(result.getResponse().getContentAsString(), GraphDto.class); + Assert.assertNotNull(returnedGraph); + Assert.assertEquals(nodesGraph.size(), returnedGraph.getNodes().size()); + Assert.assertEquals(edgesGraph.size(), returnedGraph.getEdges().size()); + Assert.assertNotNull(returnedGraph.getNodes().iterator().next().getId()); + Assert.assertNotNull(returnedGraph.getEdges().iterator().next().getId()); + } + + @Test + public void createGraphEmptyNodeCollectionException() throws Exception { + Set nodesGraph = Collections.emptySet(); + Set edgesGraph = TestUtils.edgesGraph.stream() + .map(eg -> new EdgeGraph(eg.getNodeOne(), eg.getNodeTwo())) + .collect(Collectors.toSet()); + GraphDto graph = new GraphDto(nodesGraph, edgesGraph); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(graph))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.GRAPH, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains("NODES: " + properties.getAppMsg().get("MSG_COLLECTION_EMPTY"))); + } + + @Test + public void deleteGraph() throws Exception { + this.mvc.perform(delete(GraphRestController.REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + this.mvc.perform(get(GraphRestController.REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(TestUtils.mapToJson(new GraphDto()))); + } + + @Test + public void getOptions() throws Exception { + MvcResult result = this.mvc.perform(options(GraphRestController.REST_URL) + .accept(MediaType.APPLICATION_JSON)).andReturn(); + Assert.assertTrue(result.getResponse().containsHeader("Allow")); + Assert.assertEquals("GET,POST,DELETE,OPTIONS", result.getResponse().getHeader("Allow")); + } + + @Test + public void checkRouteEmptyInputCollectionException() throws Exception { + List route = new ArrayList<>(); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.GRAPH, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_EMPTY"))); + } + + @Test + public void checkRouteNotEnoughDataException() throws Exception { + List route = Collections.singletonList("v1"); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_ERROR, error.getType()); + Assert.assertEquals(ErrorPlaceType.GRAPH, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(properties.getAppMsg().get("MSG_COLLECTION_CONTAIN_ONE_ELEMENT"))); + } + + @Test + public void checkRouteNotFoundException() throws Exception { + List route = Stream.of("v7", "v2", "v1").collect(Collectors.toList()); + MvcResult result = this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))).andReturn(); + Assert.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY.value(), result.getResponse().getStatus()); + ErrorInfo error = TestUtils.mapFromJson(result.getResponse().getContentAsString(), ErrorInfo.class); + Assert.assertEquals(ErrorType.DATA_NOT_FOUND, error.getType()); + Assert.assertEquals(ErrorPlaceType.GRAPH, error.getPlace()); + List errMsgs = Arrays.asList(error.getMessages()); + Assert.assertTrue(errMsgs.contains(String.format(properties.getNodeMsg().get("NODE_MSG_BY_NAME_ERROR"), "v7"))); + } + + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphApiDocumentation.java b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphApiDocumentation.java new file mode 100644 index 0000000..81893d8 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphApiDocumentation.java @@ -0,0 +1,186 @@ +package ru.resprojects.linkchecker.web.rest.apidocs; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.web.rest.GraphRestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +@AutoConfigureRestDocs(outputDir = "target/generated-snippets") +public class GraphApiDocumentation { + + @Autowired + private MockMvc mvc; + + @Test + public void getGraph() throws Exception { + this.mvc.perform(MockMvcRequestBuilders.get(GraphRestController.REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(getGraphResponseDoc("get-graph")); + } + + @Test + public void exportGraphToGraphVizFormat() throws Exception { + this.mvc.perform(get(GraphRestController.REST_URL + "/export").accept(MediaType.TEXT_HTML_VALUE)) + .andExpect(status().isOk()) + .andDo(document("export-graph")); + } + + @Test + public void createGraph() throws Exception { + String jsonGraph = "{\n" + + " \"nodes\":[\n" + + " {\"name\":\"v1\"},\n" + + " {\"name\":\"v2\"},\n" + + " {\"name\":\"v3\"},\n" + + " {\"name\":\"v4\"},\n" + + " {\"name\":\"v5\"}\n" + + " ],\n" + + " \"edges\":[\n" + + " {\"nodeOne\":\"v1\",\"nodeTwo\":\"v2\"},\n" + + " {\"nodeOne\":\"v2\",\"nodeTwo\":\"v3\"},\n" + + " {\"nodeOne\":\"v3\",\"nodeTwo\":\"v4\"},\n" + + " {\"nodeOne\":\"v3\",\"nodeTwo\":\"v5\"},\n" + + " {\"nodeOne\":\"v5\",\"nodeTwo\":\"v4\"},\n" + + " {\"nodeOne\":\"v5\",\"nodeTwo\":\"v2\"}\n" + + " ]\n" + + "}"; + this.mvc.perform(post(GraphRestController.REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonGraph)) + .andExpect(status().isCreated()) + .andDo(document("create-graph", + requestFields( + fieldWithPath("nodes") + .description("Коллекция вершин графа (ноды)"), + fieldWithPath("nodes[].name") + .description("Уникальное имя вершины графа"), + fieldWithPath("edges") + .description("Коллекция рёбер графа"), + fieldWithPath("edges[].nodeOne") + .description("Уникальное имя вершины графа"), + fieldWithPath("edges[].nodeTwo") + .description("Уникальное имя вершины графа") + ))) + .andDo(getGraphResponseDoc("create-graph")); + } + + @Test + public void createGraphEmptyNodeCollectionException() throws Exception { + Set nodesGraph = Collections.emptySet(); + Set edgesGraph = TestUtils.edgesGraph.stream() + .map(eg -> new GraphDto.EdgeGraph(eg.getNodeOne(), eg.getNodeTwo())) + .collect(Collectors.toSet()); + GraphDto graph = new GraphDto(nodesGraph, edgesGraph); + this.mvc.perform(post(GraphRestController.REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(graph))) + .andDo(ErrorResponseDoc("create-graph-exception")); + } + + @Test + public void checkRouteEmptyInputCollectionException() throws Exception { + List route = new ArrayList<>(); + this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))) + .andDo(ErrorResponseDoc( "checkroute-graph-exception-1")); + } + + @Test + public void checkRouteNotEnoughDataException() throws Exception { + List route = Collections.singletonList("v1"); + this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))) + .andDo(ErrorResponseDoc( "checkroute-graph-exception-2")); + } + + @Test + public void checkRouteNotFoundException() throws Exception { + List route = Stream.of("v7", "v2", "v1").collect(Collectors.toList()); + this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))) + .andDo(ErrorResponseDoc( "checkroute-graph-exception-3")); + } + + @Test + public void deleteGraph() throws Exception { + this.mvc.perform(delete(GraphRestController.REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-graph")); + } + + private RestDocumentationResultHandler getGraphResponseDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("nodes") + .description("Коллекция вершин графа (ноды)"), + fieldWithPath("nodes[].id") + .description("Идентификатор вершины графа"), + fieldWithPath("nodes[].name") + .description("Уникальное имя вершины графа"), + fieldWithPath("nodes[].counter") + .description("Колличество проходов через узел"), + fieldWithPath("edges") + .description("Коллекция рёбер графа"), + fieldWithPath("edges[].id") + .description("Уникальный идентификатор ребра графа"), + fieldWithPath("edges[].nodeOne") + .description("Уникальное имя вершины графа"), + fieldWithPath("edges[].nodeTwo") + .description("Уникальное имя вершины графа") + )); + } + + public static RestDocumentationResultHandler ErrorResponseDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("url") + .description("REST-запрос, при котором возникла ошибка"), + fieldWithPath("type") + .description("тип ошибки"), + fieldWithPath("place") + .description("места возникновения ошибок"), + fieldWithPath("messages") + .description("сообщения об ошибках") + )); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphCheckRouteApiDocumentation.java b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphCheckRouteApiDocumentation.java new file mode 100644 index 0000000..4842b17 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphCheckRouteApiDocumentation.java @@ -0,0 +1,50 @@ +package ru.resprojects.linkchecker.web.rest.apidocs; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.services.GraphService; +import ru.resprojects.linkchecker.web.rest.GraphRestController; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@WebMvcTest(GraphRestController.class) +@AutoConfigureRestDocs(outputDir = "target/generated-snippets") +public class GraphCheckRouteApiDocumentation { + + @Autowired + private MockMvc mvc; + + @MockBean + private GraphService graphService; + + @Test + public void checkRouteInGraph() throws Exception { + List route = Stream.of("v1", "v2", "v3").collect(Collectors.toList()); + String returnedResult = String.format("Route for nodes %s is found", route.toString()); + given(this.graphService.checkRoute(anySet())).willReturn(returnedResult); + this.mvc.perform(post(GraphRestController.REST_URL + "/checkroute") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(route))) + .andExpect(status().isOk()) + .andDo(document("checkroute-graph")) + ; + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphEdgeApiDocumentation.java b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphEdgeApiDocumentation.java new file mode 100644 index 0000000..8c2920e --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphEdgeApiDocumentation.java @@ -0,0 +1,237 @@ +package ru.resprojects.linkchecker.web.rest.apidocs; + +import com.google.gson.reflect.TypeToken; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import ru.resprojects.linkchecker.web.rest.GraphEdgeRestController; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +@AutoConfigureRestDocs(outputDir = "target/generated-snippets") +public class GraphEdgeApiDocumentation { + + @Autowired + private MockMvc mvc; + + @Test + public void getEdges() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseEdgesDoc("get-edges")); + } + + @Test + public void getEdgeById() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byId/5005").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseEdgeDoc("get-edge-by-id")); + } + + @Test + public void getEdgesByNodeName() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseEdgesDoc("get-edges-by-node-name")); + } + + @Test + public void getEdgeByNodeNames() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v1&nodeTwo=v2").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseEdgeDoc("get-edge-by-nodes-name")); + } + + @Test + public void getEdgeByIdNotFoundException() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("get-edge-exception-1")); + } + + @Test + public void getEdgesByNameNotFoundException() throws Exception { + this.mvc.perform(get(GraphEdgeRestController.EDGE_REST_URL + "/byName/v100") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("get-edge-exception-2")); + } + + @Test + public void addNewEdge() throws Exception { + String jsonEdge = "{\"nodeOne\": \"v1\", \"nodeTwo\": \"v4\"}"; + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonEdge)) + .andExpect(status().isCreated()) + .andDo(document("create-edge", + requestFields( + fieldWithPath("nodeOne") + .description("Уникальное имя вершины графа"), + fieldWithPath("nodeTwo") + .description("Уникальное имя вершины графа") + ))) + .andDo(ResponseEdgeDoc("create-edge")); + } + + @Test + public void addNewEdges() throws Exception { + String jsonEdge = "[{\"nodeOne\": \"v1\", \"nodeTwo\": \"v4\"},{\"nodeOne\": \"v2\", \"nodeTwo\": \"v4\"}]"; + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonEdge)) + .andExpect(status().isCreated()) + .andDo(document("create-edges", + requestFields( + fieldWithPath("[]") + .description("Коллекция рёбер графа"), + fieldWithPath("[].nodeOne") + .description("Уникальное имя вершины графа"), + fieldWithPath("[].nodeTwo") + .description("Уникальное имя вершины графа") + ))) + .andDo(ResponseEdgesDoc("create-edges")); + } + + @Test + public void addNewEdgeValidationException() throws Exception { + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{\"nodeOne\": \"\", \"nodeTwo\": \"\"}")) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-edge-exception-1")); + } + + @Test + public void addNewEdgeAlreadyPresentException() throws Exception { + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{\"nodeOne\": \"v1\", \"nodeTwo\": \"v2\"}")) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-edge-exception-2")); + } + + @Test + public void addNewEdgesEmptyCollectionException() throws Exception { + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content("[]")) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-edge-exception-3")); + } + + @Test + public void addNewEdgesCollectionContainNullObjectException() throws Exception { + String jsonEdge = "[null,{\"nodeOne\": \"v2\", \"nodeTwo\": \"v4\"}]"; + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonEdge)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-edge-exception-4")); + } + + @Test + public void addNewEdgesCollectionContainAlreadyPresentNodeException() throws Exception { + String jsonEdge = "[{\"nodeOne\": \"v1\", \"nodeTwo\": \"v2\"},{\"nodeOne\": \"v1\", \"nodeTwo\": \"v4\"},{\"nodeOne\": \"v2\", \"nodeTwo\": \"v4\"}]"; + this.mvc.perform(post(GraphEdgeRestController.EDGE_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonEdge)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-edge-exception-5")); + } + + @Test + public void deleteAllEdges() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-all-edges")); + } + + @Test + public void deleteEdgeById() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byId/5005").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-edge-by-id")); + } + + @Test + public void deleteEdgesByNodeName() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName/v4").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-edges-by-node-name")); + } + + @Test + public void deleteEdgeByNodeNames() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v1&nodeTwo=v2").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-edge-by-nodes-name")); + } + + @Test + public void deleteEdgeByIdNotFoundException() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-edge-exception-1")); + } + + @Test + public void deleteEdgesByNodeNameNotFoundException() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName/v50") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-edge-exception-2")); + } + + @Test + public void deleteEdgeByNodeNamesNotFoundException() throws Exception { + this.mvc.perform(delete(GraphEdgeRestController.EDGE_REST_URL + "/byName?nodeOne=v50&nodeTwo=v2") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-edge-exception-3")); + } + + private static RestDocumentationResultHandler ResponseEdgeDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("id") + .description("Идентификатор ребра графа"), + fieldWithPath("nodeOne") + .description("Уникальное имя первой вершины графа, которое связывается текущим ребром"), + fieldWithPath("nodeTwo") + .description("Уникальное имя второй вершины графа, которое связывается текущим ребром") + )); + } + + private static RestDocumentationResultHandler ResponseEdgesDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("[]") + .description("Коллекция рёбер"), + fieldWithPath("[].id") + .description("Идентификатор ребра графа"), + fieldWithPath("[].nodeOne") + .description("Уникальное имя первой вершины графа, которое связывается текущим ребром"), + fieldWithPath("[].nodeTwo") + .description("Уникальное имя второй вершины графа, которое связывается текущим ребром") + )); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphNodeApiDocumentation.java b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphNodeApiDocumentation.java new file mode 100644 index 0000000..b112f5b --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/apidocs/GraphNodeApiDocumentation.java @@ -0,0 +1,242 @@ +package ru.resprojects.linkchecker.web.rest.apidocs; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import ru.resprojects.linkchecker.TestUtils; +import ru.resprojects.linkchecker.dto.GraphDto; +import ru.resprojects.linkchecker.web.rest.GraphNodeRestController; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +@AutoConfigureMockMvc +@AutoConfigureRestDocs(outputDir = "target/generated-snippets") +public class GraphNodeApiDocumentation { + + @Autowired + private MockMvc mvc; + + @Test + public void getNodes() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseNodesDoc("get-nodes")); + } + + @Test + public void getNodeById() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byId/5000").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseNodeDoc("get-node-by-id")); + } + + @Test + public void getNodeByName() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(ResponseNodeDoc("get-node-by-name")); + } + + @Test + public void getNodeByNameNotFoundException() throws Exception { + this.mvc.perform(get(GraphNodeRestController.NODES_REST_URL + "/byName/v10") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("get-node-by-name-exception")); + } + + @Test + public void addNewNodeToGraph() throws Exception { + String jsonNode = "{\"name\": \"v6\"}"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNode)) + .andExpect(status().isCreated()) + .andDo(document("create-node", + requestFields( + fieldWithPath("name") + .description("Уникальное имя вершины графа") + ))) + .andDo(ResponseNodeDoc("create-node")); + } + + @Test + public void addNewNodesToGraph() throws Exception { + String jsonNodes = "[{\"name\":\"v6\"}, {\"name\":\"v7\"}, {\"name\":\"v8\"}]"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNodes)) + .andDo(document("create-nodes", + requestFields( + fieldWithPath("[]") + .description("Коллекция вершин графа (ноды)"), + fieldWithPath("[].name") + .description("Уникальное имя вершины графа") + ))) + .andDo(ResponseNodesDoc("create-nodes")) + ; + } + + @Test + public void addNewNodeValidationException() throws Exception { + String jsonNode = "{\"name\":\"\"}"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNode)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-node-exception-1")); + } + + @Test + public void addNewNodeAlreadyPresentException() throws Exception { + String jsonNode = "{\"name\": \"v1\"}"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNode)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-node-exception-2")); + } + + @Test + public void addNewNodesEmptyCollectionException() throws Exception { + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content("[]")) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-node-exception-3")); + } + + @Test + public void addNewNodesCollectionContainNullObjectException() throws Exception { + String jsonNodes = "[null, {\"name\": \"v7\"}, {\"name\": \"v8\"}]"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNodes)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-node-exception-4")); + } + + @Test + public void addNewNodesCollectionContainAlreadyPresentNodeException() throws Exception { + String jsonNodes = "[{\"name\":\"v1\"}, {\"name\":\"v7\"}, {\"name\":\"v8\"}]"; + this.mvc.perform(post(GraphNodeRestController.NODES_REST_URL + "/create/byBatch") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(jsonNodes)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("create-node-exception-5")); + } + + @Test + public void deleteAllNodes() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-all-nodes")); + } + + @Test + public void deleteNodeById() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byId/5000").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-node-by-id")); + } + + @Test + public void deleteNodeByName() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byName/v1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(document("delete-node-by-name")); + } + + @Test + public void deleteNodeByObject() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(TestUtils.nodeGraph))) + .andExpect(status().isNoContent()) + .andDo(document("delete-node-by-obj")); + } + + @Test + public void deleteNodeByIdNotFoundException() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byId/5050") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-node-exception-1")); + } + + @Test + public void deleteNodeByNameNotFoundException() throws Exception { + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byName/v10") + .accept(MediaType.APPLICATION_JSON)) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-node-exception-2")); + } + + @Test + public void deleteNodeByObjectWithNullIdNotFoundException() throws Exception { + GraphDto.NodeGraph newNode = new GraphDto.NodeGraph(null, "v10", 0); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-node-exception-3")); + } + + @Test + public void deleteNodeByObjectNotFoundException() throws Exception { + GraphDto.NodeGraph newNode = new GraphDto.NodeGraph(5020, "v10", 0); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-node-exception-4")); + } + + @Test + public void deleteNodeByObjectWithIncorrectIdException() throws Exception { + GraphDto.NodeGraph newNode = new GraphDto.NodeGraph(5000, "v10", 0); + this.mvc.perform(delete(GraphNodeRestController.NODES_REST_URL + "/byObj") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(TestUtils.mapToJson(newNode))) + .andDo(GraphApiDocumentation.ErrorResponseDoc("delete-node-exception-5")); + } + + private static RestDocumentationResultHandler ResponseNodeDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("id") + .description("Идентификатор вершины графа"), + fieldWithPath("name") + .description("Уникальное имя вершины графа"), + fieldWithPath("counter") + .description("Колличество проходов через вершину графа") + )); + } + + private static RestDocumentationResultHandler ResponseNodesDoc(String documentIdentifier) { + return document(documentIdentifier, + responseFields( + fieldWithPath("[]") + .description("Коллекция вершин графа (ноды)"), + fieldWithPath("[].id") + .description("Идентификатор вершины графа"), + fieldWithPath("[].name") + .description("Уникальное имя вершины графа"), + fieldWithPath("[].counter") + .description("Колличество проходов через вершину графа") + )); + } + +} diff --git a/src/test/java/ru/resprojects/linkchecker/web/rest/json/JsonGraphDtoTests.java b/src/test/java/ru/resprojects/linkchecker/web/rest/json/JsonGraphDtoTests.java new file mode 100644 index 0000000..f8c39e1 --- /dev/null +++ b/src/test/java/ru/resprojects/linkchecker/web/rest/json/JsonGraphDtoTests.java @@ -0,0 +1,99 @@ +package ru.resprojects.linkchecker.web.rest.json; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.GsonTester; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.linkchecker.dto.GraphDto; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.resprojects.linkchecker.TestUtils.*; + +@RunWith(SpringRunner.class) +@JsonTest +public class JsonGraphDtoTests { + + @Autowired + private GsonTester jsonGraph; + + @Autowired + private GsonTester jsonNodeGraph; + + @Autowired + private GsonTester jsonEdgeGraph; + + @Autowired + private GsonTester> jsonNodesGraph; + + @Autowired + private GsonTester> jsonEdgesGraph; + + + @Test + public void serializeJsonGraphDto() throws Exception { + assertThat(this.jsonGraph.write(graph)).isEqualTo("graph.json"); + assertThat(this.jsonGraph.write(graph)).isEqualToJson("graph.json"); + assertThat(this.jsonGraph.write(graph)).hasJsonPathArrayValue("@.nodes"); + assertThat(this.jsonGraph.write(graph)).hasJsonPathArrayValue("@.edges"); + assertThat(this.jsonGraph.write(graph)) + .extractingJsonPathArrayValue("@.nodes") + .hasSameSizeAs(graph.getNodes()); + assertThat(this.jsonGraph.write(graph)) + .extractingJsonPathArrayValue("@.edges") + .hasSameSizeAs(graph.getEdges()); + } + + @Test + public void serializeJsonNodes() throws Exception { + assertThat(this.jsonNodesGraph.write(nodesGraph)).isEqualTo("nodes.json"); + assertThat(this.jsonNodesGraph.write(nodesGraph)).isEqualToJson("nodes.json"); + } + + @Test + public void serializeJsonEdges() throws Exception { + assertThat(this.jsonEdgesGraph.write(edgesGraph)).isEqualTo("edges.json"); + assertThat(this.jsonEdgesGraph.write(edgesGraph)).isEqualToJson("edges.json"); + } + + @Test + public void serializeJsonNode() throws Exception { + assertThat(this.jsonNodeGraph.write(nodeGraph)).isEqualTo("node.json"); + assertThat(this.jsonNodeGraph.write(nodeGraph)).isEqualToJson("node.json"); + assertThat(this.jsonNodeGraph.write(nodeGraph)).extractingJsonPathStringValue("@.name") + .isEqualTo(nodeGraph.getName()); + assertThat(this.jsonNodeGraph.write(nodeGraph)) + .extractingJsonPathNumberValue("@.id") + .isEqualTo(nodeGraph.getId()); + } + + @Test + public void serializeJsonEdge() throws Exception { + assertThat(this.jsonEdgeGraph.write(edgeGraph)).isEqualTo("edge.json"); + assertThat(this.jsonEdgeGraph.write(edgeGraph)).isEqualToJson("edge.json"); + assertThat(this.jsonEdgeGraph.write(edgeGraph)).extractingJsonPathStringValue("@.nodeOne") + .isEqualTo(edgeGraph.getNodeOne()); + assertThat(this.jsonEdgeGraph.write(edgeGraph)) + .extractingJsonPathStringValue("@.nodeTwo") + .isEqualTo(edgeGraph.getNodeTwo()); + assertThat(this.jsonEdgeGraph.write(edgeGraph)) + .extractingJsonPathNumberValue("@.id") + .isEqualTo(edgeGraph.getId()); + } + + @Test + public void deserializeJsonNode() throws Exception { + String content = "{\"id\": 5000, \"name\": \"v1\", \"counter\": 0}"; + assertThat(this.jsonNodeGraph.parse(content)).isEqualTo(nodeGraph); + } + + @Test + public void deserializeJsonEdge() throws Exception { + String content = "{\"id\": 5005, \"nodeOne\": \"v1\", \"nodeTwo\": \"v2\"}"; + assertThat(this.jsonEdgeGraph.parse(content)).isEqualTo(edgeGraph); + } + +} diff --git a/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edge.json b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edge.json new file mode 100644 index 0000000..d303cbc --- /dev/null +++ b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edge.json @@ -0,0 +1 @@ +{"id": 5005, "nodeOne": "v1", "nodeTwo": "v2"} \ No newline at end of file diff --git a/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edges.json b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edges.json new file mode 100644 index 0000000..7f0b334 --- /dev/null +++ b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/edges.json @@ -0,0 +1,22 @@ +[ + { + "id": 5006, + "nodeOne": "v1", + "nodeTwo": "v3" + }, + { + "id": 5005, + "nodeOne": "v1", + "nodeTwo": "v2" + }, + { + "id": 5007, + "nodeOne": "v1", + "nodeTwo": "v5" + }, + { + "id": 5008, + "nodeOne": "v3", + "nodeTwo": "v4" + } +] \ No newline at end of file diff --git a/src/test/resources/ru/resprojects/linkchecker/web/rest/json/graph.json b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/graph.json new file mode 100644 index 0000000..10ab01d --- /dev/null +++ b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/graph.json @@ -0,0 +1,51 @@ +{ + "nodes": [ + { + "id": 5000, + "name": "v1", + "counter": 0 + }, + { + "id": 5001, + "name": "v2", + "counter": 0 + }, + { + "id": 5002, + "name": "v3", + "counter": 0 + }, + { + "id": 5003, + "name": "v4", + "counter": 0 + }, + { + "id": 5004, + "name": "v5", + "counter": 0 + } + ], + "edges": [ + { + "id": 5006, + "nodeOne": "v1", + "nodeTwo": "v3" + }, + { + "id": 5005, + "nodeOne": "v1", + "nodeTwo": "v2" + }, + { + "id": 5007, + "nodeOne": "v1", + "nodeTwo": "v5" + }, + { + "id": 5008, + "nodeOne": "v3", + "nodeTwo": "v4" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/ru/resprojects/linkchecker/web/rest/json/node.json b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/node.json new file mode 100644 index 0000000..c67380f --- /dev/null +++ b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/node.json @@ -0,0 +1 @@ +{"id": 5000, "name": "v1", "counter": 0} \ No newline at end of file diff --git a/src/test/resources/ru/resprojects/linkchecker/web/rest/json/nodes.json b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/nodes.json new file mode 100644 index 0000000..21c4dd0 --- /dev/null +++ b/src/test/resources/ru/resprojects/linkchecker/web/rest/json/nodes.json @@ -0,0 +1,27 @@ +[ + { + "id": 5000, + "name": "v1", + "counter": 0 + }, + { + "id": 5001, + "name": "v2", + "counter": 0 + }, + { + "id": 5002, + "name": "v3", + "counter": 0 + }, + { + "id": 5003, + "name": "v4", + "counter": 0 + }, + { + "id": 5004, + "name": "v5", + "counter": 0 + } +] \ No newline at end of file diff --git a/wiki_files/.gitkeep b/wiki_files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/wiki_files/db_graph_storage.png b/wiki_files/db_graph_storage.png new file mode 100644 index 0000000..0f495cb Binary files /dev/null and b/wiki_files/db_graph_storage.png differ